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
Dieses Spiel ist eine Variante klassischer Kombinationsspiele wie Bejeweled und Candy Crush. Durch Ziehen verbindet der Spieler Blöcke derselben Farbe, um sie zu entfernen. Das Ziel ist allerdings nicht, lange Reihen gleichfarbiger Blöcke zu entfernen, das Spielfeld zu leeren oder Punkte zu sammeln, sondern mehrere besondere „magische Blöcke“, die über das Spielfeld verteilt sind, miteinander zu verbinden.
Dieses Tutorial ist eine Schritt-für-Schritt-Anleitung, in der wir das Spiel auf Grundlage eines fertigen Designs erstellen. In der Praxis kostet es viel Zeit und Mühe, ein funktionierendes Design zu finden. Vielleicht beginnst du mit einer Grundidee und suchst dann nach einer Möglichkeit, sie als Prototyp umzusetzen, um ihr Potenzial besser zu verstehen. Selbst ein einfaches Spiel wie „Magic Link“ erfordert einiges an Designarbeit. Dieses Spiel durchlief mehrere Überarbeitungen und Experimente, bis es seine endgültige (und immer noch längst nicht perfekte) Form und seine Spielregeln hatte. Für dieses Tutorial überspringen wir diesen Prozess aber und beginnen direkt mit der Umsetzung des fertigen Designs.
Zunächst musst du ein neues Projekt erstellen und das Asset-Paket importieren:

Das Spielfeld wird in jeder Runde zufällig mit farbigen Blöcken und mehreren magischen Blöcken gefüllt. Für die farbigen Blöcke gelten diese Regeln:
Die magischen Blöcke verhalten sich anders. Für sie gelten diese Regeln:
Für die Interaktion des Spielers mit dem Spiel gelten die folgenden Regeln:
Der Schwierigkeitsgrad bestimmt die Anzahl der magischen Blöcke, die auf dem Spielfeld platziert werden.
Wie bei jedem Projekt brauchen wir einen groben Plan für die Umsetzung. Es gibt viele Möglichkeiten, das Spiel zu strukturieren und aufzubauen. Technisch könnten wir das gesamte Spiel im GUI-System umsetzen, wenn wir wollten. Meist ist es jedoch naheliegend, ein Spiel mit Spielobjekten (game objects) und Sprites aufzubauen und die GUI-APIs für die Bildschirmoberfläche und eingeblendete Spielinformationen zu verwenden. Diesen Weg gehen wir hier.
Da wir mit einer recht geringen Anzahl von Dateien rechnen, halten wir die Ordnerstruktur des Projekts sehr einfach:

Die Datei game.project behält größtenteils ihre Standardeinstellungen, aber einige Einstellungen müssen wir festlegen. Zunächst brauchen wir eine Auflösung für das Spiel. Die Auflösung lässt sich später recht leicht ändern. Für ein fertiges Spiel müssen wir außerdem dafür sorgen, dass es unabhängig von der Auflösung und dem Seitenverhältnis des Zielgeräts gut aussieht.
Wir haben uns für eine Auflösung von 640 × 960 Pixeln entschieden, die native Auflösung des iPhone 4. Diese Auflösung passt auch auf viele Monitore, was Spieltests am Computer angenehm macht. Wenn du mit einer anderen Auflösung arbeiten möchtest, musst du nur einige Werte entsprechend anpassen.

Außerdem müssen wir die maximale Anzahl gerenderter Sprites erhöhen. Wenn du möchtest, kannst du zum nächsten Abschnitt springen und hierher zurückkehren, sobald dich die Konsole darauf hinweist, dass du die Sprite-Grenze erreicht hast.

Wir können die maximal benötigte Anzahl an Sprites berechnen:
Nehmen wir also an, dass wir höchstens 30 magische Blöcke haben. Das Spielfeld besteht aus 63 Blöcken (Sprites). Bei den 30 magischen Blöcken kommen jeweils 4 Sprites für Spezialeffekte hinzu. Das sind 120 zusätzliche Sprites. Zusammen mit den Verbindungsgrafiken (in diesem Fall höchstens 33) müssen wir also in jedem Frame mindestens 120 + 33 = 153 Sprites zeichnen. Die nächste Zweierpotenz ist 256.
Es reicht jedoch nicht aus, das Maximum auf 256 zu setzen. Jedes Mal, wenn wir das Spielfeld leeren und zurücksetzen, löschen wir alle aktuellen Spielobjekte und erzeugen neue. Die Sprite-Anzahl muss alle Objekte berücksichtigen, die während des Frames existieren. Dazu gehören auch gelöschte Objekte, da sie erst am Ende des Frames entfernt werden. Es genügt daher, die maximale Anzahl an Sprites auf 512 zu setzen.
![]()
Alle für das Spiel benötigten Assets wurden im Voraus vorbereitet. Wir fügen sie als Bilder mit 512 × 512 Pixeln hinzu und lassen die Engine sie auf die Zielgröße verkleinern.
Wenn du hidpi in den Projekteinstellungen aktivierst, erhält der Backbuffer eine hohe Auflösung. Große Bilder, die verkleinert gezeichnet werden, erscheinen auf Retina-Bildschirmen dadurch sehr scharf.

Neben den Blöcken sind ein „connector“-Bild und Effekt-Sprites enthalten. Außerdem gibt es zwei Hintergrundbilder: eines als Hintergrund für das Spielfeld und eines für das Hauptmenü. Füge alle Bilder zum Ordner images hinzu und erstelle dann eine Atlasdatei namens sprites.atlas. Öffne die Atlasdatei und füge alle Bilder hinzu.

Es gibt außerdem einige GUI-Bilder, mit denen GUI-Elemente wie Schaltflächen und Popups erstellt werden. Diese fügen wir einem separaten Atlas namens gui.atlas hinzu.
Im ersten Schritt erstellen wir die Spielfeldlogik. Das Spielfeld liegt in einer eigenen Sammlung (collection), die alles enthält, was während des Spiels auf dem Bildschirm zu sehen ist. Fürs Erste benötigen wir nur die Komponente (component) „blockfactory“ vom Typ Fabrik (factory) und das Skript. Später ergänzen wir eine Fabrik für Verbindungen, eine GUI-Komponente für das Hauptmenü und schließlich die Lademechanik, um das Spiel vom Hauptmenü aus zu starten, sowie eine Möglichkeit, zum Menü zurückzukehren.
board.collection im Ordner main. Gib der Sammlung den Namen „board“, damit wir sie später adressieren können. Wenn du die Sprite-Komponente für den Hintergrund hinzufügst, setze ihre Z-Position unbedingt auf -1. Andernfalls wird sie nicht hinter all den Blöcken gezeichnet, die wir später erzeugen./main/board.collection, damit wir leicht testen können.

Die Skriptdatei board.script enthält die gesamte Logik für das Spielfeld selbst und die Blöcke darauf. Erstelle zunächst die Funktion zum Aufbau des Spielfelds und rufe sie (vorübergehend) aus init() auf. Wir fügen außerdem zwei Funktionen hinzu, die wir jetzt noch nicht verwenden, die später aber nützlich sind:
filter()build_blocklist()Nach dem Aufbau des Spielfelds verwenden wir zwei verschiedene Datenbestände mit allen Blöcken, self.blocks und self.board:
-- board.script
go.property("timer", 0) -- Use to time events
local blocksize = 80 -- Distance between block centers
local edge = 40 -- Left and right edge.
local bottom_edge = 50 -- Bottom edge.
local boardwidth = 7 -- Number of columns
local boardheight = 9 -- Number of rows
local centeroff = vmath.vector3(8, -8, 0) -- Center offset for connector gfx since there's shadow below in the block img
local dropamount = 3 -- The number of blocks dropped on a "drop"
local colors = { hash("orange"), hash("pink"), hash("blue"), hash("yellow"), hash("green") }
--
-- filter(function, table)
-- e.g: filter(is_even, {1,2,3,4}) -> {2,4}
--
local function filter(func, tbl)
local new = {}
for i, v in pairs(tbl) do
if func(v) then
new[i] = v
end
end
return new
end
--
-- Build a list of blocks in 1 dimension for easy filtering
--
local function build_blocklist(self)
self.blocks = {}
for x, l in pairs(self.board) do
for y, b in pairs(self.board[x]) do
table.insert(self.blocks, { id = b.id, color = b.color, x = b.x, y = b.y })
end
end
end
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
msg.post(".", "acquire_input_focus")
msg.post("#", "start_level")
end
local function build_board(self)
math.randomseed(os.time())
local pos = vmath.vector3()
local c
local x = 0
local y = 0
for x = 0,boardwidth-1 do
pos.x = edge + blocksize / 2 + blocksize * x
self.board[x] = {}
for y = 0,boardheight-1 do
pos.y = bottom_edge + blocksize / 2 + blocksize * y
-- Calc z
pos.z = x * -0.1 + y * 0.01 -- <1>
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
self.board[x][y] = { id = id, color = c, x = x, y = y }
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
build_board(self)
end
end
Die Spielfeldlogik erzeugt „block“-Spielobjekte über die Fabrikkomponente „blockfactory“. Damit das funktioniert, müssen wir das Spielobjekt für den Block erstellen. Der Block besitzt ein Skript und ein Sprite. Wir setzen die Standardanimation des Sprites auf einen beliebigen farbigen Block in sprites.atlas und ergänzen dann block.script um Code, damit der Block beim Erzeugen die richtige Farbe annimmt:

-- block.script
go.property("color", hash("none"))
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
Setze Prototype der Fabrikkomponente „blockfactory“ auf die neue Spielobjektdatei block.go.

Jetzt solltest du das Spiel starten können und ein Spielfeld sehen, das mit zufällig gefärbten Blöcken gefüllt ist:

Jetzt haben wir ein Spielfeld und sollten die Interaktion mit dem Spieler hinzufügen. Zunächst definieren wir die Eingabebindungen in game.input_binding im Ordner input. Stelle sicher, dass die Einstellungen in game.project deine Eingabebindungsdatei verwenden.

Wir benötigen nur eine Bindung und weisen MOUSE_BUTTON_LEFT dem Aktionsnamen „touch“ zu. Dieses Spiel verwendet keine Mehrfacheingabe per Berührung. Praktischerweise übersetzt Defold Berührungseingaben mit einem Finger in Klicks mit der linken Maustaste.
Das Spielfeld übernimmt die Verarbeitung der Eingaben, deshalb müssen wir den entsprechenden Code in board.script ergänzen:
-- board.script
function on_input(self, action_id, action)
if action_id == hash("touch") and action.value == 1 then
-- What block was touched or dragged over?
local x = math.floor((action.x - edge) / blocksize)
local y = math.floor((action.y - bottom_edge) / blocksize)
if x < 0 or x >= boardwidth or y < 0 or y >= boardheight or self.board[x][y] == nil then
-- outside board.
return
end
if action.pressed then
-- Player started touch
msg.post(self.board[x][y].id, "make_orange")
self.dragging = true
elseif self.dragging then
-- then drag
msg.post(self.board[x][y].id, "make_green")
end
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
end
end
Die Nachrichten make_orange und make_green dienen nur vorübergehend als sichtbare Rückmeldung, dass der Code funktioniert. Wir müssen block.script um Code ergänzen, der diese Nachrichten verarbeitet:
-- block.script
function on_message(self, message_id, message, sender)
if message_id == hash("make_orange") then
sprite.play_flipbook("#sprite", hash("orange"))
elseif message_id == hash("make_green") then
sprite.play_flipbook("#sprite", hash("green"))
end
end
Jetzt erhalten die Blöcke zuerst eine make_orange-Nachricht und danach make_green-Nachrichten, solange du den Bildschirm berührst (oder die Maustaste gedrückt hältst). Daher flackern die Blöcke wahrscheinlich nur kurz orange auf (wenn überhaupt), bevor sie grün werden. Aber wir wissen nun, welchen Block der Spieler berührt! Wenn du die Verarbeitung der Eingabe genauer verfolgen möchtest, füge print()- oder pprint()-Aufrufe in den Code ein.
Jetzt brauchen wir Assets für die Markierung, die anzeigt, wenn der Spieler Blöcke miteinander verbindet. Die Idee ist, einfach eine Grafik über jeden Block zu legen, um seine Verbindung anzuzeigen.
Wir müssen ein Spielobjekt namens „connector“ erstellen, das das Sprite-Bild für die Verbindungen enthält, sowie eine Fabrikkomponente namens „connector factory“ im Spielobjekt „board“:


Das Skript für dieses Spielobjekt ist minimal. Es muss nur die Grafik passend zum restlichen Spiel skalieren und die Z-Reihenfolge richtig einstellen.
-- connector.script
function init(self)
go.set_scale_xy(0.18) -- Scale in 2D without changing Z.
go.set(".", "position.z", 1) -- Put on top.
end
Die Funktion same_color_neighbors() gibt eine Liste von Blöcken zurück, die an einen bestimmten Block (an der Position x, y) angrenzen und dieselbe Farbe haben. Sie verwendet die Funktion filter(), die auf die vollständige flache Liste der Blöcke in self.blocks angewendet wird.
-- board.script
--
-- Returns a list of neighbor blocks of the same color as the
-- block on x, y
--
local function same_color_neighbors(self, x, y)
local f = function (v)
return (v.id ~= self.board[x][y].id) and
(v.x == x or v.x == x - 1 or v.x == x + 1) and
(v.y == y or v.y == y - 1 or v.y == y + 1) and
(v.color == self.board[x][y].color)
end
return filter(f, self.blocks)
end
Die Hilfsfunktion in_blocklist() prüft, ob ein Block in einer Liste von Blöcken vorhanden ist:
-- board.script
--
-- Does the block exist in the list of blocks?
--
local function in_blocklist(blocks, block)
for i, b in pairs(blocks) do
if b.id == block then
return true
end
end
return false
end
Wir verwenden diese Funktionen bei Berührungs- und Zieheingaben in on_input(), um die berührten Blöcke zu einer Verbindungskette zusammenzustellen. Dabei prüfen wir bereits auf magische Blöcke und ignorieren sie, obwohl es noch keine gibt:
-- board.script
function on_input(self, action_id, action)
...
-- If trying to manipulate magic blocks, ignore.
if self.board[x][y].color == hash("magic") then
return
end
if action.pressed then
-- List of neighbors of the same color as touched block
self.neighbors = same_color_neighbors(self, x, y)
self.chain = {}
table.insert(self.chain, self.board[x][y])
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
self.dragging = true
elseif self.dragging then
-- then drag
if in_blocklist(self.neighbors, self.board[x][y].id) and not in_blocklist(self.chain, self.board[x][y].id) then
-- dragging over a same-colored neighbor
table.insert(self.chain, self.board[x][y])
self.neighbors = same_color_neighbors(self, x, y)
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
end
end
Zum Schluss entfernen wir beim Beenden der Berührung alle sichtbaren Verbindungsmarkierungen.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
end

Jetzt haben wir die Logik zum Verbinden gleichfarbiger Blöcke, und die verbundenen Blöcke zu entfernen ist einfach. Wir setzen die Position auf dem Spielfeld auf hash("removing") statt einfach auf nil, weil wir später bei der Logik für magische Blöcke sicherstellen müssen, dass diese nur an die Stellen gerade entfernter Blöcke gleiten. Wenn wir die Position auf dem Spielfeld hier auf nil setzen, können wir gerade entfernte Blöcke nicht von solchen unterscheiden, die schon zuvor entfernt wurden.
-- board.script
-- Remove the currently selected block-chain
--
local function remove_chain(self)
-- Delete all chained blocks
for i, c in ipairs(self.chain) do
self.board[c.x][c.y] = hash("removing")
go.delete(c.id)
end
self.chain = {}
end
Außerdem brauchen wir eine Funktion, die Positionen auf dem Spielfeld tatsächlich entfernt (auf nil setzt), wenn sie zuvor auf hash("removing") gesetzt wurden:
-- board.script
--
-- Set removed blocks to nil
--
local function nilremoved(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] == hash("removing") then
self.board[x][y] = nil
end
end
end
end
Wir erstellen auch eine Funktion, die die übrigen Blöcke nach unten gleiten lässt, wenn Blöcke darunter entfernt (auf nil gesetzt) werden. Wir durchlaufen das Spielfeld spaltenweise von links nach rechts und jede Spalte von unten nach oben. Wenn wir auf eine leere (nil) Position stoßen, lassen wir alle Blöcke darüber nach unten gleiten.
-- board.script
--
-- Apply shift-down logic to all blocks.
--
local function slide_board(self)
-- Slide all remaining blocks down into blank spots.
-- Going column by column makes this easy.
local dy = 0
local pos = vmath.vector3()
for x = 0,boardwidth - 1 do
dy = 0
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil then
if dy > 0 then
-- Move down dy steps
self.board[x][y - dy] = self.board[x][y]
self.board[x][y] = nil
-- Calc new position
self.board[x][y - dy].y = self.board[x][y - dy].y - dy
go.animate(self.board[x][y-dy].id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * (y - dy), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y-dy].id, "position.z", x * -0.1 + (y-dy) * 0.01)
end
else
dy = dy + 1
end
end
end
-- blocklist needs updating
build_blocklist(self)
end

Jetzt können wir diese Funktionen einfach in on_input() aufrufen, wenn die Berührung beendet wurde und sich Blöcke in self.chain befinden.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and slide the remaining blocks down.
remove_chain(self)
nilremoved(self)
slide_board(self)
end
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Jetzt ist es Zeit, die magischen Blöcke hinzuzufügen. Zuerst ermöglichen wir es, einen Block in einen magischen Block umzuwandeln. So können wir das gefüllte Spielfeld in einem separaten Durchlauf bearbeiten und die gewünschten Blöcke in magische verwandeln. Um die magischen Blöcke etwas interessanter zu gestalten, erstellen wir zunächst einen animierten magischen Effekt als Spielobjekt magic_fx.go, das wir vom magischen Block aus erzeugen können.

Dieses Spielobjekt enthält zwei Sprites. Eines stellt die Farbe „magic“ dar (ein Sprite mit dem Bild magic-sphere_layer2.png), das andere einen Lichteffekt namens „light“ (ein Sprite mit dem Bild magic-sphere_layer3.png). Das Objekt beginnt beim Erzeugen abhängig vom Wert der Eigenschaft direction zu rotieren. Außerdem reagiert es auf zwei Nachrichten: lights_on und lights_off, die das Sprite für den Lichteffekt steuern.
Erstelle ein neues Skript und füge es als Skriptkomponente zu magic_fx.go hinzu:
-- magic_fx.script
go.property("direction", hash("left"))
function init(self)
msg.post("#", "lights_off")
if self.direction == hash("left") then
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 3 + math.random())
else
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, -360, go.EASING_LINEAR, 2 + math.random())
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("lights_on") then
msg.post("#light", "enable")
elseif message_id == hash("lights_off") then
msg.post("#light", "disable")
end
end
Der magische Block erzeugt nun bei der Nachricht make_magic zwei magic_fx-Spielobjekte. Sie drehen sich in entgegengesetzte Richtungen und erzeugen dadurch ein schönes Farbenspiel im Inneren der Blöcke. Außerdem fügen wir block.go ein weiteres Sprite mit dem Bild magic-sphere_layer4.png hinzu. Dieses Bild erhält einen höheren Z-Wert als der erzeugte Effekt und zeichnet die Hülle oder Abdeckung („cover“) der magischen Kugel.

Beachte, dass wir dem Spielobjekt des Blocks eine Factory-Komponente hinzufügen und ihr unser Spielobjekt magic_fx.go als Prototype zuweisen müssen. Das Blockskript muss außerdem auf die Nachrichten lights_on und lights_off reagieren und sie an die erzeugten Objekte weiterleiten. Beachte auch, dass die erzeugten Objekte gelöscht werden müssen, wenn der Block gelöscht wird. Dafür sorgt die Funktion final() des Blocks. All das geschieht in block.script.
-- block.script
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
self.fx1 = nil
self.fx2 = nil
msg.post("#cover", "disable")
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
function final(self)
if self.fx1 ~= nil then
go.delete(self.fx1)
end
if self.fx2 ~= nil then
go.delete(self.fx2)
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("make_magic") then
self.color = hash("magic")
msg.post("#cover", "enable")
msg.post("#sprite", "enable")
sprite.play_flipbook("#sprite", hash("magic-sphere_layer1"))
self.fx1 = factory.create("#fxfactory", p, nil, { direction = hash("left") })
self.fx2 = factory.create("#fxfactory", p, nil, { direction = hash("right") })
go.set_parent(self.fx1, go.get_id())
go.set_parent(self.fx2, go.get_id())
go.set(self.fx1, "position.z", 0.01)
go.set(self.fx1, "scale.xy", 1)
go.set(self.fx2, "position.z", 0.02)
go.set(self.fx2, "scale.xy", 1)
elseif message_id == hash("lights_on") or message_id == hash("lights_off") then
msg.post(self.fx1, message_id)
msg.post(self.fx2, message_id)
end
end
Jetzt können wir magische Blöcke erzeugen und sie auch aufleuchten lassen. Mit diesem Effekt zeigen wir an, dass ein magischer Block neben einem anderen magischen Block liegt.

Den Code, der das Spielfeld mit Blöcken füllt, müssen wir nun so ändern, dass auch einige magische Blöcke darauf erscheinen:
-- board.script
local function build_board(self)
...
-- Distribute magic blocks.
local rand_x = 0
local rand_y
for y = 0, boardheight - 1, boardheight / self.num_magic do
local set = false
while not set do
rand_y = math.random(math.floor(y), math.min(boardheight - 1, math.floor(y + boardheight / self.num_magic)))
rand_x = math.random(0, boardwidth - 1)
if self.board[rand_x][rand_y].color ~= hash("magic") then
msg.post(self.board[rand_x][rand_y].id, "make_magic")
self.board[rand_x][rand_y].color = hash("magic")
set = true
end
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
Die wichtigste Mechanik der magischen Blöcke ist ihre Fähigkeit, seitwärts zu gleiten, wenn neben ihnen ein anderer Block verschwindet. Alle Einzelheiten dieser Mechanik setzen wir in der Funktion slide_magic_blocks() in board.script um. Der Algorithmus ist einfach:
M der magischen Blöcke.M so lange, bis die Liste nicht mehr schrumpft. Für jeden Durchlauf gilt:
hash("removing") markierte Blockposition befindet, entferne ihn einfach aus der Liste M.hash("removing") markierte Lücke befindet, lasse ihn dorthin gleiten, setze seine alte Position auf hash("removing") und entferne ihn dann aus der Liste M.-- board.script
-- Apply the shifting logic to magic blocks. Only slide to positions
-- marked for removal with hash("removing")
--
local function slide_magic_blocks(self)
-- Slide all magic blocks to the side that should slide first.
-- This works best going row by row!
local row_m
for y = 0,boardheight - 1 do
row_m = {}
-- Build list of magic blocks on this row.
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil and self.board[x][y] ~= hash("removing") and self.board[x][y].color == hash("magic") then
table.insert(row_m, self.board[x][y])
end
end
local mc = #row_m + 1
-- Go through list, slide and remove if possible. Reiterate until the list does not shrink.
while #row_m < mc do
mc = #row_m
for i, m in pairs(row_m) do
local x = m.x
if y > 0 and self.board[x][y-1] == hash("removing") then
-- Hole below, do nothing.
row_m[i] = nil
elseif x > 0 and self.board[x-1][y] == hash("removing") then
-- Hole to the left! Slide magic block there
self.board[x-1][y] = self.board[x][y]
self.board[x-1][y].x = x - 1
go.animate(self.board[x][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x - 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y].id, "position.z", (x - 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
elseif x < boardwidth - 1 and self.board[x + 1][y] == hash("removing") then
-- Hole to the right. Slide magic block there
self.board[x+1][y] = self.board[x][y]
self.board[x+1][y].x = x + 1
go.animate(self.board[x+1][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x + 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x+1][y].id, "position.z", (x + 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
end
end
end
end
end
Wir können die Mechanik ausprobieren, indem wir in on_input() einen Aufruf der Funktion ergänzen:
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Jetzt wird klar, warum wir beim Entfernen von Positionen die Zwischenmarkierung hash("removing") verwendet haben. Ohne sie würden magische Blöcke in jede freie Position neben ihnen hin- und hergleiten. Das wäre vielleicht eine interessante Mechanik, aber nicht die für dieses kleine Spiel vorgesehene.
Nun brauchen wir Logik, die erkennt, ob magische Blöcke verbunden sind (links, rechts, über- oder untereinander liegen). Außerdem müssen wir wissen, ob alle magischen Blöcke auf dem Spielfeld miteinander verbunden sind. Der verwendete Algorithmus ist recht geradlinig:
M aller magischen Blöcke auf dem Spielfeld.M:
region gesetzt ist, weise ihm die Regionsnummer R zu (anfangs 1).R und gehe dann zu deren Nachbarn, den Nachbarn dieser Nachbarn und so weiter über.R um 1.
Hier ist die Implementierung des Algorithmus:
-- board.script
--
-- Build list of all current magic blocks.
--
local function magic_blocks(self)
local magic = {}
for x = 0,boardwidth - 1 do
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil and self.board[x][y].color == hash("magic") then
table.insert(magic, self.board[x][y])
end
end
end
return magic
end
--
-- Filter out adjacent magic blocks
--
local function adjacent_magic_blocks(blocks, block)
return filter(function (e)
return (block.x == e.x and math.abs(block.y - e.y) == 1) or
(block.y == e.y and math.abs(block.x - e.x) == 1)
end, blocks)
end
--
-- Spread region to neighbors
--
local function mark_neighbors(blocks, block, region)
local neighbors = adjacent_magic_blocks(blocks, block)
for i, m in pairs(neighbors) do
if m.region == nil then
m.region = region
mark_neighbors(blocks, m, region)
end
end
end
--
-- Mark all magic block regions
--
local function mark_magic_regions(self)
local m_blocks = magic_blocks(self)
-- 1. Clear all region marks and count neighbors
for i, m in pairs(m_blocks) do
m.region = nil
local n = 0
for _ in pairs(adjacent_magic_blocks(m_blocks, m)) do n = n + 1 end
m.neighbors = n
end
-- 2. Assign regions and spread them
local region = 1
for i, m in pairs(m_blocks) do
if m.region == nil then
m.region = region
mark_neighbors(m_blocks, m, region)
region = region + 1
end
end
return m_blocks
end
Wir erstellen außerdem Funktionen, mit denen wir die Anzahl der Regionen unter den magischen Blöcken zählen können. Ist die Anzahl der Regionen 1, wissen wir, dass alle magischen Blöcke miteinander verbunden sind. Außerdem fügen wir eine Funktion hinzu, die das Licht in allen magischen Blöcken ausschaltet, und eine weitere, die die Lichteffekte in den magischen Blöcken einschaltet, die magische Nachbarblöcke haben:
-- board.script
--
-- Count the number of connected regions among the magic blocks.
--
local function count_magic_regions(blocks)
local maxr = 0
for i, m in pairs(blocks) do
if m.region > maxr then
maxr = m.region
end
end
return maxr
end
--
-- Shut off lights on all listed magic blocks
--
local function shutdown_lined_up_magic(self)
for i, m in ipairs(self.lined_up_magic) do
msg.post(m.id, "lights_off")
end
end
--
-- Set highlight for all magic blocks
--
local function highlight_magic(blocks)
for i, m in pairs(blocks) do
if m.neighbors > 0 then
msg.post(m.id, "lights_on")
else
msg.post(m.id, "lights_off")
end
end
end
Jetzt können wir diese Teile der Logik in den Gesamtablauf einfügen. Da das Spielfeld zufällig erzeugt wird, besteht zunächst eine geringe Wahrscheinlichkeit, dass es bereits im Gewinnzustand beginnt. In diesem Fall verwerfen wir das Spielfeld einfach und bauen es erneut auf:
-- board.script
--
-- Clear the board
--
local function clear_board(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil then
go.delete(self.board[x][y].id)
self.board[x][y] = nil
end
end
end
end
local function build_board(self)
...
-- Build 1d list that we can easily filter.
build_blocklist(self)
local magic_blocks = mark_magic_regions(self)
if count_magic_regions(magic_blocks) == 1 then
-- "Win" from start. Make new board.
clear_board(self)
build_board(self)
end
highlight_magic(magic_blocks)
end
Die restliche Logik kommt in on_input(). Es gibt noch keinen Code, der die Nachricht level_completed verarbeitet, aber das genügt fürs Erste:
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and refill board.
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
local magic_blocks = mark_magic_regions(self)
-- Highlight adjacent magic blocks.
if count_magic_regions(magic_blocks) == 1 then
-- Win!
msg.post("#", "level_completed")
end
highlight_magic(magic_blocks)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Jetzt lässt sich das Spiel spielen und der Gewinnzustand erreichen, auch wenn noch nichts passiert, sobald du alle magischen Blöcke miteinander verbindest.

Die Idee hinter dem „Abwurf“ (drop) ist, eine einfache Mechanik für den Spielfortschritt hinzuzufügen. Der Spieler kann durch Drücken der Schaltfläche DROP eine begrenzte Anzahl von Abwürfen auslösen. Dabei fallen einfach einige neue zufällige Blöcke auf das Spielfeld. Der Spieler beginnt mit einem Abwurf und erhält jedes Mal, wenn er ein Level abschließt, einen weiteren. Der Code für die Abwurfmechanik passt in zwei Funktionen: Eine gibt eine Liste möglicher Stellen zurück, an denen abgeworfene Blöcke landen können, und die andere führt den eigentlichen Abwurf samt Animation aus.
-- board.script
--
-- Find spots for a drop.
--
local function dropspots(self)
local spots = {}
for x = 0, boardwidth - 1 do
for y = 0, boardheight - 1 do
if self.board[x][y] == nil then
table.insert(spots, { x = x, y = y })
break
end
end
end
-- If more than dropamount, randomly remove a slot until dropamount
for c = 1, #spots - dropamount do
table.remove(spots, math.random(#spots))
end
return spots
end
--
-- Perform the drop
--
local function drop(self, spots)
for i, s in pairs(spots) do
local pos = vmath.vector3()
pos.x = edge + blocksize / 2 + blocksize * s.x
pos.y = 1000
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
go.animate(id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * s.y, go.EASING_OUTBOUNCE, 0.5)
-- Calc new z
go.set(id, "position.z", s.x * -0.1 + s.y * 0.01)
self.board[s.x][s.y] = { id = id, color = c, x = s.x, y = s.y }
end
-- Rebuild blocklist
build_blocklist(self)
end
Wir können Abwürfe testen, indem wir den folgenden Code beispielsweise in on_reload() ausführen oder ihn an eine vorübergehende Eingabeaktion binden:
s = dropspots(self)
if #s > 0 then
-- Do the drop
drop(self, s)
end

Jetzt ist es Zeit, alles zusammenzufügen. Zuerst erstellen wir einen Startbildschirm und trennen ihn vom Spielfeld. Schritt 1 besteht darin, eine main_menu.gui zu erstellen und sie mit einer Start-Schaltfläche (einem Textknoten und einem texturierten Box-Knoten), einem Textknoten für den Titel und einigen dekorativen Blöcken (texturierten Box-Knoten) einzurichten. Das Skript main_menu.gui_script, das wir an die GUI anhängen, animiert die dekorativen Blöcke in init(). Es enthält außerdem eine Funktion on_input(), die eine start_game-Nachricht an ein Hauptskript sendet. Dieses Skript erstellen wir gleich.

-- main_menu.gui_script
function init(self)
msg.post(".", "acquire_input_focus")
local bs = { "brick1", "brick2", "brick3", "brick4", "brick5", "brick6" }
for i, b in ipairs(bs) do
local n = gui.get_node(b)
local rt = (math.random() * 3) + 1
local a = math.random(-45, 45)
gui.set_color(n, vmath.vector4(1, 1, 1, 0))
gui.animate(n, "position.y", -100 - math.random(0, 50), gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "color.w", 1, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "rotation.z", a, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
end
gui.animate(gui.get_node("start"), "color.x", 1, gui.EASING_INOUTSINE, 1, 0, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local start = gui.get_node("start")
if gui.pick_node(start, action.x, action.y) then
msg.post("/main#script", "start_game")
end
end
end
Da das Starten des Spiels bald das Skript des Hauptmenüs übernimmt, entferne den vorübergehenden Aufruf zum Aufbau des Spielfelds aus init() in board.script:
-- board.script
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
end
Das Hauptskript verwaltet den gesamten Spielzustand und startet das Spiel auf Anforderung. Wir möchten hier erreichen, dass main.collection nur die Mindestmenge an Assets enthält, die wir beim Start anzeigen müssen. Dazu enthält main.collection ein Spielobjekt namens „main“ mit der GUI des Hauptmenüs, einer Skriptkomponente und vor allem einer Collection Proxy-Komponente.
Mit dem Sammlungs-Proxy (collection proxy) können wir Sammlungen im laufenden Spiel dynamisch laden und entladen. Er handelt stellvertretend für eine angegebene Sammlungsdatei. Indem wir Nachrichten an den Proxy senden, laden, initialisieren, aktivieren, deaktivieren und entladen wir die dynamische Sammlung. Eine vollständige Beschreibung ihrer Verwendung findest du in der Dokumentation zu Sammlungs-Proxys.
In unserem Fall setzen wir die Eigenschaft Collection der Sammlungs-Proxy-Komponente auf board.collection, die das „Level“ enthält.

Öffne nun game.project und ändere die Startsammlung (bootstrap collection) main_collection auf /main/main.collectionc.

Ein Spiel zu starten bedeutet jetzt, unserem Sammlungs-Proxy Nachrichten zum Laden, Initialisieren und Aktivieren des Spielfelds zu senden und anschließend das Hauptmenü zu deaktivieren (damit es nicht angezeigt wird). Bei der Rückkehr zum Hauptmenü geschieht das Umgekehrte (vorausgesetzt, der Proxy hat die Sammlung geladen).
-- main.script
function init(self)
msg.post("#", "to_main_menu")
self.state = "MAIN_MENU"
end
function on_message(self, message_id, message, sender)
if message_id == hash("to_main_menu") then
if self.state ~= "MAIN_MENU" then
msg.post("#boardproxy", "unload")
end
msg.post("main:/main#menu", "enable") -- <1>
self.state = "MAIN_MENU"
elseif message_id == hash("start_game") then
msg.post("#boardproxy", "load")
msg.post("#menu", "disable")
elseif message_id == hash("proxy_loaded") then
-- Board collection has loaded...
msg.post(sender, "init")
msg.post("board:/board#script", "start_level", { difficulty = 1 }) -- <2>
msg.post(sender, "enable")
self.state = "GAME_RUNNING"
end
end
Bevor wir die letzten Teile der Logik zum Spielfeldskript hinzufügen, sollten wir das Spielfeld um einige GUI-Elemente ergänzen. Zuerst fügen wir oberhalb des Spielfelds eine Schaltfläche RESTART und eine Schaltfläche DROP hinzu.

Das Skript für die GUI des Spielfelds sendet bei einem Klick Nachrichten an den GUI-Dialog für den Neustart und beim Klicken auf DROP zurück an das Spielfeldskript selbst:
-- board.gui_script
function init(self)
msg.post("#", "show")
msg.post("/restart#gui", "hide")
msg.post("/level_complete#gui", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
msg.post("#", "enable")
elseif message_id == hash("set_drop_counter") then
local n = gui.get_node("drop_counter")
gui.set_text(n, message.drops .. " x")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local restart = gui.get_node("restart")
local drop = gui.get_node("drop")
if gui.pick_node(restart, action.x, action.y) then
-- Show the restart dialog box.
msg.post("/restart#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(drop, action.x, action.y) then
msg.post("/board#script", "drop")
end
end
end
Der Dialog RESTART ist einfach. Wir erstellen ihn als restart.gui und hängen ein einfaches Skript an, das bei einem Klick auf NO nichts tut, bei einem Klick auf YES eine restart_level-Nachricht an das Spielfeldskript sendet und bei einem Klick auf Quit to main menu eine to_main_menu-Nachricht an das Hauptskript:

-- restart.gui_script
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local yes = gui.get_node("yes")
local no = gui.get_node("no")
local quit = gui.get_node("quit")
if gui.pick_node(no, action.x, action.y) then
msg.post("#", "hide")
msg.post("/board#gui", "show")
elseif gui.pick_node(yes, action.x, action.y) then
msg.post("board:/board#script", "restart_level")
msg.post("/board#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(quit, action.x, action.y) then
msg.post("main:/main#script", "to_main_menu")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Wir erstellen außerdem in level_complete.gui einen einfachen GUI-Dialog für den Levelabschluss mit einem einfachen Skript, das eine next_level-Nachricht an das Spielfeldskript sendet, wenn der Spieler auf CONTINUE klickt:

-- level_complete.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local continue = gui.get_node("continue")
if gui.pick_node(continue, action.x, action.y) then
msg.post("board#script", "next_level")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Ein Dialog stellt das aktuelle Level vor. Sein Skript enthält nur das Ausblenden und Anzeigen des Dialogs. Beim Anzeigen wird der Dialogtext auf eine Nachricht gesetzt, die den aktuellen Schwierigkeitsgrad enthält:

-- present_level.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
local n = gui.get_node("message")
gui.set_text(n, "Level " .. message.level)
msg.post("#", "enable")
end
end
Wir fügen außerdem einen Dialog hinzu, der erscheint, wenn der Spieler einen Abwurf versucht, dafür aber kein Platz ist.

-- no_drop_room.gui_script
function init(self)
msg.post("#", "hide")
self.t = 0
end
function update(self, dt)
if self.t < 0 then
msg.post("#", "hide")
else
self.t = self.t - dt
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
self.t = 1
msg.post("#", "enable")
end
end
Zum Schluss fügen wir diese GUI-Komponenten zu board.collection hinzu und ergänzen den benötigten Code in board.script:

In on_message() brauchen wir Code für alle Nachrichten, die an das Spielfeld und von ihm gesendet werden.
start_levelgo.animate() als Zeitgeber verwenden, indem wir den Wert von „timer“ animieren, der sonst für nichts verwendet wird.restart_levellevel_completednext_level-Nachricht zurück, wenn der Spieler darin auf die Schaltfläche CONTINUE klickt.next_levelstart_level mit dem nächsten Schwierigkeitsgrad.drop-- board.script
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
self.num_magic = message.difficulty + 1
build_board(self)
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post("present_level#gui", "show", { level = message.difficulty } )
-- Wait some...
go.animate("#", "timer", go.PLAYBACK_ONCE_FORWARD, 1, go.EASING_LINEAR, 2, 0, function ()
msg.post("present_level#gui", "hide")
msg.post(".", "acquire_input_focus")
end)
elseif message_id == hash("restart_level") then
clear_board(self)
build_board(self)
self.drops = 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post(".", "acquire_input_focus")
elseif message_id == hash("level_completed") then
-- turn off input
msg.post(".", "release_input_focus")
-- Animate the magic!
for i, m in ipairs(magic_blocks(self)) do
go.set_scale_xy(0.17, m.id)
go.animate(m.id, "scale.xy", go.PLAYBACK_LOOP_PINGPONG, 0.19, go.EASING_INSINE, 0.5, 0)
end
-- Show completion screen
msg.post("level_complete#gui", "show")
elseif message_id == hash("next_level") then
clear_board(self)
self.drops = self.drops + 1
-- Difficulty level is number of magic blocks - 1
msg.post("#", "start_level", { difficulty = self.num_magic })
elseif message_id == hash("drop") then
s = dropspots(self)
if #s == 0 then
-- Can't perform drop
msg.post("no_drop_room#gui", "show")
elseif self.drops > 0 then
-- Do the drop
drop(self, s)
self.drops = self.drops - 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
end
end
end
Geschafft! Das Spiel und dieses Tutorial sind jetzt abgeschlossen! Viel Spaß beim Spielen!

Dieses kleine Spiel hat einige interessante Eigenschaften. Experimentiere ruhig damit. Hier sind einige Übungen, mit denen du dich weiter mit Defold vertraut machen kannst: