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
У цьому уроці ви пройдете процес створення однієї з найпоширеніших класичних ігор, які можна спробувати відтворити. Ця гра має чимало різновидів; у нашому змійка їсть «їжу» й росте лише тоді, коли їсть. Ця змійка також повзає ігровим полем із перешкодами.
![]()
У цьому уроці ви навчитеся:
Цей урок розрахований на початківців, але якщо ви зовсім не знайомі з Defold і розробкою ігор, радимо спершу прочитати кілька вступних посібників, особливо про будівельні блоки Defold і глосарій. Якщо ви ще не завантажили Defold, перегляньте посібник зі встановлення. Також радимо переглянути огляд редактора, щоб швидше освоїтися в ньому, хоча тут ми також наводимо знімки екрана для кожного кроку.
Запустіть Defold і:

Готово!
Почнемо з визначення роздільності гри.
game.project ліворуч, на панелі Assets. Двічі клацніть його, щоб відкрити.game.project.Width і Height) на 768⨉768 або інше значення, кратне 16.
Це потрібно тому, що гра відображатиметься на сітці, кожен сегмент якої матиме розмір 16x16 пікселів, і так екран гри не обрізатиме частини сегментів. Файл game.project містить усі важливі налаштування проєкту — про них можна прочитати в посібнику з налаштувань проєкту.
Готово!
Для мінімалістичного клону «Змійки» потрібно зовсім небагато графіки. Один зелений сегмент розміром 16⨉16 для змійки, один білий блок для перешкод і один менший червоний блок, що позначатиме їжу.
Спершу створіть каталог для ресурсів у редакторі Defold:
mainNew Folder.assets і натисніть Create Folder.
Готово!
Зображення нижче — єдиний ресурс, який вам знадобиться:


Докладніше про імпортування ресурсів можна прочитати тут.
Готово!
Defold має вбудований компонент (component) карти плиток, який ви використаєте для створення ігрового поля з плиток, розташованих на сітці. Карта плиток дає змогу задавати та зчитувати окремі плитки, що чудово підходить для цієї гри. Оскільки карти плиток беруть графіку з джерела плиток, потрібно його створити:
assets.New ▸ Tile Source у розділі «Resources».snake.tilesource).
Джерело плиток відкриється у спеціальному редакторі Tile Source Editor для цього типу файлів, і вам буде запропоновано вказати для нього зображення, щоб воно запрацювало. Праворуч розташована панель Properties:
Задайте для властивості Image щойно імпортований графічний файл.

Залиште значення властивостей Width і Height рівними 16 (типове значення). Так зображення розміром 32⨉32 пікселі буде поділено на 4 плитки з номерами 1–4.

Зверніть увагу, що властивість Extrude Borders має значення 2 пікселі. Це запобігає появі візуальних артефактів навколо плиток, у яких графіка сягає самого краю.
Якщо ви змінюєте файл, поруч із його назвою на вкладці з’являється зірочка *. Виберіть File ▸ Save All або натисніть Ctrl+S (⌘Cmd + S на Mac), щоб зберегти всі файли.
Готово!
Тепер у вас є готове джерело плиток, тож час створити компонент карти плиток ігрового поля:
Клацніть правою кнопкою миші папку main і виберіть New ▸ Tile Map у розділі «Components». Назвіть новий файл «grid» (редактор збереже його як «grid.tilemap»).

Він відкриється в редакторі Tile Map Editor, який підсвітить обов’язкове поле Tile Source, тож задайте для властивості Tile Source раніше створений файл «snake.tilesource».

Готово!
Defold зберігає лише ту ділянку карти плиток, яка насправді використовується, тож потрібно додати достатньо плиток, щоб заповнити межі екрана.
layer1 на панелі Outline праворуч.Виберіть пункт меню Edit ▸ Select Tile... або натисніть Space, щоб відкрити палітру плиток, а потім клацніть плитку, якою хочете малювати.


Щоб заповнити екран гри, вам знадобиться карта розміром 48x48 плиток (оскільки розмір нашого екрана — 768 пікселів, а плитки мають розмір 16 пікселів, отримуємо 768/16 = 48).
Коли закінчите, збережіть карту плиток.
Готово!
Тепер потрібно додати нашу карту плиток до гри. Якщо ви знайомі з будівельними блоками Defold, то знаєте, що компоненти є частинами ігрових об’єктів (game objects), а ігрові об’єкти можна визначати в колекціях (collections).
Відкрийте main.collection, двічі клацнувши його на панелі Assets. У шаблоні Empty Project це типова стартова колекція (bootstrap collection), яка завантажується під час запуску рушія.
Клацніть правою кнопкою миші кореневий елемент на панелі Outline і виберіть Add Game Object, щоб створити новий ігровий об’єкт у колекції, яка завантажується на початку гри.

Клацніть правою кнопкою миші новий ігровий об’єкт і виберіть Add Component File. Виберіть щойно створений файл «grid.tilemap».

Тепер у колекції нашої гри є карта плиток. Вона має бути видимою, коли ви запустите гру з редактора.
Project ▸ Build або натисніть Ctrl + B (⌘Cmd + B на Mac).
Готово!
Клацніть правою кнопкою миші папку main у вікні Assets і виберіть New ▸ Script у розділі Scripts. Назвіть новий файл скрипту «snake» (його буде збережено як «snake.script»). Цей файл міститиме всю логіку гри.

Поверніться до main.collection і клацніть правою кнопкою миші ігровий об’єкт із картою плиток. Виберіть Add Component File і вкажіть файл «snake.script».

Тепер компонент карти плиток і скрипт на своїх місцях.
Готово!
Скрипт, який ви напишете, керуватиме всією грою. Ми додаватимемо можливості одну за одною.
Ідея його роботи така:
Відкрийте snake.script і знайдіть функцію init(). Рушій викликає її під час ініціалізації скрипту на початку гри. Замініть код на такий:
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
У цьому коді ми:
self.segments, яка містить список таблиць, кожна з яких зберігає позицію сегмента за X і Y.self.dir, що містить напрямок за X і Y.self.speed, виражену в плитках за секунду.self.time, яке використовуватимемо для відстеження швидкості руху.Код скрипту вище написано мовою Lua. У ньому є кілька моментів, на які варто звернути увагу, але якщо ви ще не розумієте чогось із наведеного нижче, не хвилюйтеся. Просто рухайтеся далі, експериментуйте й дайте собі час — зрештою ви розберетеся. Поки що достатньо запам’ятати, що в init() ми щойно ініціалізували змінні, які використовуватимемо.
self. Посилання self використовується для зберігання даних екземпляра.self можна використовувати як таблицю Lua для зберігання даних. Просто використовуйте крапковий запис, як і для будь-якої іншої таблиці: self.data = "value". Посилання дійсне протягом усього життєвого циклу скрипту, у цьому випадку — від початку гри до виходу з неї.{}.{x = 10, y = 20}), вкладеними таблицями Lua ({ {a = 1}, {b = 2} }) або даними інших типів.Готово!
Функція init() викликається рівно один раз, коли у запущеній грі створюється екземпляр компонента-скрипту. Натомість функція update() викликається один раз кожного кадру. Це робить її ідеальною для ігрової логіки реального часу.
Ідея оновлення така: через певний заданий проміжок часу виконувати такі дії:

:::sidenote Пам’ятайте, що голова нашої змійки розташована в кінці таблиці, а хвіст — на початку. :::
update() у snake.script і замініть код на такий: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
У цьому коді ми:
update() — так званий «дельта-час», або dt.# — це оператор для отримання довжини таблиці за умови, що вона використовується як масив, як у нашому випадку: усі сегменти є значеннями таблиці без явно заданого ключа.self.dir).#grid має лише 1 шар із назвою layer1.i міститиме позицію в таблиці (починаючи з 1), а s — поточний сегмент.Якщо зараз запустити гру, ви маєте побачити, як змійка завдовжки 4 сегменти повзе ігровим полем зліва направо.

Готово!
Перш ніж додавати код для реагування на введення гравця, потрібно налаштувати прив’язки введення.
game.input_binding у папці input і двічі клацніть його, щоб відкрити.
Файл прив’язок введення зіставляє фактичне введення користувача (клавіші, рухи миші тощо) з назвами дій, які передаються скриптам, що запросили введення.
Готово!
Налаштувавши прив’язки, відкрийте snake.script і додайте такий рядок на початку функції 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
Доданий рядок:
Потім знайдіть функцію on_input і введіть такий код:
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
Ці гілки if...elseif... виконують такі дії:
action поле pressed має значення true (гравець натиснув клавішу), тоді:Запустіть гру ще раз і переконайтеся, що можете керувати змійкою.
Готово!
Тепер зверніть увагу: якщо натиснути дві клавіші одночасно, це спричинить два виклики on_input(), по одному для кожного натискання. У наведеному вище коді лише останній виклик вплине на напрямок змійки, оскільки наступні виклики on_input() перезаписуватимуть значення в self.dir.
Також зауважте: якщо змійка рухається ліворуч, а ви натиснете клавішу right, вона поверне в себе. На перший погляд, очевидний спосіб виправити цю проблему — додати ще одну умову до гілок if у 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
...
Проте якщо змійка рухається ліворуч, а гравець швидко натисне спершу up, а потім right до наступного кроку руху, лише натискання right матиме ефект, і змійка вріжеться в себе. З доданими до гілок if умовами, показаними вище, введення буде проігноровано. Недобре!
Правильне розв’язання цієї проблеми — зберігати введення в черзі та брати з неї записи під час руху змійки:
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
Цього разу ми:
self.dirqueue, яка ініціалізується як порожня таблиця.У функції update() додайте:
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 не дорівнює null), перевіряємо, чи newdir вказує в напрямку, протилежному до self.dir.І змініть on_input, щоб натомість зберігати поточне введення в черзі:
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.Запустіть гру й переконайтеся, що вона працює як очікується.
Готово!
Змійці потрібна їжа на карті, щоб ставати довшою та швидшою. Додаймо її!
Над функцією init() додайте нову функцію:
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
У цій функції ми:
put_food(), яка розміщує їжу на карті.self.food.Потім викличте її в кінці функції 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(), задаємо початкове значення генератора випадкових чисел, інакше генеруватиметься та сама послідовність випадкових значень. Це початкове значення потрібно задавати лише один раз.put_food() на початку гри, щоб гравець починав із їжею на карті.Готово!
Тепер, щоб визначити, чи зіткнулася змійка з чимось, достатньо подивитися, що розташовано на карті плиток там, куди вона прямує, і відреагувати.
Додайте змінну, що відстежує, чи жива змійка:
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
Потім додайте логіку перевірки зіткнення зі стіною/перешкодою та їжею:
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
Тепер спробуйте пограти й переконайтеся, що все працює добре!
На цьому урок завершується, але продовжуйте експериментувати з грою та виконайте кілька вправ нижче!
Готово!
Ось повний код скрипту для довідки:
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
Корисною вправою буде спробувати реалізувати такі поліпшення: