This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
Beginner
Dieses Tutorial führt dich durch die Entwicklung eines der bekanntesten Spieleklassiker, die du nachbauen kannst. Es gibt viele Varianten dieses Spiels. In dieser Variante frisst eine Schlange „Futter“ und wächst nur beim Fressen. Außerdem bewegt sie sich über ein Spielfeld mit Hindernissen.
![]()
In diesem Tutorial lernst du, wie du:
Dieses Tutorial richtet sich an Einsteiger. Wenn du aber mit Defold und der Spieleentwicklung noch gar nicht vertraut bist, empfehlen wir dir, zuerst einige einführende Handbücher zu lesen, insbesondere über Defolds Bausteine und das Glossar. Falls du Defold noch nicht heruntergeladen hast, sieh dir das Installationshandbuch an. Empfehlenswert ist auch die Übersicht des Editors, um dich schnell im Editor zurechtzufinden. Hier zeigen wir aber ebenfalls Bildschirmaufnahmen zu jedem Schritt.
Starte Defold und:

Erledigt!
Zuerst legen wir die Auflösung des Spiels fest.
game.project. Doppelklicke darauf, um sie zu öffnen.game.project.Width und Height) auf 768 × 768 oder ein anderes Vielfaches von 16.
Das ist sinnvoll, weil das Spiel auf einem Raster gezeichnet wird, in dem jedes Segment 16 × 16 Pixel groß ist. So schneidet der Spielbildschirm keine Segmente teilweise ab. Die Datei game.project enthält alle wichtigen Einstellungen des Projekts. Du kannst sie im Handbuch zu den Projekteinstellungen nachlesen.
Erledigt!
Für einen minimalistischen Snake-Klon brauchst du nur wenig Grafik: ein grünes Segment mit 16 × 16 Pixeln für die Schlange, einen weißen Block für die Hindernisse und einen kleineren roten Block, der das Futter darstellt.
Erstelle zuerst im Defold-Editor ein Verzeichnis für die Assets:
mainNew Folder.assets ein und klicke auf Create Folder.
Erledigt!
Das folgende Bild ist das einzige Asset, das du brauchst:


Weitere Einzelheiten zum Importieren von Assets findest du hier.
Erledigt!
Defold bietet eine integrierte Komponente (component) für Kachelkarten, mit der du das Spielfeld aus Kacheln erstellst, die in einem Raster angeordnet sind. Eine Kachelkarte ermöglicht es dir, einzelne Kacheln zu setzen und auszulesen, was perfekt zu diesem Spiel passt. Da Kachelkarten ihre Grafiken aus einer Kachelquelle (tile source) beziehen, musst du eine erstellen:
assets.New ▸ Tile Source im Abschnitt „Resources“.snake.tilesource).
Die Kachelquelle öffnet sich in einem eigenen Kachelquelleneditor für diesen Dateityp. Du wirst aufgefordert, ein Bild dafür anzugeben, damit sie funktioniert. Auf der rechten Seite findest du den Bereich Properties:
Setze die Eigenschaft Image auf die gerade importierte Grafikdatei.

Die Eigenschaften Width und Height sollten auf 16 (dem Standardwert) bleiben. Dadurch wird das 32 × 32 Pixel große Bild in 4 Kacheln mit den Nummern 1–4 aufgeteilt.

Beachte, dass die Eigenschaft Extrude Borders auf 2 Pixel gesetzt ist. Das verhindert Bildfehler an den Rändern von Kacheln, deren Grafik bis ganz an den Rand reicht.
Wenn du eine Datei änderst, erscheint neben ihrem Namen auf ihrer Registerkarte ein Sternchen *. Wähle File ▸ Save All oder verwende die Tastenkombination Ctrl+S (⌘Cmd + S auf dem Mac), um alle Dateien zu speichern.
Erledigt!
Deine Kachelquelle ist jetzt einsatzbereit. Es ist also Zeit, die Kachelkartenkomponente für das Spielfeld zu erstellen:
Klicke mit der rechten Maustaste auf den Ordner main und wähle New ▸ Tile Map im Abschnitt „Components“. Nenne die neue Datei „grid“ (der Editor speichert die Datei als „grid.tilemap“).

Sie öffnet sich in einem Kachelkarteneditor, der hervorhebt, dass eine Tile Source benötigt wird. Setze deshalb die Eigenschaft Tile Source auf die zuvor erstellte Datei „snake.tilesource“.

Erledigt!
Defold speichert nur den tatsächlich verwendeten Bereich der Kachelkarte. Du musst daher genügend Kacheln hinzufügen, um den Bildschirm bis zu seinen Grenzen auszufüllen.
Outline auf der rechten Seite die Ebene layer1.Wähle den Menüpunkt Edit ▸ Select Tile... oder die Taste Space, um die Kachelpalette anzuzeigen. Klicke dann auf die Kachel, die du zum Zeichnen verwenden möchtest.


Du brauchst eine Kachelkarte mit 48 × 48 Kacheln, um unseren Spielbildschirm auszufüllen (denn unsere Anzeige ist 768 Pixel groß und unsere Kacheln sind 16 Pixel groß, also 768/16 = 48).
Speichere die Kachelkarte, wenn du fertig bist.
Erledigt!
Jetzt müssen wir unsere Kachelkarte zum Spiel hinzufügen. Wenn du mit Defolds Bausteinen vertraut bist, weißt du: Komponenten sind Teil von Spielobjekten (game objects), und Spielobjekte können in Sammlungen (collections) definiert werden.
Öffne main.collection, indem du im Bereich Assets darauf doppelklickst. In der Vorlage Empty Project ist dies standardmäßig die Startsammlung (bootstrap collection), die beim Start der Engine geladen wird.
Klicke mit der rechten Maustaste auf die Wurzel in Outline und wähle Add Game Object. Dadurch entsteht ein neues Spielobjekt in der Sammlung, die beim Spielstart geladen wird.

Klicke mit der rechten Maustaste auf das neue Spielobjekt und wähle Add Component File. Wähle die gerade erstellte Datei „grid.tilemap“.

Jetzt haben wir eine Kachelkarte in unserer Spielsammlung. Sie sollte sichtbar sein, wenn du das Spiel aus dem Editor startest.
Project ▸ Build oder verwende die Tastenkombination Ctrl + B (⌘Cmd + B auf dem Mac).
Erledigt!
Klicke mit der rechten Maustaste im Browser Assets auf den Ordner main und wähle New ▸ Script im Abschnitt Scripts. Nenne die neue Skriptdatei „snake“ (sie wird als „snake.script“ gespeichert). Diese Datei wird die gesamte Spiellogik enthalten.

Kehre zu main.collection zurück und klicke mit der rechten Maustaste auf das Spielobjekt, das die Kachelkarte enthält. Wähle Add Component File und dann die Datei „snake.script“.

Jetzt sind die Kachelkartenkomponente und das Skript eingerichtet.
Erledigt!
Das Skript, das du jetzt schreibst, wird das gesamte Spiel steuern. Wir fügen die Funktionen nach und nach hinzu.
Die Idee dafür ist folgende:
Öffne snake.script und suche die Funktion init(). Die Engine ruft diese Funktion auf, wenn das Skript beim Spielstart initialisiert wird. Ändere den Code wie folgt:
function init(self)
self.segments = { -- <1>
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0} -- <2>
self.speed = 7.0 -- <3>
self.time = 0 -- <4>
end
In diesem Code führen wir Folgendes aus:
self.segments. Sie enthält eine Liste von Tabellen, die jeweils die X- und Y-Position eines Segments speichern.self.dir, die eine X- und eine Y-Richtung enthält.self.speed, angegeben in Kacheln pro Sekunde.self.time einen Zeitgeberwert, mit dem wir die Bewegungsgeschwindigkeit steuern werden.Der Skriptcode oben ist in der Sprache Lua geschrieben. Es gibt ein paar Dinge am Code zu beachten, aber wenn du etwas davon noch nicht verstehst, mach dir keine Sorgen. Mach einfach mit, experimentiere und lass dir Zeit — irgendwann wirst du es verstehen. Für den Moment kannst du dir merken, dass wir in init() nur die Variablen initialisiert haben, die wir verwenden werden.
self eine Referenz auf die aktuelle Instanz der Skriptkomponente. Die Referenz self wird zum Speichern von Instanzdaten verwendet.self wie eine Lua-Tabelle verwenden, in der du Daten speicherst. Verwende dafür einfach die Punktnotation wie bei jeder anderen Tabelle: self.data = "value". Die Referenz ist während der gesamten Lebensdauer des Skripts gültig, in diesem Fall vom Spielstart bis zum Beenden des Spiels.{} umschlossen.{x = 10, y = 20}), verschachtelte Lua-Tabellen ({ {a = 1}, {b = 2} }) oder andere Datentypen sein.Erledigt!
Die Funktion init() wird genau einmal aufgerufen, wenn die Skriptkomponente im laufenden Spiel instanziiert wird. Die Funktion update() hingegen wird einmal pro Frame aufgerufen. Damit eignet sie sich ideal für Spiellogik in Echtzeit.
Die Idee für die Aktualisierung ist, in einem festgelegten Zeitabstand Folgendes zu tun:

:::sidenote Denk daran, dass der Kopf unserer Schlange am Ende der Tabelle steht und das Schwanzende am Anfang. :::
update() in snake.script und ändere den Code wie folgt:function update(self, dt)
self.time = self.time + dt -- <1>
if self.time >= 1.0 / self.speed then -- <2>
local head = self.segments[#self.segments] -- <3>
local newhead = {
x = head.x + self.dir.x,
y = head.y + self.dir.y
} -- <4>
table.insert(self.segments, newhead) -- <5>
local tail = table.remove(self.segments, 1) -- <6>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0) -- <7>
for i, s in ipairs(self.segments) do -- <8>
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2) -- <9>
end
self.time = 0 -- <10>
end
end
In diesem Code führen wir Folgendes aus:
update() — die sogenannte „delta time“, kurz dt.# ist der Operator, mit dem du die Länge einer Tabelle ermittelst, sofern sie als Array verwendet wird. Das ist bei uns der Fall — alle Segmente sind Tabellenwerte ohne angegebenen Schlüssel.self.dir) ein neues Kopfsegment.#grid hat nur 1 Ebene namens layer1.i die Position in der Tabelle (beginnend bei 1) und s das aktuelle Segment.Wenn du das Spiel jetzt startest, solltest du sehen, wie die 4 Segmente lange Schlange von links nach rechts über das Spielfeld kriecht.

Erledigt!
Bevor du Code hinzufügst, der auf Spielereingaben reagiert, musst du die Eingabezuordnungen einrichten.
input die Datei game.input_binding und öffne sie mit einem Doppelklick.
Die Eingabebindungsdatei ordnet tatsächliche Benutzereingaben (Tasten, Mausbewegungen usw.) den Namen von Aktionen zu. Diese werden an Skripte weitergegeben, die Eingaben angefordert haben.
Erledigt!
Wenn die Bindungen eingerichtet sind, öffne snake.script und füge am Anfang der Funktion init() die folgende Zeile hinzu:
function init(self)
msg.post(".", "acquire_input_focus") -- <1>
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
end
Die hinzugefügte Zeile:
Suche dann die Funktion on_input und gib den folgenden Code ein:
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then -- <1>
self.dir.x = 0 -- <2>
self.dir.y = 1
elseif action_id == hash("down") and action.pressed then
self.dir.x = 0
self.dir.y = -1
elseif action_id == hash("left") and action.pressed then
self.dir.x = -1
self.dir.y = 0
elseif action_id == hash("right") and action.pressed then
self.dir.x = 1
self.dir.y = 0
end
end
Diese if...elseif...-Verzweigungen führen Folgendes aus:
pressed in der Tabelle action auf true gesetzt ist (der Spieler hat die Taste gedrückt), dann:Starte das Spiel erneut und prüfe, ob du die Schlange steuern kannst.
Erledigt!
Beachte nun, dass das gleichzeitige Drücken zweier Tasten zu zwei Aufrufen von on_input() führt, einem für jeden Tastendruck. Im oben gezeigten Code wirkt sich nur der letzte Aufruf auf die Richtung der Schlange aus, weil nachfolgende Aufrufe von on_input() die Werte in self.dir überschreiben.
Beachte außerdem, dass die Schlange in sich selbst hineinsteuert, wenn sie sich nach links bewegt und du die Taste right drückst. Die scheinbar naheliegende Lösung ist, den if-Bedingungen in on_input() eine weitere Bedingung hinzuzufügen:
if action_id == hash("up") and self.dir.y ~= -1 and action.pressed then
...
elseif action_id == hash("down") and self.dir.y ~= 1 and action.pressed then
...
Wenn sich die Schlange jedoch nach links bewegt und der Spieler vor dem nächsten Bewegungsschritt schnell zuerst up und dann right drückt, wirkt sich nur der Tastendruck auf right aus, und die Schlange bewegt sich in sich selbst hinein. Mit den oben ergänzten Bedingungen in den if-Abfragen wird die Eingabe ignoriert. Nicht gut!
Eine geeignete Lösung ist, die Eingaben in einer Warteschlange zu speichern und bei jeder Bewegung der Schlange Einträge daraus zu entnehmen:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.speed = 7.0
self.time = 0
self.dirqueue = {} -- <1>
end
Diesmal haben wir:
self.dirqueue hinzugefügt, die als leere Tabelle initialisiert wird.Ergänze die Funktion update() wie folgt:
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed then
local newdir = table.remove(self.dirqueue, 1) -- <1>
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y -- <2>
if not opposite then
self.dir = newdir -- <3>
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 0)
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
newdir ist nicht null), prüfe, ob newdir in die entgegengesetzte Richtung zu self.dir zeigt.Ändere außerdem on_input so, dass es die aktuelle Eingabe stattdessen in der Warteschlange speichert:
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1}) -- <1>
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
self.dir direkt zu setzen.Starte das Spiel und prüfe, ob es sich wie erwartet spielt.
Erledigt!
Die Schlange braucht Futter auf der Karte, damit sie lang und schnell werden kann. Fügen wir es hinzu!
Füge oberhalb der Funktion init() eine neue Funktion hinzu:
local function put_food(self) -- <1>
self.food = {x = math.random(2, 47), y = math.random(2, 47)} -- <2>
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3) -- <3>
end
In dieser Funktion führen wir Folgendes aus:
put_food(), die ein Stück Futter auf der Karte platziert.self.food.Rufe sie dann am Ende der Funktion init() auf:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
math.randomseed(socket.gettime()) -- <1>
put_food(self) -- <2>
end
math.random() Zufallswerte abrufst. Sonst wird dieselbe Folge von Zufallswerten erzeugt. Dieser Startwert sollte nur einmal gesetzt werden.put_food() beim Spielstart auf, damit sich zu Beginn ein Stück Futter auf der Karte befindet.Erledigt!
Um jetzt zu erkennen, ob die Schlange mit etwas kollidiert, musst du nur nachsehen, was sich auf der Kachelkarte an der Position befindet, auf die die Schlange zusteuert, und darauf reagieren.
Füge eine Variable hinzu, die festhält, ob die Schlange lebt oder nicht:
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true -- <1>
math.randomseed(socket.gettime())
put_food(self)
end
Füge dann Logik hinzu, die auf Kollisionen mit Wänden/Hindernissen und Futter prüft:
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then -- <1>
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y) -- <2>
if tile == 2 or tile == 4 then
self.alive = false -- <3>
elseif tile == 3 then
self.speed = self.speed + 1 -- <4>
put_food(self)
else
local tail = table.remove(self.segments, 1) -- <5>
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
Probiere das Spiel jetzt aus und stelle sicher, dass es sich gut spielt!
Damit ist das Tutorial abgeschlossen. Experimentiere aber gern weiter mit dem Spiel und bearbeite einige der folgenden Übungen!
Erledigt!
Hier ist der vollständige Skriptcode zum Nachschlagen:
local function put_food(self)
self.food = {x = math.random(2, 47), y = math.random(2, 47)}
tilemap.set_tile("#grid", "layer1", self.food.x, self.food.y, 3)
end
function init(self)
msg.post(".", "acquire_input_focus")
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24}
}
self.dir = {x = 1, y = 0}
self.dirqueue = {}
self.speed = 7.0
self.time = 0
self.alive = true
math.randomseed(socket.gettime())
put_food(self)
end
function update(self, dt)
self.time = self.time + dt
if self.time >= 1.0 / self.speed and self.alive then
local newdir = table.remove(self.dirqueue, 1)
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y
if not opposite then
self.dir = newdir
end
end
local head = self.segments[#self.segments]
local newhead = {x = head.x + self.dir.x, y = head.y + self.dir.y}
table.insert(self.segments, newhead)
local tile = tilemap.get_tile("#grid", "layer1", newhead.x, newhead.y)
if tile == 2 or tile == 4 then
self.alive = false
elseif tile == 3 then
self.speed = self.speed + 1
put_food(self)
else
local tail = table.remove(self.segments, 1)
tilemap.set_tile("#grid", "layer1", tail.x, tail.y, 1)
end
for i, s in ipairs(self.segments) do
tilemap.set_tile("#grid", "layer1", s.x, s.y, 2)
end
self.time = 0
end
end
function on_input(self, action_id, action)
if action_id == hash("up") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = 1})
elseif action_id == hash("down") and action.pressed then
table.insert(self.dirqueue, {x = 0, y = -1})
elseif action_id == hash("left") and action.pressed then
table.insert(self.dirqueue, {x = -1, y = 0})
elseif action_id == hash("right") and action.pressed then
table.insert(self.dirqueue, {x = 1, y = 0})
end
end
Es ist eine gute Übung, diese Verbesserungen umzusetzen: