Всем привет
Продолжаем серию разборов чужого кода с этой страницы: Публичный пример Defold .
Сегодня разбираем пример от britzl — Селектор уровня.
Попробуйте этот проект в действии: запустите пример .
Исходная папка с проектом: ссылка на github .
Исходная папка с публичными примера Defold на github: ссылка на github.
Обращение к новичкам:
Я предполагаю, что у вас уже установлен Defold, если нет, перейдите по этой ссылке: Добро пожаловать в Defold.
Также, будет плюсом, если вы хотя бы поверхностно знакомы со строительными блоками Defold. Если нет, ознакомиться с основными концепциями Defold можно на официальном сайте — перейдите по этой ссылке.6.
Пример того, как скачать и открыть готовый проект:
Скачиваем архив с примерами проектов из github
Распаковываем скачанный ZIP архив примеров в любую папку во вашему усмотрению.
Переходим в папку examples
.
Ищем проект play_animation
.
Открываем game.project
.
Внимание: скриншоты представлены ниже, это пример скачивания и открытия проекта, в этом примере мы рассматриваем п
Preformatted text
роект с названием play_animation, потому название папок проекта будет отличаться.


Этот пример демонстрирует как создать создать прокручиваемую карту выбора уровней в Defold с использованием прокси-коллекций, для динамической загрузки и выгрузки уровней.
Рассмотрим атласы:
episode[1..4].atlas
— добавлены фоновые изображения для каждого эпизода в отдельный атлас.
map.atlas
— добавлены изображения деревьев, которые будут расставляться для каждого эпизода.
Рассмотрим cursor.go:
Создан игровой объект-курсор, который невидим для нашего глаза. При этом он сможет взаимодействовать с другими игровыми объектами, имеющий компонент “объект столкновений” (collision object). Можете добавить компонент “sprite” с изображением в игровой объект и запустить пример, вы увидите, что курсор может быть обладать визуальным представлением.
Этот игровой объект нам нужен, чтобы мы могли выбирать уровень и перемещаться по эпизодам игровой карты.
Рассмотрим marker.go:
Этот игровой объект нужен для того, чтобы перейти на определенный уровень. Он также содержит компонент “объект столкновения”. Объект столкновения курсора взаимодействует с объектом столкновения маркера.
Метка отвечает за отображения номера уровня.
Рассмотрим controller.collection:
Эта загрузочная коллекция, содержит в себе игровой объект
controller
. Он отвечает за загрузку/выгрузку прокси-коллекций, переходом между картой. Скрипт реализует ту самую выгрузку/загрузку коллекций.
Рассмотрим game.collection:
Коллекция, предназначенная для игрового уровня.
Содержит метку и скрипт.
Рассмотрим episode1.collection:
Расставлены маркеры, фоновое изображение, изображения деревьев. Добавлен курсор для взаимодействия с маркерами. Скрипт эпизода, для перехода на соответствующий маркеру уровень.
С другими коллекциями episode всё также.
Рассмотрим map.collection:
У нас имеется игровой объект
controller
, он позволяет прокручивать карту вверх-вниз.Каждый эпизод (например,
ep1
, ep2
…) — это collectionproxy
, в котором размещены маркеры уровней.Эпизоды привязаны к якорным объектам
/ep1
, /ep2
, и т.д., для синхронизации их позиции.Когда эпизод попадает в зону видимости (
LOADED_THRESHOLD
) — он загружается (async_load
), когда выходит — выгружается (unload
).Пользователь может скроллить карту вверх/вниз, и уровни в эпизодах будут обновляться и доступны для выбора
Непонятно только то, зачем здесь фабрика? Объекты динамически не создаются
Рассмотрим game.script:
function on_message(self, message_id, message, sender)
if message_id == hash("play_level") then
label.set_text("#label", tostring(message.level_id))
print("playing level", message.level_id)
end
end
Приходит сообщение "play_level"
, вместе с ним приходит и значение message.level_id
, соответствующее выбранному уровню.
Устанавливается текстовое значение метке из message.level_id
.
Рассмотрим cursor.script:
function init(self)
msg.post(".", "acquire_input_focus")
end
function on_message(self, message_id, message, sender)
if message_id == hash("trigger_response") then
if message.enter then
self.marker_id = message.other_id
else
self.marker_id = nil
end
end
end
В on_message(...)
происходит обработка сообщений между объектами столкновений курсора и маркера.
Когда курсор входит в trigger
(например, игрок входит в зону), движок отправляет сообщение trigger_response
. В этом сообщении:
message.enter == true
— объект вошёл в зону.message.enter == false
— объект вышел из зоны.
Также в message.other_id
содержится список ID объектов, которые пересеклись с триггером.
Можете вывести print(message.enter)
.
triger_response относится к обработке столкновений (collisions), в частности, к событиям, вызываемым телами с типом trigger. Триггеры — это объекты, которые регистрируют простейшие явления. Они просто считывают, не взаимодействуют.
function on_input(self, action_id, action)
go.set_position(vmath.vector3(action.x, action.y, 0))
if action_id == hash("touch") and action.released and self.marker_id then
msg.post("map:/controller", "marker_selected", { id = self.marker_id })
end
end
Устанавливаем курсору положение, соответствующее входному вводу мыши. Из-за того, что в привязках ввода у нас стоит input
и action
в разделе мыши, скрипту есть что обрабатывать.
Если мы уберём привязку действий, то мы не сможем обрабатывать ввод мыши. Если мы уберём go.set_position(vmath.vector3(action.x, action.y, 0))
, то курсор не будет двигаться в том же положение, что и ваша мышь на компьютере.
if action_id == hash("touch") and action.released and self.marker_id then
msg.post("map:/controller", "marker_selected", { id = self.marker_id })
end
Если игрок кликнул на мышь и отпустил кнопку, и курсор находится в области маркера, то передадим контроллеру сообщение, что уровень выбран, и с данными, какой был выбран уровень.
Рассмотрим controller.collection:
local MAP = msg.url("controller:/controller#mapproxy")
local GAME = msg.url("controller:/controller#gameproxy")
function init(self)
msg.post(".", "acquire_input_focus")
msg.post(MAP, "load")
end
function on_message(self, message_id, message, sender)
if message_id == hash("show_game") then
self.level_id = message.level_id
msg.post(GAME, "load")
msg.post(MAP, "unload")
elseif message_id == hash("show_map") then
msg.post(MAP, "load")
msg.post(GAME, "unload")
elseif message_id == hash("proxy_loaded") then
print("proxy_loaded", sender)
msg.post(sender, "enable")
if sender == GAME then
msg.post("game:/game", "play_level", { level_id = self.level_id })
end
elseif message_id == hash("proxy_unloaded") then
print("proxy_unloaded", sender)
end
end
local MAP = msg.url("controller:/controller#mapproxy")
local GAME = msg.url("controller:/controller#gameproxy")
Этот код создаёт локальные переменные MAP
и GAME
, в которые сохраняются URL-адреса к collectionproxy
объектам внутри Defold. Эти переменные используются для указания на объекты #mapproxy
и #gameproxy
.
msg.post(MAP, load)
Отправляем сообщение на коллекцию по адресу, хранящемуся в переменной MAP
. Загрузить коллекцию. Когда пример запускает, загружается эта коллекция — коллекция карты.
...
if message_id == hash("show_game") then
self.level_id = message.level_id
msg.post(GAME, "load")
msg.post(MAP, "unload")
elseif message_id == hash("show_map") then
msg.post(MAP, "load")
msg.post(GAME, "unload")
...
Этот кода отвечает за переключение между коллекциями, путём отправки сообщений компонентам прокси-коллекций. Какую загружаем коллекцию, какую выгружаем, всё обрабатывается здесь.
...
elseif message_id == hash("proxy_loaded") then
print("proxy_loaded", sender)
msg.post(sender, "enable")
if sender == GAME then
msg.post("game:/game", "play_level", { level_id = self.level_id })
end
elseif message_id == hash("proxy_unloaded") then
print("proxy_unloaded", sender)
end
...
Если коллекция загружена, выводим сообщение в консоль print("proxy_loaded", sender)
. Если коллекция выгружается, то выводим сообщение в консоль print("proxy_unloaded", sender)
.
if sender == GAME then
msg.post("game:/game", "play_level", { level_id = self.level_id })
end
Отправим в коллекцию game
, компоненту скрипт game
сообщение. Помните, в game.script
имеется обработка сообщения по изменению текста компоненту label ?
Рассмотрим episode.script:
go.property("episode", 1)
function init(self)
self.target_pos = go.get_position()
for i=1,5 do
local marker_id = hash("/marker" .. i)
local level_id = i + ((self.episode - 1) * 5)
label.set_text(msg.url(nil, marker_id, "label"), tostring(level_id))
msg.post("map:/controller", "register_marker", { level_id = level_id, marker_id = marker_id })
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("update_position") then
go.set_position(message.position)
end
end
go.property("episode", 1)
Этот код определяет свойство объекта. С помощью этого кода можно будет задать номер эпизода. Это позволяет использовать один и тот же игровой объект с одним скриптом для разных уровней.
self.target_pos = go.get_position()
Сохраняет текущую позицию объекта как целевую target_pos.
...
for i=1,5 do
local marker_id = hash("/marker" .. i)
local level_id = i + ((self.episode - 1) * 5)
...
Цикл создаёт 5 маркеров.
marker_id
— это ID вроде hash(“/marker1”), hash(“/marker2”) и т.д.
level_id
рассчитывает номер уровня с учётом эпизода:
Если episode
== 1, то уровни: 1–5.
Если episode
== 2, то уровни: 6–10.
label.set_text(msg.url(nil, marker_id, "label"), tostring(level_id))
- Устанавливает текст на компонент
label
, расположенный внутри каждого маркера. msg.url(nil, marker_id, "label")
— создаёт URL к label-компоненту маркера.
...
msg.post("map:/controller", "register_marker", { level_id = level_id, marker_id = marker_id })
end
...
Отправляет сообщение "register_marker"
на контроллер карты в map.collection
, передаёт:
level_id
— номер уровня.marker_id
— ID объекта-маркера.
Это нужно, чтобы контроллер карты знал, какие маркеры соответствуют каким уровням.
...
if message_id == hash("update_position") then
go.set_position(message.position)
end
...
Обрабатывает сообщение “update_position”, чтобы переместить объект на новую позицию.
Коллекция эпизода будет устанавливаться на полученную позицию(якорь из другой коллекции).
Рассмотрим map.script:
Этот код представляет собой основной контроллер для прокручиваемой карты эпизодов в Defold, где каждый эпизод представлен как отдельная коллекция (collectionproxy
). Он реализует динамическую загрузку/выгрузку эпизодов, обработку прокрутки, а также выбор уровней внутри эпизодов.
local episodes = {
{
proxy = msg.url("map:/episodes#ep1proxy"), markers = {},
},
{
proxy = msg.url("map:/episodes#ep2proxy"), markers = {},
},
{
proxy = msg.url("map:/episodes#ep3proxy"), markers = {},
},
{
proxy = msg.url("map:/episodes#ep4proxy"), markers = {},
},
}
episodes
— таблица с информацией для эпизодов.
Каждый эпизод:
proxy
— ссылка наcollectionproxy
эпизода.markers
— таблица маркеров уровней в эпизоде.
-- precalculate some links and ids
for i,episode in ipairs(episodes) do
episode.id = i
-- the anchor is the game object this episode should position itself as
episode.anchor = hash("/ep" .. i)
-- collection name (the socket part of a defold url) this episode belongs to
episode.socket = hash("ep" .. i)
-- url to the root game object of the episode
episode.root = msg.url(episode.socket, "/bg", nil)
end
Дополнительные свойства к таблице добавляются через цикл:
for i, episode in ipairs(episodes)
:
id
: порядковый номер.anchor
: якорный объект, например,/ep1
,/ep2
— по нему определяется, где должен быть эпизод.socket
: используется в URL-адресе как имя коллекции.root
: URL к корневому GameObject эпизода (например,/bg
), используется для обновления позиции.
local EPISODE_HEIGHT = 1920
local SCREEN_HEIGHT = sys.get_config("display.height")
local LOADED_THRESHOLD = (SCREEN_HEIGHT / 2) + EPISODE_HEIGHT * 1.5
EPISODE_HEIGHT
— высота одного эпизода.
SCREEN_HEIGHT
— высота экрана, указанного в game.project.
LOADED_THRESHOLD
— вертикальный диапазон видимости: если эпизод попадает в него — он должен быть загружен.
SCREEN_HEIGHT
- Это половина высоты экрана — от центра до верхнего или нижнего края.
- Почему половина? Потому что якорь (
episode.anchor
) обычно находится в центре эпизода, и мы хотим сравнивать центр эпизода с краем видимой области.
EPISODE_HEIGHT * 1.5
- Это буферная зона в 1.5 высоты эпизода.
- Она нужна, чтобы начать загружать эпизод ещё до того, как он полностью войдёт на экран.
- Это создаёт эффект предзагрузки, чтобы эпизод успел подгрузиться к тому моменту, когда пользователь до него доскроллит.
Без буфера:
- Эпизод начал бы загружаться только тогда, когда его якорь оказался точно в пределах экрана — это может привести к задержке: пользователь уже видит уровень, а он ещё не готов.
С буфером:
- Эпизод загружается заранее, когда он ещё чуть ниже или выше экрана.
- Это даёт время на
async_load()
иproxy_loaded
, чтобы загрузка прошла незаметно для игрока.
local function update_positions()
for i,episode in pairs(episodes) do
-- the episode will follow the anchor game object
local position = go.get_world_position(episode.anchor)
-- update the position of the episode if it's loaded, unless we're unloading it
if episode.loaded and not episode.unloading then
msg.post(episode.root, "update_position", { position = position})
end
-- toggle loaded/unloaded state based on position of episode
local should_be_loaded = position.y > -LOADED_THRESHOLD and position.y < LOADED_THRESHOLD
if should_be_loaded and not episode.loaded and not episode.loading then
load_episode(i)
elseif not should_be_loaded and episode.loaded and not episode.unloading then
unload_episode(i)
end
end
end
Этот фрагмент кода отвечает за управление загрузкой, выгрузкой и позиционированием эпизодов на карте, привязанных к якорным объектам (anchor
). Он вызывается каждый кадр через update()
.
for i, episode in pairs(episodes) do
Перебирает все эпизоды (episodes) — таблицу, содержащую данные об эпизодах, включая proxy, anchor, loaded, и т.д.
local position = go.get_world_position(episode.anchor)
Получает мировую позицию якорного объекта (anchor) текущего эпизода.
Что такое “мировая позиция”?
В 2D или 3D играх у каждого объекта есть:
Локальная позиция (local position)
- Это позиция относительно родителя.
- Например, если объект находится на позиции
(100, 0)
в родителе, и родитель сам смещён, то итоговая позиция будет другая.
Мировая позиция (world position)
- Это абсолютная позиция объекта в мире с учётом всех вложенностей и смещений родителей.
Якорь (episode.anchor) — это Game Object, к которому привязан эпизод. Он “ведёт” за собой коллекцию.
Для лучшего понимания я добавил к якорю спрайт(синяя полоска — это спрайт якоря):
Якорь расположен на позиции. На позицию которого будет загружена коллекция.
if episode.loaded and not episode.unloading then
msg.post(episode.root, "update_position", { position = position })
end
Если эпизод уже загружен (episode.loaded == true
) и не находится в процессе выгрузки:
Отправляется сообщение update_position
в корневой Game Object (ep1:/bg
) коллекции.
Это позволяет синхронизировать положение эпизода с якорем — например, при скролле экрана.
local should_be_loaded = position.y > -LOADED_THRESHOLD and position.y < LOADED_THRESHOLD
Вычисляется, находится ли эпизод в пределах экрана (или рядом):
Если position.y
попадает в диапазон (-LOADED_THRESHOLD, +LOADED_THRESHOLD
), значит эпизод должен быть загружен.
LOADED_THRESHOLD
— заранее рассчитанный диапазон видимости (на основе высоты экрана и эпизода).
if should_be_loaded and not episode.loaded and not episode.loading then
load_episode(i)
Если эпизод должен быть видим:
И он ещё не загружен (episode.loaded
== false
)
И в процессе загрузки он не находится (episode.loading
== false
), тогда вызывается load_episode(i)
, чтобы начать загрузку через async_load
.
elseif not should_be_loaded and episode.loaded and not episode.unloading then
unload_episode(i)
end
Если эпизод вышел за пределы видимости, и он:
Уже загружен.
Ещё не выгружается. Тогда вызывается unload_episode(i)
для освобождения ресурсов.
function on_message(self, message_id, message, sender)
if message_id == hash("register_marker") then
local episode = find_episode_from_url(sender)
episode.markers[message.marker_id] = message.level_id
elseif message_id == hash("marker_selected") then
local episode = find_episode_from_url(sender)
local level_id = episode.markers[message.id]
msg.post("controller:/controller", "show_game", { level_id = level_id })
elseif message_id == hash("proxy_loaded") then
print("loaded", sender)
msg.post(sender, "enable")
local episode = find_episode_from_proxy(sender)
episode.loading = false
episode.loaded = true
msg.post(episode.root, "update_position", { position = go.get_world_position(episode.anchor), instant = true })
elseif message_id == hash("proxy_unloaded") then
print("unloaded", sender)
local episode = find_episode_from_proxy(sender)
episode.unloading = false
episode.loaded = false
end
end
...
if message_id == hash("register_marker") then
local episode = find_episode_from_url(sender)
episode.markers[message.marker_id] = message.level_id
...
Это сообщение отправляется из эпизода, когда он загружается и регистрирует свои маркеры уровней Это нужно, чтобы при выборе маркера игра знала, какой уровень за ним стоит.
elseif message_id == hash("marker_selected") then
local episode = find_episode_from_url(sender)
local level_id = episode.markers[message.id]
msg.post("controller:/controller", "show_game", { level_id = level_id })
Это сообщение отправляется, когда пользователь нажал на маркер уровня.
- Определяется, из какого эпизода пришло нажатие (
find_episode_from_url(sender)
). - По
message.id
(этоmarker_id
) находится нужныйlevel_id
. - Посылается сообщение
"show_game"
в контроллер, чтобы запустить нужный уровень.
elseif message_id == hash("proxy_loaded") then
print("loaded", sender)
msg.post(sender, "enable")
local episode = find_episode_from_proxy(sender)
episode.loading = false
episode.loaded = true
msg.post(episode.root, "update_position", { position = go.get_world_position(episode.anchor), instant = true })
Это системное сообщение от collectionproxy
, когда загрузка эпизода завершена.
episode.loading = false
, episode.loaded = true
— обновляет флаги состояния.
Отправляется сообщение "update_position"
в корень объекта эпизода, чтобы установить его позицию по якорю сразу после загрузки.
Это нужно, что бы правильно отобразить загруженный эпизод и синхронизировать его позицию.
elseif message_id == hash("proxy_unloaded") then
print("unloaded", sender)
local episode = find_episode_from_proxy(sender)
episode.unloading = false
episode.loaded = false
Это сообщение от collectionproxy
, когда эпизод полностью выгружен.
- Выводит отладку.
- Снимает флаг
unloading
и ставитloaded = false
.
Это нужно, чтобы отметить, что эпизод выгружен, и его ресурсы освобождены.
function on_input(self, action_id, action)
local action_pos = vmath.vector3(action.x, action.y, 0)
go.set_position(action_pos)
if action_id == hash("touch") then
if action.pressed then
self.pressed = true
self.last_pos = action_pos
end
-- scroll episodes while left mouse button is pressed
if self.pressed then
-- scroll the episodes root
-- don't scroll horizontally
-- or vertically outside bounds
self.episodes_pos = self.episodes_pos + (action_pos - self.last_pos)
self.episodes_pos.x = 0
self.episodes_pos.y = math.max(math.min(self.episodes_pos.y, 0), -(EPISODE_HEIGHT - SCREEN_HEIGHT + (#episodes - 1) * EPISODE_HEIGHT))
self.last_pos = action_pos
end
if action.released then
self.pressed = false
end
end
end
local action_pos = vmath.vector3(action.x, action.y, 0)
go.set_position(action_pos)
action.x
и action.y
— координаты касания (или мыши).
Создаётся вектор action_pos
, представляющий текущую точку ввода.
go.set_position(action_pos)
— перемещает текущий Game Object(controller) в точку касания.
if action_id == hash("touch") then
Проверка: используется ли идентификатор "touch"
(задан в input.bindings
).
Это может быть как нажатие мыши, так и касание пальцем.
if action.pressed then
self.pressed = true
self.last_pos = action_pos
end
При первом нажатии (pressed == true
):
Сохраняется, что пользователь начал ввод (self.pressed = true)
Запоминается позиция начала (self.last_pos
), чтобы затем вычислять смещение.
if self.pressed then
self.episodes_pos = self.episodes_pos + (action_pos - self.last_pos)
Пока пользователь держит палец/кнопку мыши:
Вычисляется смещение (разница между текущей и предыдущей позицией).
Это смещение добавляется к текущей позиции корневого объекта /episodes
.
self.episodes_pos.x = 0
Горизонтальное движение запрещено (фиксируем X).
self.episodes_pos.y = math.max(
math.min(self.episodes_pos.y, 0),
-(EPISODE_HEIGHT - SCREEN_HEIGHT + (#episodes - 1) * EPISODE_HEIGHT)
)
Это ограничение на вертикальное смещение объекта /episodes, чтобы не дать пользователю проскроллить слишком далеко вверх или вниз.
self.last_pos = action_pos
Обновляем last_pos для следующего кадра.
if action.released then
self.pressed = false
end
Когда пользователь отпустил экран/мышь — флаг pressed сбрасывается.
local function load_episode(id)
local episode = episodes[id]
episode.loading = true
msg.post(episode.proxy, "async_load")
end
Из глобальной таблицы episodes
выбирается эпизод с указанным id.
Устанавливается флаг loading
, чтобы пометить, что эпизод начал загружаться.
Это защищает от повторной попытки загрузки, пока он ещё не закончил загружаться.
Отправляется сообщение async_load
в collectionproxy
, указанный в episode.proxy
.
Это запускает асинхронную загрузку эпизода — коллекция начнёт загружаться в фоне.
Позже Defold отправит сообщение proxy_loaded
, когда загрузка завершится.
Зачем это нужно?
Подгрузка эпизодов только тогда, когда они находятся в зоне видимости.
За счёт async_load
ресурсы не грузятся все сразу, а по мере необходимости.
Флаг loading
предотвращает дублирующую загрузку.
local function unload_episode(id)
local episode = episodes[id]
episode.unloading = true
msg.post(episode.proxy, "unload")
end
Из глобальной таблицы episodes
выбирается эпизод с указанным id.
Устанавливается флаг unloading
, чтобы пометить, что эпизод сейчас выгружается.
Это используется для защиты: пока идёт выгрузка, повторная выгрузка не начнётся. Отправляет сообщение "unload"
в соответствующий collectionproxy
эпизода.
Это приводит к выгрузке коллекции — все игровые объекты внутри collectionproxy
будут удалены, ресурсы освобождены
local function find_episode_from_proxy(proxy_url)
for i = 1, #episodes do
if episodes[i].proxy == proxy_url then
return episodes[i]
end
end
end
for i = 1, #episodes do
- Проходит по всей таблице
episodes
, в которой хранятся данные по каждому эпизоду. #episodes
— это количество эпизодов.
if episodes[i].proxy == proxy_url then
— сравнивает текущий proxy
из таблицы с тем, что был передан в аргументе proxy_url
.
return episodes[i]
— возвращает найденный эпизод как таблицу (с его флагами, якорем, root, markers и т.д.).
Зачем нужна эта функция?
Чтобы сопоставить URL компонента proxy с логической структурой эпизода.
Это позволяет обновить флаги episode.loading
, episode.loaded
и т.д., или управлять другими действиями (например, позиционированием).
local function find_episode_from_url(url)
for i=1,#episodes do
if episodes[i].socket == url.socket then
return episodes[i]
end
end
end
Это вспомогательная функция, которая по любой ссылке msg.url
на объект внутри коллекции (эпизода) находит, к какому эпизоду он принадлежит. Возвращает таблицу с данными эпизода.
Кратко соберём весь код в цельную картину:
Мы запускаем игру. Контролер, отвечающий за загрузку/выгрузку состояния игры загружает с помощью прокси-коллекции коллекцию карты. В этой карте загружаются данные эпизодов. Загружается эпизод и маркеры. Дальше вступает в дело контроллер карты. Коллекция эпизода накладывается на позицию якоря. Когда происходит прокрутка, коллекции эпизодов загружаются/выгружаются параллельно самой игре. Когда кликаем на маркеры, осуществляется переход на соответствующий уровень через контроллер.
Надеюсь, кому-нибудь этот материал будет полезен.
Всем спасибо за внимание
Другие разборы:
- Разбор проектов на Defold 1.Tilemap Collisions [tilemap]
- Разбор проектов на Defold 2. Параллакс [parallax]
- Разбор проектов на Defold 3. Движение игровых объектов [animation, movement, input]
- Разбор проектов на Defold 4. Воспроизвести анимацию [animation, movement, input]
- Разбор проектов на Defold 5. Меню и игра. Прокси-коллекции [proxy-collection, gameloop, collection, gui]
- Разбор проектов на Defold 6. Пауза [пауза]
- Разбор проектов на Defold 7. Простая кнопка [простая кнопка]
- Разбор проектов на Defold 8. Фабрики и свойства [фабрики и свойства]