Всем привет, в этом уроке мы создадим прототип механики перемещения как в Tomb of the Mask.
На основе этого урока вы сможете создать что-то подобное:

Ссылка на готовый прототип из этого урока:
Предисловие
Этот урок создан в первую очередь для начинающих. Для тех, кто только встал на путь игрового разработчика, настоятельно рекомендую ознакомиться с базовыми концепциями на официальном сайте Defold, так меньше будет вопросов во время прохождения этого урока:
В отличие от предыдущих уроков (Создаём прототип базового кликера на Defold, Создаём прототип музыкальной игры (ритм-игра)), в этом уроке я не буду использовать Git, дабы сосредоточиться только на Defold.
Создание проекта и добавление ассетов
Создадим проект:
Создадим папку
assets и подпапку png. Добавим в папку png изображение для будущего тайлового источника:Вот как может выглядеть изображение с тайловыми плитками:
Ассеты для ваших игр можете скачать на Top game assets tagged Tileset - itch.io
Я взял тайловые ассеты с проекта от britzl: publicexamples/examples/tilemap_layers at master · britzl/publicexamples · GitHub
Создание тайловой карты
Создадим папку game, подпапку core и game.tilesourse:
Выберем добавленное изображение в качестве источников тайлов:
Создадим тайловую карту:
Выберем тайловый источник для тайловой карты:
Переключаемся на слой:

Нажмём пробел и выберем тайловую плитку:
Должны увидеть тайлы:
Теперь выберете плитку и нарисуйте вашу карту:
Я изменю разрешения экрана для игры (вы можете оставить текущее или установить любое другое значение):
Создание игрока
Создадим папку player в game:
В папке
player создадим игровой объект:В игровом объекте
player создадим компонент sprite:Для спрайта выберем изображение, ссылаясь на наш тайловый источник:
Установим анимацию по умолчанию
anim:Перейдём в тайловый источник
game.tilesourse и установим тайл для анимации, я установлю такие значения:Не ставьте
fpsвысокий, если вы подвержены эпилептическому приступу
Номер тайла ты можешь посмотреть в левом верхнем углу, если наведешь на плитку:
Нажми на анимацию:
А теперь нажми
пробел. У тебя будет проигрываться анимация, отключить ты её можешь здесь:Также воспроизвести анимацию тайлов можно и в игровом объекте, достаточно выбрать компонент
sprite и нажать пробел:Ещё раз. Воспроизвести анимацию можно на
space, отключить на ctrl + T:
Создание уровня
Создадим папку level, а в ней создадим коллекцию c именем level:
В
level.collection создадим игровой объект:Переименуем его на
level:Добавим туда файловый компонент:
Выберем нашу созданную ранее тайловую карту:
Сохраните проект (
ctl + S) и нажмите ctrl + B:Мы не видим нашу тайловую карту. Дело в том, что наша коллекция
level.collection не является нашей стартовой коллекцией. Поэтому она и не загружается при запуске игры.Исправим это, укажем в качестве загрузочной коллекции нашу
level.collection в файле game.project:Сохраним(
ctrl + s) и соберем проект(ctrl + B):Теперь добавим игровой объект в
level.collection:Переместим его:
Изменим Z-позицию спрайта игрового объекта
player, чтобы спрайт персонажа был поверх тайловой карты:Сохраним и запустим проект:
Настраиваем привязки ввода
Перейдём в game.inpup_binding:
Создадим привязку ввода по клавише A(Ф):
Нажатию этой клавиши будет соответствовать
action left:Создадим привязки ввода для остальных клавиш, соответствующих передвижению вверх/вниз/вправо. Стандартное управление WASD:
Пишем логику:
Создадим в папке player скрипт player.script:
Теперь в игровой объект игрока добавим скрипт для будущей логики:
Перейдём в player.script и напишем такой код в on_input(...):
function on_input(self, action_id, action)
if action.pressed then
if action_id == hash("left") then
print("Бегу влево")
elseif action_id == hash("right") then
print("Бегу вправо")
elseif action_id == hash("up") then
print("Бегу наверх")
elseif action_id == hash("down") then
print("Бегу вниз")
end
end
end
Теперь сохраним проект(ctrl + s), соберем билд и запустим игру(ctrl+b):
Когда мы нажимаем на наши клавиши
WASD, ничего не происходит…Это потому что мы не дали скрипту “возможность” получать данные из ввода(в нашем случае, из клавиатуры).
Исправим это, добавим такую строчку кода в функцию инициализации скрипта:
function init(self)
msg.post(".", "acquire_input_focus")
end
Теперь попробуем снова собрать проект и запустить его:
Как вы видите, с каждым нажатием определённой клавиши, выводится соответствующее значение этой клавиши в консоль.
Создадим таблицу, которая будет соответствовать карте нашего уровня. 0 будет означать, что по этой плитке нельзя перемещаться.1 будет означать, что по этой плитке можно перемещаться.
Размер тайла у нас 16 пикселей.
local TILE_SIZE = 16 -- Размер тайла
-- Простая карта проходимости (0 - стена, 1 - проход)
local passability = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
}
Давайте установим начальные значение игроку:
function init(self)
-- Запрашиваем у системы ввода фокус, чтобы наш объект мог получать события ввода (например, клавиатуры)
msg.post(".", "acquire_input_focus")
-- Устанавливаем начальную позицию игрока в терминах тайлов (ячейки сетки)
self.tile_x = 1 -- начало в первом столбце
self.tile_y = 1 -- начало в первой строке
self.moving = false -- флаг, что объект сейчас неподвижен
-- Рассчитываем стартовую позицию в мировых координатах
-- Центр тайла с учетом размера тайла TILE_SIZE
local start_pos = vmath.vector3(
self.tile_x * TILE_SIZE + TILE_SIZE / 2, -- позиция по X
self.tile_y * TILE_SIZE + TILE_SIZE / 2, -- позиция по Y
0 -- позиция по Z (слой)
)
-- Устанавливаем позицию игрового объекта в вычисленную стартовую точку
go.set_position(start_pos)
end
Теперь нам нужно реализовать логику перемещения по этому полю.
Когда игрок нажимает на клавиши (W,A,S,D), то он перемещается по оси XY.
Создадим функцию для перемещения:
local function move(self, input_x, input_y)
-- Вычисляем координаты следующего тайла по осям X и Y,
-- прибавляя значения input_x и input_y к текущим координатам объекта
local next_tile_x = self.tile_x + input_x
local next_tile_y = self.tile_y + input_y
-- Рассчитываем позицию цели в мировых координатах,
-- центр тайла с учетом TILE_SIZE и смещений
local target_pos = vmath.vector3(
next_tile_x * TILE_SIZE + TILE_SIZE / 2,
next_tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
-- Запускаем анимацию перемещения игрового объекта к target_pos
-- go.animate меняет свойство "position" плавно с линейной интерполяцией
-- go.PLAYBACK_ONCE_FORWARD означает проиграть анимацию один раз вперед
-- 0.1 - длительность анимации в секундах,
-- последний параметр - функция обратного вызова, которая вызывается после окончания анимации (здесь пустая)
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target_pos, go.EASING_LINEAR, 0.1, 0, function()
end)
-- Обновляем внутренние координаты тайла объекта для дальнейшей логики
self.tile_x = next_tile_x
self.tile_y = next_tile_y
end
Обновим нашу функцию on_input(...):
function on_input(self, action_id, action)
-- Обрабатываем событие ввода, например, нажатия клавиш
-- Проверяем, что клавиша была именно нажата (pressed)
if action.pressed then
-- В зависимости от идентификатора действия вызываем движение
if action_id == hash("left") then
print("Влево!") -- Вывод в консоль для отладки направления
move(self, -1, 0) -- Движение на один тайл влево по оси X
elseif action_id == hash("right") then
print("Вправо!")
move(self, 1, 0) -- Движение на один тайл вправо
elseif action_id == hash("up") then
print("Вверх!")
move(self, 0, 1) -- Движение на один тайл вверх по оси Y
elseif action_id == hash("down") then
print("Вниз!")
move(self, 0, -1) -- Движение на один тайл вниз
end
end
end
Теперь мы можем перемещаться по полю в любом направлении:

Давайте попробуем ограничить движения игрока на поле, добавим функцию, проверяющую координаты в пределах карты:
local function is_passable(x, y)
-- Проверяем, что координаты в пределах карты и проход возможен
if y > 0 and y <= #passability and x > 0 and x <= #passability[1] then
print("Проход возможен!")
return passability[y][x] == 1
end
print("Проход запрещён!")
return false
end
Обновим функцию move(...) добавим:
local function move(self, input_x, input_y)
-- Вычисляем следующие координаты тайла, куда хотим сдвинуться
local next_tile_x = self.tile_x + input_x
local next_tile_y = self.tile_y + input_y
-- Рассчитываем мировую позицию центра целевого тайла (учитывая размер тайла)
local target_pos = vmath.vector3(
next_tile_x * TILE_SIZE + TILE_SIZE / 2,
next_tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
-- Проверяем, можно ли пройти в эти координаты (не границы и проходимо)
if is_passable(next_tile_x, next_tile_y) then
-- Запускаем анимацию плавного перемещения игрового объекта к target_pos
-- go.animate плавно изменит свойство "position" объекта до target_pos
-- go.PLAYBACK_ONCE_FORWARD — проиграть анимацию один раз вперед
-- go.EASING_LINEAR — плавное и равномерное движение
-- 0.1 — длительность анимации в секундах
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target_pos, go.EASING_LINEAR, 0.1, 0, function()
-- В колбэке можно добавить действия после завершения перемещения (пока пусто)
end)
-- Обновляем внутренние координаты текущего тайла,
-- чтобы при следующем движении знать новое расположение объекта
self.tile_x = next_tile_x
self.tile_y = next_tile_y
end
end

В текущей реализации движения, нам необходимо постоянно нажимать на клавиши, чтобы задавать направление для перемещения. Но нашей целью является возможность задать один раз направления для движения:
local function move(self, input_x, input_y)
-- Вычисляем целевые координаты тайла, куда нужно сдвинуться
local next_tile_x = self.tile_x + input_x
local next_tile_y = self.tile_y + input_y
-- Рассчитываем мировую позицию центра следующего тайла с учётом размера тайла
local target_pos = vmath.vector3(
next_tile_x * TILE_SIZE + TILE_SIZE / 2,
next_tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
-- Помечаем, что объект сейчас движется
self.moving = true
-- Сохраняем направление движения для возможного продолжения движения
self.dir = { dx = input_x, dy = input_y }
-- Проверяем, проходим ли следующий тайл
if is_passable(next_tile_x, next_tile_y) then
local object = self
-- Запускаем анимацию смещения позиции объекта к целевой точке
-- go.PLAYBACK_ONCE_FORWARD — проиграть один раз вперед,
-- go.EASING_LINEAR — линейное равномерное движение,
-- 0.05 — длительность анимации в секундах
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target_pos, go.EASING_LINEAR, 0.05, 0, function()
-- После анимации обновляем координаты текущего тайла на новые
object.tile_x = next_tile_x
object.tile_y = next_tile_y
-- Отмечаем, что движение завершено
object.moving = false
-- Если направление движения всё еще задано, рекурсивно вызываем move для плавного продолжения движения
if object.dir then
move(object, object.dir.dx, object.dir.dy)
end
end)
else
-- Если движение невозможно (непроходимо или за пределами карты),
-- сбрасываем состояние движения и направление
self.moving = false
self.dir = nil
end
end

Теперь добавим логику, которая будет препятствовать изменению направления движения во время движения ![]()
Обновленный код для move(...):
local function move(self, input_x, input_y)
-- Если объект уже движется, то не запускаем новое движение
if self.moving then
return
end
-- Вычисляем координаты следующей ячейки тайловой сетки,
-- прибавляя смещение input_x, input_y к текущим координатам объекта
local next_tile_x = self.tile_x + input_x
local next_tile_y = self.tile_y + input_y
-- Рассчитываем цель перемещения в мировых координатах (пикселях),
-- устанавливаем позицию по центру тайла с учётом TILE_SIZE
local target_pos = vmath.vector3(
next_tile_x * TILE_SIZE + TILE_SIZE / 2,
next_tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
-- Запоминаем направление движения для возможного повторного вызова move
self.dir = { dx = input_x, dy = input_y }
-- Отмечаем, что объект теперь движется
self.moving = true
-- Проверяем, можно ли пройти в ячейку с новыми координатами (next_tile_x, next_tile_y)
if is_passable(next_tile_x, next_tile_y) then
local object = self
-- Запускаем анимацию перемещения игрового объекта к target_pos
-- go.animate меняет позицию объекта плавно с линейным интерполированием за 0.01 секунды
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target_pos, go.EASING_LINEAR, 0.01, 0, function()
-- По окончании анимации:
-- обновляем внутренние координаты объекта (в терминах тайлов)
object.tile_x = next_tile_x
object.tile_y = next_tile_y
-- Снимаем флаг движения, теперь объект можно двигать снова
self.moving = false
-- Если направление движения сохранено, рекурсивно вызываем move для продолжения движения
if object.dir then
move(object, object.dir.dx, object.dir.dy)
end
end)
else
-- Если движение невозможно (стенка, препятствие, выход за пределы)
-- то отменяем состояние движения и направление
self.moving = false
self.dir = nil
end
end
Обновленный код для on_input(...):
function on_input(self, action_id, action)
-- Обработка событий ввода игрока (например, нажатия клавиш)
-- Проверяем, что клавиша была нажата (только при событии pressed)
-- и что объект не находится в процессе движения (self.moving == false)
if action.pressed and not self.moving then
-- В зависимости от идентификатора нажатого действия запускаем движение в нужном направлении
if action_id == hash("left") then
print("Влево!") -- Отладочный вывод при движении влево
move(self, -1, 0) -- Сдвиг на один тайл влево по X (-1)
elseif action_id == hash("right") then
print("Вправо!") -- Отладочный вывод при движении вправо
move(self, 1, 0) -- Сдвиг на один тайл вправо по X (+1)
elseif action_id == hash("up") then
print("Вверх!") -- Отладочный вывод при движении вверх
-- animate_move(self, 0, 1) -- (закомментирован вызов альтернативного метода анимации)
move(self, 0, 1) -- Сдвиг на один тайл вверх по Y (+1)
elseif action_id == hash("down") then
print("Вниз!") -- Отладочный вывод при движении вниз
move(self, 0, -1) -- Сдвиг на один тайл вниз по Y (-1)
end
end
end
Итоговый код:
local TILE_SIZE = 16 -- Размер одного тайла (ячейки) в пикселях. Используется для вычисления позиции в игровом мире.
-- Таблица проходимости карты. 1 – проходимо, 0 – препятствие.
-- Каждая внутренняя таблица – строка (по Y), каждый элемент – столбец (по X).
local passability = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
}
-- Функция проверки проходимости ячейки по координатам x, y (индексы тайлов)
local function is_passable(x, y)
-- Проверка, что координаты не выходят за границы массива и что ячейка проходима (значение 1)
if y > 0 and y <= #passability and x > 0 and x <= #passability[1] then
print("Проход возможен!")
return passability[y][x] == 1 -- возвращаем true если проходимо
end
print("Проход запрещён!")
return false -- вне карты или непроходимо
end
-- Функция инициализации (вызывается при старте объекта)
function init(self)
msg.post(".", "acquire_input_focus") -- получаем фокус ввода клавиатуры для объекта
-- Задаём начальные координаты игрока в терминах индексов тайлов (1,1 - верхний левый угол)
self.tile_x = 1
self.tile_y = 1
self.moving = false -- флаг, обозначающий, что игрок сейчас не движется
-- Рассчитываем позицию по пикселям так, чтобы объект стоял по центру тайла
local start_pos = vmath.vector3(
self.tile_x * TILE_SIZE + TILE_SIZE / 2,
self.tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
go.set_position(start_pos) -- выставляем позицию игрового объекта
end
-- Функция движения объекта с вектором смещения input_x, input_y (в терминах тайлов)
local function move(self, input_x, input_y)
if self.moving then
return -- если уже движемся, игнорируем новые команды движения
end
-- Вычисляем координаты целевой ячейки с учётом направления движения
local next_tile_x = self.tile_x + input_x
local next_tile_y = self.tile_y + input_y
-- Рассчитываем позицию цели в мировых координатах (пикселях)
local target_pos = vmath.vector3(
next_tile_x * TILE_SIZE + TILE_SIZE / 2,
next_tile_y * TILE_SIZE + TILE_SIZE / 2,
0
)
-- Сохраняем направление движения для возможного повторного шага
self.dir = { dx = input_x, dy = input_y }
self.moving = true -- отмечаем, что начало движения
-- Проверяем, проходима ли целевая ячейка
if is_passable(next_tile_x, next_tile_y) then
local object = self
-- Запускаем анимацию перемещения к цели (позиция меняется плавно за 0.01 сек)
go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target_pos, go.EASING_LINEAR, 0.01, 0, function()
-- По завершении анимации обновляем позицию в тайлах и снимаем флаг движения
object.tile_x = next_tile_x
object.tile_y = next_tile_y
self.moving = false
-- Если сохранилось направление, рекурсивно вызываем move для продолжения движения в том же направлении
if object.dir then
move(object, object.dir.dx, object.dir.dy)
end
end)
else
-- Если ход невозможен (препятствие или за пределами карты), отменяем движение
self.moving = false
self.dir = nil
end
end
function on_input(self, action_id, action)
-- Обрабатываем событие ввода, например, нажатия клавиш
-- Проверяем, что клавиша была именно нажата (pressed)
if action.pressed then
-- В зависимости от идентификатора действия вызываем движение
if action_id == hash("left") then
print("Влево!") -- Вывод в консоль для отладки направления
move(self, -1, 0) -- Движение на один тайл влево по оси X
elseif action_id == hash("right") then
print("Вправо!")
move(self, 1, 0) -- Движение на один тайл вправо
elseif action_id == hash("up") then
print("Вверх!")
move(self, 0, 1) -- Движение на один тайл вверх по оси Y
elseif action_id == hash("down") then
print("Вниз!")
move(self, 0, -1) -- Движение на один тайл вниз
end
end
end
Я немного увеличил скорость воспроизведения анимации:

Если есть какие-то вопросы, пишите.
Спасибо за внимание!




















































