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
Ten samouczek przeprowadzi cię przez proces tworzenia jednej z najczęściej odtwarzanych klasycznych gier. Istnieje wiele wariantów tej gry; w tej wersji wąż zjada “jedzenie” i rośnie tylko wtedy, gdy je zje. Wąż porusza się też po planszy zawierającej przeszkody.
![]()
W tym samouczku nauczysz się:
Ten samouczek jest przeznaczony dla początkujących, ale jeśli jesteś zupełnie nowy w Defold i tworzeniu gier, zalecamy najpierw przeczytać kilka wprowadzających instrukcji, szczególnie o blokach budulcowych Defold oraz słowniczku. Jeśli nie masz jeszcze pobranego Defold, sprawdź instrukcję instalacji. Warto też zajrzeć do przeglądu edytora, aby szybko poznać sam edytor, ale w tym samouczku pokazujemy również zrzuty ekranu dla każdego kroku.
Uruchom Defold i:

Gotowe!
Zaczniemy od zdefiniowania rozdzielczości gry.
game.project po lewej stronie, w panelu Assets. Kliknij go dwukrotnie, aby go otworzyć.game.project.Width i Height) na 768⨉768 lub inną wielokrotność 16.
Powodem jest to, że gra będzie rysowana na siatce, w której każdy segment ma 16x16 pikseli, więc ekran gry nie utnie żadnych częściowych segmentów. Plik game.project zawiera wszystkie ważne ustawienia projektu - więcej przeczytasz o nich w instrukcji ustawień projektu.
Gotowe!
Do minimalistycznego klona Snake potrzeba bardzo niewiele grafiki. Jeden zielony segment 16⨉16 dla węża, jeden biały blok dla przeszkód i jeden mniejszy czerwony blok reprezentujący jedzenie.
Najpierw utwórz katalog na zasoby w edytorze Defold:
mainNew Folder.assets i kliknij Create Folder.
Gotowe!
Poniższy obraz jest jedynym zasobem, którego potrzebujesz:


Więcej szczegółów możesz też przeczytać w instrukcji o importowaniu zasobów graficznych.
Gotowe!
Defold udostępnia wbudowany komponent Tilemap, którego użyjesz do utworzenia planszy złożonej z kafelków wyrównanych do siatki. Mapa kafelków pozwala ustawiać i odczytywać pojedyncze kafelki, co idealnie pasuje do tej gry. Ponieważ mapy kafelków pobierają grafikę z Tilesource, musisz utworzyć taki zasób:
assets.New ▸ Tile Source w sekcji “Resources”.snake.tilesource).
Tilesource otworzy się w dedykowanym edytorze Tilesource dla tego typu pliku i pojawi się prośba o wskazanie obrazu, który jest wymagany. Po prawej stronie znajdziesz panel Properties:
Ustaw właściwość Image na właśnie zaimportowany plik graficzny.

Właściwości Width i Height powinny pozostać ustawione na 16 (wartość domyślna). Podzieli to obraz 32⨉32 piksele na 4 kafelki, ponumerowane 1-4.

Zwróć uwagę, że właściwość Extrude Borders jest ustawiona na 2 piksele. Zapobiega to artefaktom wizualnym wokół kafelków, których grafika dochodzi aż do krawędzi.
Jeśli dokonasz zmian w pliku, przy jego nazwie na karcie pojawi się znak gwiazdki *. Wybierz File ▸ Save All lub użyj skrótu Ctrl+S (⌘Cmd + S na Macu), aby zapisać wszystkie pliki.
Gotowe!
Masz już gotowe źródło kafelków, więc pora utworzyć komponent mapy kafelków dla planszy:
Kliknij prawym przyciskiem folder main i wybierz New ▸ Tile Map w sekcji “Components”. Nazwij nowy plik “grid” (edytor zapisze go jako “grid.tilemap”).

Plik otworzy się w edytorze Tilemap i zaznaczy, że potrzebuje Tile Source, więc ustaw właściwość Tile Source na wcześniej utworzony plik “snake.tilesource”.

Gotowe!
Defold przechowuje tylko ten obszar mapy kafelków, który jest rzeczywiście używany, dlatego musisz dodać wystarczająco dużo kafelków, aby wypełnić granice ekranu.
layer1 w panelu Outline po prawej stronie.Wybierz opcję menu Edit ▸ Select Tile... albo skrót Space, aby wyświetlić paletę kafelków, a następnie kliknij kafelek, którego chcesz używać podczas malowania.


Będziesz potrzebować mapy kafelków o rozmiarze 48x48 kafelków (ponieważ nasz ekran ma 768 pikseli, a kafelki mają 16px, więc 768/16 = 48), aby wypełnić ekran gry.
Zapisz mapę kafelków po zakończeniu.
Gotowe!
Teraz musimy dodać mapę kafelków do gry. Jeśli znasz bloki budulcowe Defold, komponenty są częścią obiektów gry, a obiekty gry mogą być definiowane w kolekcjach.
Otwórz main.collection, klikając go dwukrotnie w panelu Assets. W szablonie Empty Project jest to domyślna kolekcja startowa ładowana przy uruchomieniu silnika.
Kliknij prawym przyciskiem korzeń w panelu Outline i wybierz Add Game Object, co utworzy nowy obiekt gry w kolekcji ładowanej przy starcie gry.

Kliknij prawym przyciskiem nowy obiekt gry i wybierz Add Component File. Wybierz utworzony przed chwilą plik “grid.tilemap”.

W tej chwili mamy mapę kafelków w kolekcji gry. Powinna być widoczna po uruchomieniu gry z edytora.
Project ▸ Build albo skrót Ctrl + B (⌘Cmd + B na Macu).
Gotowe!
Kliknij prawym przyciskiem folder main w przeglądarce Assets i wybierz New ▸ Script w sekcji Scripts. Nazwij nowy plik skryptu “snake” (zostanie zapisany jako “snake.script”). Ten plik będzie zawierał całą logikę gry.

Wróć do main.collection i kliknij prawym przyciskiem obiekt gry zawierający mapę kafelków. Wybierz Add Component File i wskaż plik “snake.script”.

Teraz komponent mapy kafelków i skrypt są już na miejscu.
Gotowe!
Skrypt, który napiszesz, będzie sterował całą grą. Będziemy dodawać funkcje jedna po drugiej.
Pomysł na działanie jest następujący:
Otwórz snake.script i znajdź funkcję init(). Ta funkcja jest wywoływana przez silnik podczas inicjalizacji skryptu przy starcie gry. Zmień kod na następujący:
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
W tym kodzie:
self.segments, zawierającą listę tabel, z których każda trzyma pozycję X i Y segmentu.self.dir, zawierającą kierunek X i Y.self.speed, wyrażoną w kafelkach na sekundę.self.time, która będzie używana do śledzenia prędkości ruchu.Powyższy kod skryptu jest napisany w języku Lua. Warto zwrócić uwagę na kilka rzeczy w tym kodzie, ale jeśli jeszcze nie rozumiesz któregoś z poniższych punktów, nie martw się. Idź dalej, eksperymentuj i daj sobie czas — w końcu to zrozumiesz. Na razie możesz zapamiętać, że w init() po prostu zainicjalizowaliśmy zmienne, których będziemy używać.
self. Referencja self służy do przechowywania danych instancji.self można używać jak tabeli Lua, w której przechowujesz dane. Używaj notacji kropkowej tak jak w każdej innej tabeli: self.data = "value". Referencja jest ważna przez cały czas życia skryptu, w tym przypadku od startu gry aż do jej zamknięcia.{}.{x = 10, y = 20}), zagnieżdżonymi tabelami Lua ({ {a = 1}, {b = 2} }) albo innymi typami danych.Gotowe!
Funkcja init() jest wywoływana dokładnie raz, gdy komponent skryptu zostaje utworzony w działającej grze. Funkcja update() jest natomiast wywoływana raz w każdej klatce, domyślnie 60 razy na sekundę. Dzięki temu idealnie nadaje się do logiki gry działającej w czasie rzeczywistym.
Pomysł na aktualizację jest taki: w ustalonym odstępie wykonuj następujące kroki:

:::sidenote Pamiętaj, że głowa węża znajduje się na końcu tabeli, a ogon na początku. :::
update() w snake.script i zmień kod na następujący: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
W tym kodzie:
update() — tak zwany “delta time”, czyli dt.# to operator używany do pobrania długości tabeli, gdy jest ona używana jako tablica, co ma miejsce w naszym przypadku — wszystkie segmenty są wartościami tabeli bez określonego klucza.self.dir).#grid ma tylko 1 warstwę o nazwie layer1.i będzie ustawione na pozycję w tabeli (zaczynając od 1), a s na aktualny segment.Jeśli teraz uruchomisz grę, powinieneś zobaczyć węża o długości 4 segmentów pełznącego od lewej do prawej po planszy.

Gotowe!
Zanim dodasz kod reagujący na wejście gracza, musisz skonfigurować połączenia wejścia.
input plik game.input_binding i kliknij go dwukrotnie, aby go otworzyć.
Plik powiązań wejścia mapuje rzeczywiste wejście użytkownika (klawisze, ruchy myszy itd.) na nazwy akcji, które trafiają do skryptów proszących o wejście.
Gotowe!
Mając powiązania, otwórz snake.script i dodaj następujący wiersz na początku funkcji init():
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
Dodany wiersz:
Następnie znajdź funkcję on_input i wpisz następujący kod:
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
Te gałęzie if...elseif... robią następujące rzeczy:
action ma pole pressed ustawione na true (gracz nacisnął klawisz), wtedy:Uruchom grę ponownie i sprawdź, czy możesz sterować wężem.
Gotowe!
Zauważ teraz, że jeśli naciśniesz dwa klawisze jednocześnie, spowoduje to dwa wywołania on_input(), po jednym dla każdego naciśnięcia. Przy kodzie zapisanym powyżej tylko wywołanie, które nastąpi jako ostatnie, wpłynie na kierunek węża, ponieważ kolejne wywołania on_input() nadpisują wartości w self.dir.
Zwróć też uwagę, że jeśli wąż porusza się w lewo, a ty naciśniesz klawisz right, wąż skręci w samego siebie. Pozornie oczywistą poprawką tego problemu jest dodanie dodatkowego warunku do instrukcji if w on_input():
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
...
Jeśli jednak wąż porusza się w lewo, a gracz szybko naciśnie najpierw up, a potem right przed następnym krokiem ruchu, efekt będzie miało tylko naciśnięcie right i wąż wejdzie w samego siebie. Po dodaniu warunków do instrukcji if pokazanych powyżej takie wejście zostanie zignorowane. Niedobrze!
Właściwym rozwiązaniem tego problemu jest zapisanie wejścia w kolejce i pobieranie wpisów z tej kolejki podczas ruchu węża:
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
Tym razem:
self.dirqueue, która jest inicjalizowana jako pusta tabela.W funkcji update() dodaj:
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 nie jest wartością null), sprawdź, czy newdir wskazuje kierunek przeciwny do self.dir.I zmodyfikuj on_input, aby zamiast tego zapisywać bieżące wejście w kolejce:
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.Uruchom grę i sprawdź, czy działa zgodnie z oczekiwaniami.
Gotowe!
Wąż potrzebuje jedzenia na mapie, aby mógł rosnąć i przyspieszać. Dodajmy je!
Nad funkcją init() dodaj nową funkcję:
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
W tej funkcji:
put_food(), która umieszcza porcję jedzenia na mapie.self.food.Następnie wywołaj ją na końcu funkcji init():
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(), ustaw ziarno losowości, w przeciwnym razie wygenerowana zostanie ta sama seria losowych wartości. To ziarno powinno być ustawione tylko raz.put_food() przy starcie gry, aby gracz zaczynał z elementem jedzenia na mapie.Gotowe!
Teraz wykrywanie, czy wąż z czymś się zderzył, sprowadza się do sprawdzenia, co znajduje się na mapie kafelków w miejscu, do którego zmierza wąż, i zareagowania.
Dodaj zmienną śledzącą, czy wąż żyje:
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
Następnie dodaj logikę sprawdzającą kolizję ze ścianą/przeszkodą oraz jedzeniem:
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
Teraz wypróbuj grę i upewnij się, że działa dobrze!
To kończy samouczek, ale zachęcamy do dalszego eksperymentowania z grą i wykonania kilku poniższych ćwiczeń!
Gotowe!
Oto kompletny kod skryptu dla odniesienia:
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
Dobrym ćwiczeniem jest spróbowanie wdrożenia tych ulepszeń: