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 содержит все важные настройки проекта — подробнее о них можно прочитать в руководстве по настройкам проекта.
Готово!
Для минималистичного клона Snake нужно совсем немного графики: один зеленый сегмент 16⨉16 для змейки, один белый блок для препятствий и один красный блок меньшего размера для еды.
Сначала создайте директорию для ассетов в редакторе Defold:
main.New Folder.assets и нажмите Create Folder.
Готово!
Ниже находится единственный ассет, который вам понадобится:


Подробнее об этом можно прочитать в руководстве по импорту графики.
Готово!
В Defold есть встроенный компонент Tilemap, который вы будете использовать для создания игрового поля из тайлов, выровненных по сетке. Tilemap позволяет задавать и читать отдельные тайлы, что идеально подходит для этой игры. Поскольку tilemap берет графику из Tilesource, сначала нужно создать его:
assets.New ▸ Tile Source в разделе “Resources”.snake.tilesource).
Tilesource откроется в отдельном редакторе Tilesource, и нужно будет указать для него изображение. Справа находится панель Properties:
Установите свойство Image на графический файл, который вы только что импортировали.

Свойства Width и Height должны остаться равными 16 (значение по умолчанию). Это разделит изображение 32⨉32 пикселя на 4 тайла с номерами 1–4.

Обратите внимание, что свойство Extrude Borders установлено в 2 пикселя. Это нужно, чтобы избежать визуальных артефактов вокруг тайлов, у которых графика доходит до самого края.
Если вы измените файл, рядом с его именем на вкладке появится звездочка *. Выберите File ▸ Save All или используйте сочетание Ctrl+S (⌘Cmd + S на Mac), чтобы сохранить все файлы.
Готово!
Теперь у вас есть tilesource, готовый к использованию, и можно создать компонент tilemap для игрового поля:
Щелкните правой кнопкой по папке main и выберите New ▸ Tile Map в разделе “Components”. Назовите новый файл “grid” (редактор сохранит его как “grid.tilemap”).

Файл откроется в редакторе Tilemap, который покажет, что ему нужен Tile Source. Установите свойство Tile Source на ранее созданный “snake.tilesource”.

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


Вам понадобится tilemap размером 48x48 тайлов, потому что размер экрана равен 768, размер тайла — 16 пикселей, а 768/16 = 48.
Когда закончите, сохраните tilemap.
Готово!
Теперь нужно добавить tilemap в игру. Если вы знакомы со структурными элементами Defold, то знаете, что компоненты входят в игровые объекты, а игровые объекты могут быть описаны в коллекциях.
Откройте main.collection, дважды щелкнув по нему в панели Assets. В шаблоне Empty Project это bootstrap-коллекция, которая загружается при старте движка.
Щелкните правой кнопкой по корню в Outline и выберите Add Game Object. Так вы создадите новый игровой объект в коллекции, которая загружается при запуске игры.

Щелкните правой кнопкой по новому игровому объекту и выберите Add Component File. Выберите файл “grid.tilemap”, который вы только что создали.

Теперь в игровой коллекции есть tilemap. Он должен быть виден, если запустить игру из редактора.
Project ▸ Build или используйте сочетание Ctrl + B (⌘Cmd + B на Mac).
Готово!
Щелкните правой кнопкой по папке main в браузере Assets и выберите New ▸ Script в разделе Scripts. Назовите новый файл скрипта “snake” (он будет сохранен как “snake.script”). В этом файле будет вся игровая логика.

Вернитесь к main.collection и щелкните правой кнопкой по игровому объекту с tilemap. Выберите Add Component File и укажите файл “snake.script”.

Теперь tilemap и скрипт на месте.
Готово!
Скрипт, который вы будете писать, будет управлять всей игрой. Мы будем добавлять возможности одну за другой.
Идея работы будет такой:
Откройте 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() вызывается ровно один раз, когда script component создается в работающей игре. Функция update(), напротив, вызывается каждый кадр, то есть по умолчанию 60 раз в секунду. Поэтому она идеально подходит для игровой логики в реальном времени.
Идея обновления такая: через заданный интервал выполнить следующее:

:::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(), то есть на “delta time” или dt.# — оператор получения длины таблицы, если она используется как массив; в нашем случае это так, потому что все сегменты — значения таблицы без явно заданных ключей.self.dir).#grid только один слой с именем layer1.i содержит позицию в таблице, начиная с 1, а s — текущий сегмент.Если сейчас запустить игру, вы увидите, как змейка длиной 4 сегмента ползет слева направо по игровому полю.

Готово!
Прежде чем добавить код для реакции на ввод игрока, нужно настроить input connections.
input файл game.input_binding и дважды щелкните по нему, чтобы открыть.
Файл input binding сопоставляет реальный пользовательский ввод (клавиши, движения мыши и т. д.) с именами действий, которые затем передаются скриптам, запросившим ввод.
Готово!
Когда привязки готовы, откройте 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), проверяем, направлен ли он противоположно 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() задаем seed генератора случайных чисел, иначе будет генерироваться одна и та же последовательность случайных значений. Этот seed нужно задать только один раз.put_food() при старте игры, чтобы игрок начинал с одним кусочком еды на карте.Готово!
Теперь определение столкновения змейки с чем-либо сводится к проверке того, что находится в tilemap там, куда движется змейка, и реакции на это.
Добавьте переменную, которая отслеживает, жива ли змейка:
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
Полезно попробовать реализовать следующие улучшения:
put_food() не учитывает позицию змейки и препятствия. Исправьте ее так, чтобы еда появлялась только на свободных местах.