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
Ten samouczek przeprowadzi cię przez proces tworzenia jednej z najpopularniejszych klasycznych gier, którą możesz spróbować odtworzyć. Istnieje wiele wariantów tej gry, a ta wersja zawiera węża, który zjada „jedzenie” i rośnie tylko wtedy, gdy coś zje. Wąż pełza też po planszy gry, na której znajdują się przeszkody.
Otwórz plik ustawień game.project i ustaw wymiary gry na 768⨉768 albo na inną wielokrotność 16. Warto to zrobić, ponieważ gra będzie rysowana na siatce, na której każdy segment ma rozmiar 16x16 pikseli, a dzięki temu ekran gry nie odetnie żadnych częściowych segmentów.
Do warstwy wizualnej potrzeba naprawdę niewiele. Jeden segment 16x16 dla węża, jeden dla przeszkód i jeden dla jedzenia. Ten obrazek to jedyny zasób, którego potrzebujesz. Kliknij obrazek prawym przyciskiem myszy, zapisz go na lokalnym dysku i przeciągnij do wybranego miejsca w folderze projektu.

Defold ma wbudowany komponent Tilemap (mapa kafelków), którego użyjesz do utworzenia planszy gry. Mapa kafelków pozwala ustawiać i odczytywać pojedyncze kafelki, więc ten projekt pasuje do niej idealnie. Ponieważ mapa kafelków pobiera grafiki z Tilesource (źródła kafelków), musisz takie źródło utworzyć:
Kliknij prawym przyciskiem myszy folder main i wybierz New ▸ Tile Source. Nazwij nowy plik “snake” (edytor zapisze plik jako “snake.tilesource”).
Ustaw właściwość Image na plik grafiki, który właśnie zaimportowałeś.
Właściwości Width i Height powinny pozostać ustawione na 16. To podzieli obraz 32⨉32 piksele na 4 kafelki ponumerowane od 1 do 4.

Zwróć uwagę, że właściwość Extrude Borders ma ustawioną wartość 1 piksela. Ma to zapobiec artefaktom wizualnym wokół kafelków, których grafika dochodzi aż do krawędzi.
Teraz masz już gotowe źródło kafelków, więc czas utworzyć komponent mapy kafelków planszy:
Kliknij prawym przyciskiem myszy folder main i wybierz New ▸ Tile Map. Nazwij nowy plik “grid” (edytor zapisze plik jako “grid.tilemap”).

Ustaw właściwość Tile Source nowej mapy kafelków na “snake.tilesource”.
Defold przechowuje tylko ten obszar mapy kafelków, który jest rzeczywiście używany, więc musisz dodać wystarczająco dużo kafelków, aby wypełnić granice ekranu.
Zaznacz warstwę “layer1”.
Wybierz opcję menu Edit ▸ Select Tile..., aby wyświetlić paletę kafelków, a następnie kliknij kafelek, którego chcesz użyć do malowania.
Namaluj obramowanie wokół krawędzi ekranu i kilka przeszkód.

Zapisz mapę kafelków, gdy skończysz.
Teraz otwórz main.collection. To główna kolekcja bootstrapowa ładowana przy starcie silnika. Kliknij prawym przyciskiem myszy korzeń w Outline i wybierz Add Game Object, aby utworzyć nowy obiekt gry w kolekcji ładowanej wraz ze startem gry.

Następnie kliknij prawym przyciskiem myszy nowy obiekt gry i wybierz Add Component File. Wskaż plik “grid.tilemap”, który właśnie utworzyłeś.

Kliknij prawym przyciskiem myszy folder main w przeglądarce Assets i wybierz New ▸ Script. Nazwij nowy plik skryptu “snake” (zostanie zapisany jako “snake.script”). Ten plik będzie przechowywał całą logikę gry.
Wróć do main.collection i kliknij prawym przyciskiem myszy obiekt gry zawierający mapę kafelków. Wybierz Add Component File i wskaż plik “snake.script”.
Teraz masz już na miejscu komponent mapy kafelków i skrypt. Jeśli uruchomisz grę, powinieneś zobaczyć planszę taką, jaką narysowałeś na mapie kafelków.

Skrypt, który napiszesz, będzie sterował całą grą. Pomysł na to, jak ma to działać, jest następujący:
Otwórz snake.script i znajdź funkcję init(). Ta funkcja jest wywoływana przez silnik, gdy skrypt zostaje zainicjalizowany przy starcie gry. Zmień kod na następujący:
function init(self)
self.segments = {
{x = 7, y = 24},
{x = 8, y = 24},
{x = 9, y = 24},
{x = 10, y = 24} } -- <1>
self.dir = {x = 1, y = 0} -- <2>
self.speed = 7.0 -- <3>
self.t = 0 -- <4>
end
Powyższy kod skryptu jest zapisany w języku Lua. Warto zwrócić uwagę na kilka rzeczy:
self. To odwołanie self służy do przechowywania danych instancji.{x = 10, y = 20}), zagnieżdżonymi tabelami Lua ({ {a = 1}, {b = 2} a}) albo innymi typami danych.self można traktować jak tabelę Lua, w której możesz przechowywać dane. Wystarczy używać notacji kropkowej tak samo jak przy każdej innej tabeli: self.data = "value". Ta referencja jest ważna przez cały czas życia skryptu, w tym przypadku od startu gry aż do jej zamknięcia.Jeśli nie zrozumiałeś wszystkiego powyżej, nie martw się. Po prostu idź dalej, eksperymentuj i daj sobie czas - w końcu to załapiesz.
Funkcja init() jest wywoływana dokładnie raz, gdy komponent skryptowy zostaje utworzony w działającej grze. Natomiast funkcja update() jest wywoływana raz na klatkę, 60 razy na sekundę. To sprawia, że idealnie nadaje się do logiki gry działającej w czasie rzeczywistym.
Pomysł na aktualizację jest taki:
Znajdź funkcję update() w snake.script i zmień kod na następujący:
function update(self, dt)
self.t = self.t + dt -- <1>
if self.t >= 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.t = 0 -- <10>
end
end
update().# to operator służący do pobierania długości tabeli użytej jako tablica, a tak właśnie jest tutaj - wszystkie segmenty są wartościami tabeli bez określonych kluczy.self.dir).i będzie ustawione na pozycję w tabeli (zaczynając od 1), a s na bieżący segment.Jeśli teraz uruchomisz grę, powinieneś zobaczyć węża o długości 4 segmentów pełzającego z lewej do prawej po planszy.

Zanim dodasz kod reagujący na wejście gracza, musisz skonfigurować wiązania wejść. Znajdź plik input/game.input_binding w przeglądarce Assets i kliknij go dwukrotnie, aby go otworzyć. Dodaj zestaw wiązań Key Trigger dla ruchu w górę, w dół, w lewo i w prawo.

Plik wiązań wejść mapuje rzeczywiste wejście użytkownika (klawisze, ruchy myszy itd.) na nazwy akcji, które trafiają do skryptów proszących o przechwycenie wejścia. Gdy wiązania są już gotowe, otwórz snake.script i dodaj następujący kod:
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.t = 0
end
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
action ma pole pressed ustawione na true (gracz nacisnął klawisz), wtedy:Uruchom grę ponownie i sprawdź, czy możesz sterować wężem.
Zwróć teraz uwagę, że jeśli naciśniesz dwa klawisze jednocześnie, spowoduje to dwa wywołania on_input(), po jednym dla każdego naciśnięcia. W 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() nadpiszą wartości w self.dir.
Zwróć też uwagę, że jeśli wąż porusza się w lewo i naciśniesz klawisz right, wąż skręci w samego siebie. Pozornie oczywistym rozwiązaniem tego problemu jest dodanie dodatkowego warunku do klauzul 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 kolejnym ruchem, wpływ będzie miał tylko nacisk right i wąż skręci w samego siebie. Po dodaniu powyższych warunków do klauzul if wejście zostanie zignorowane. Niedobrze!
Prawidłowym rozwiązaniem tego problemu jest zapisanie wejścia w kolejce i pobieranie z niej wpisów, gdy wąż się porusza:
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 = {} -- <1>
self.speed = 7.0
self.t = 0
end
function update(self, dt)
self.t = self.t + dt
if self.t >= 1.0 / self.speed then
local newdir = table.remove(self.dirqueue, 1) -- <2>
if newdir then
local opposite = newdir.x == -self.dir.x or newdir.y == -self.dir.y -- <3>
if not opposite then
self.dir = newdir -- <4>
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.t = 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}) -- <5>
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
newdir nie jest nil), sprawdź, czy newdir wskazuje w kierunku przeciwnym do self.dir.self.dir bezpośrednio.Uruchom grę i sprawdź, czy działa zgodnie z oczekiwaniami.
Wąż potrzebuje jedzenia na mapie, żeby mógł rosnąć i poruszać się szybciej. Dodajmy to!
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
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.t = 0
math.randomseed(socket.gettime()) -- <4>
put_food(self) -- <5>
end
put_food(), która umieszcza na mapie porcję jedzenia.self.food.math.random(), ustaw ziarno generatora losowego, bo w przeciwnym razie będzie generowana ta sama sekwencja losowych wartości. To ziarno powinno być ustawione tylko raz.put_food() na starcie gry, aby gracz zaczynał z jedzeniem na mapie.Teraz wykrywanie, czy wąż zderzył się z czymkolwiek, sprowadza się do sprawdzenia, co znajduje się na mapie kafelków w miejscu, w które wąż zmierza, i odpowiedniej reakcji. 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.alive = true -- <1>
self.t = 0
math.randomseed(socket.gettime())
put_food(self)
end
Następnie dodaj logikę sprawdzającą kolizję ze ścianą/przeszkodą i z jedzeniem:
function update(self, dt)
self.t = self.t + dt
if self.t >= 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.t = 0
end
end
Teraz spróbuj uruchomić grę i upewnij się, że działa dobrze!
Na tym kończy się samouczek, ale zachęcamy do dalszych eksperymentów z grą i wykonania kilku ćwiczeń poniżej!
Oto pełny kod skryptu do wykorzystania jako punkt 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.alive = true
self.t = 0
math.randomseed(socket.gettime())
put_food(self)
end
function update(self, dt)
self.t = self.t + dt
if self.t >= 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.t = 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
Wersja gry, którą możesz zagrać na początku tego samouczka, zawiera kilka dodatkowych usprawnień. Dobrym ćwiczeniem będzie spróbowanie zaimplementowania tych ulepszeń:
put_food() nie uwzględnia pozycji węża ani tego, gdzie znajdują się przeszkody. Napraw to.