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
В этом учебнике мы начнём с пустого проекта и соберём полноценную игру-раннер с анимированным персонажем, физическими столкновениями, сбором предметов и подсчётом очков.
При изучении нового игрового движка нужно усвоить много нового, поэтому мы создали этот учебник как удобный старт. Это довольно полный разбор, который показывает, как работают движок и редактор. Мы предполагаем, что вы уже немного знакомы с программированием.
Если вам нужно введение в Lua, посмотрите наше руководство Lua in Defold.
Если этот учебник кажется вам слишком сложным для первого знакомства, загляните на страницу учебников, где собраны материалы разной сложности.
Если вам удобнее смотреть видео, обратите внимание на видеоверсию на Youtube.
Мы используем игровые ресурсы из двух других учебников с небольшими изменениями. Учебник разбит на несколько шагов, и каждый из них заметно приближает нас к финальной игре.
В результате получится игра, где вы управляете героем, который бежит по окружению, собирает монеты и избегает препятствий. Герой движется с постоянной скоростью, а игрок управляет только прыжком одной кнопкой (или касанием экрана на мобильном устройстве). Уровень состоит из бесконечного потока платформ, по которым нужно прыгать, и монет, которые нужно собирать.
Если вы застрянете на любом этапе этого учебника или при создании своей игры, не стесняйтесь обращаться за помощью на форум Defold. Там можно обсуждать Defold, задавать вопросы команде Defold, смотреть, как другие разработчики решают свои задачи, и находить новое вдохновение.
На протяжении учебника подробные объяснения отдельных концепций и приёмов помечаются такими абзацами. Если чувствуете, что эти секции уходят в слишком большую глубину, смело пропускайте их.
Итак, начнём. Надеемся, вы получите удовольствие от прохождения этого учебника и он поможет вам уверенно начать работать с Defold.
Скачать ресурсы для этого учебника можно здесь.
Первый шаг — загрузить следующие файлы.
Если редактор Defold у вас ещё не установлен, самое время это исправить:
Перейдите на страницу загрузки Defold, где вы найдёте кнопки загрузки для macOS, Windows и Linux (Ubuntu):

Чтобы запустить редактор, откройте папку “Applications” и дважды щёлкните по файлу “Defold”.

D:\Defold). Не рекомендуется перемещать Defold в C:\Program Files (x86)\ или C:\Program Files\, так как это может помешать обновлению редактора.Чтобы запустить редактор, откройте папку “Defold” и дважды щёлкните по файлу “Defold.exe”.

$ unzip Defold-x86_64-linux.zip -d Defold
Чтобы запустить редактор, перейдите в папку, куда вы его распаковали, затем выполните исполняемый файл Defold или дважды щёлкните по нему на рабочем столе.
$ cd Defold
$ ./Defold
В меню Help > Create Desktop Entry есть помощник для создания ярлыка на рабочем столе.
Если возникли проблемы с запуском редактора, открытием проекта или запуском игры на Defold, обратитесь к разделу про Linux в FAQ.
Все бета- и стабильные версии Defold также доступны на GitHub.
После установки и запуска редактора создайте новый проект и подготовьте его к работе. Создайте новый проект на основе шаблона “Empty Project”.
В этом учебнике используются возможности Spine, которые после Defold 1.2.188 были вынесены в отдельное расширение. Если вы используете более новую версию, добавьте Spine Extension в раздел dependencies файла game.project.
При первом запуске редактор открывается пустым, без загруженного проекта, поэтому выберите в меню Open Project и укажите только что созданный проект. Также редактор предложит создать для проекта “branch”.
Теперь в панели Assets вы увидите все файлы проекта. Если дважды щёлкнуть по файлу “main/main.collection”, он откроется в центральной области редактора:

Редактор состоит из следующих основных областей:
print() и pprint() из скриптов. Если приложение или игра не запускается, в первую очередь стоит смотреть именно сюда. За вкладкой консоли находятся также вкладки с информацией об ошибках и редактор кривых, который используется при создании particle effects.Шаблон проекта “Empty” на самом деле полностью пуст. Тем не менее выберите Project ▸ Build, чтобы собрать проект и запустить игру.

Чёрный экран — зрелище не самое захватывающее, но это уже работающее игровое приложение на Defold, и мы легко превратим его во что-то более интересное.
Редактор Defold работает с файлами. Дважды щёлкнув по файлу в Assets pane, вы открываете его в подходящем редакторе и можете работать с его содержимым.
Когда вы заканчиваете редактирование файла, его нужно сохранить. Выберите File ▸ Save в главном меню. Редактор подсказывает это, добавляя звёздочку ‘*’ к имени файла на вкладке, если в файле есть несохранённые изменения.

Прежде чем идти дальше, зададим несколько параметров проекта. Откройте ресурс game.project из Assets Pane, прокрутите до раздела Display и установите width и height в 1280 и 720.
Также нужно добавить в проект расширение Spine, чтобы анимировать героя. Добавьте версию Spine extension, совместимую с вашей установленной версией редактора Defold. Доступные версии Spine можно посмотреть здесь:
https://github.com/defold/extension-spine/releases
Щёлкните правой кнопкой по ссылке на zip-файл нужного релиза:

Добавьте ссылку на релиз в список game.project dependencies. После добавления Spine extension редактор нужно перезапустить, чтобы активировать включённую в него интеграцию.
Начнём с самых первых шагов и создадим для персонажа окружение, а точнее — кусок прокручивающейся земли. Сделаем это поэтапно.
Atlas — это файл, который объединяет несколько отдельных изображений в одно большое. Это экономит место и повышает производительность. Подробнее об Atlas и других возможностях 2D-графики читайте в документации по 2D graphics.


Почему ничего не работает!? Частая проблема у новичков в Defold — забыть сохранить файл. После добавления изображений в atlas нужно сохранить файл, прежде чем это изображение станет доступным для использования.
Создайте файл коллекции ground.collection для земли и добавьте в него 7 game object (щёлкните правой кнопкой по корню коллекции в Outline и выберите Add Game Object). Назовите объекты “ground0”, “ground1”, “ground2” и т. д., изменяя свойство Id в Properties. Обратите внимание: Defold автоматически назначает новым game object уникальный id.
Для каждого объекта добавьте компонент sprite (щёлкните правой кнопкой по game object в Outline, выберите Add Component, затем Sprite и OK), установите свойство Image этого sprite на только что созданный atlas и выберите одной из двух картинок земли в качестве default animation. Задайте X-позицию компонента sprite (а не game object) равной 190 и Y-позицию равной 40. Поскольку ширина изображения — 380 пикселей, а мы смещаем его вбок на половину ширины, pivot game object окажется на левом краю изображения sprite.


Проще всего, вероятно, собрать один полностью настроенный и масштабированный game object со sprite component, а затем копировать его. Выберите его в Outline, затем выполните Edit ▸ Copy, а потом Edit ▸ Paste.
Обратите внимание: если вы захотите использовать более крупные или более мелкие тайлы, достаточно изменить масштаб. Но тогда также придётся изменить X-позиции всех ground game object, чтобы они соответствовали новой ширине.
Сохраните файл, затем добавьте ground.collection в main.collection: сначала откройте main.collection, затем щёлкните правой кнопкой по корневому объекту в Outline и выберите Add Collection From File. В диалоге выберите ground.collection и нажмите OK. Убедитесь, что размещаете ground.collection в позиции 0, 0, 0, иначе она будет визуально смещена. Сохраните.
Запустите игру (Project ▸ Build), чтобы убедиться, что всё на месте.

На этом этапе вы, возможно, уже задаётесь вопросом, что вообще представляют собой все эти вещи, которые мы создаём, поэтому давайте на минуту посмотрим на самые базовые строительные блоки любого проекта Defold:
Пока этого описания, вероятно, достаточно. Однако гораздо более глубокий разбор есть в руководстве Building blocks. Позже к нему полезно вернуться, чтобы лучше понять, как работает Defold.
Теперь, когда все куски земли на месте, заставить их двигаться довольно просто. Идея такая: сдвигаем куски справа налево, а когда какой-то из них оказывается за левой границей экрана, переносим его в крайнее правое положение. Чтобы перемещать все эти game object, нужен Lua-скрипт, так что создадим его:
-- ground.script
local pieces = { "ground0", "ground1", "ground2", "ground3",
"ground4", "ground5", "ground6" } -- <1>
function init(self) -- <2>
self.speed = 360 -- Speed in pixels/s
end
function update(self, dt) -- <3>
for i, p in ipairs(pieces) do -- <4>
local pos = go.get_position(p)
if pos.x <= -228 then -- <5>
pos.x = 1368 + (pos.x + 228)
end
pos.x = pos.x - self.speed * dt -- <6>
go.set_position(pos, p) -- <7>
end
end
init() вызывается, когда game object начинает существовать в игре. В ней мы инициализируем локальную переменную объекта, которая хранит скорость движения земли.update() вызывается каждый кадр, обычно 60 раз в секунду. dt содержит количество секунд с момента предыдущего вызова.dt даёт независимую от частоты кадров скорость в пикселях в секунду.Defold — это быстрый core-движок, который управляет вашими данными и game object. Любая логика и поведение, необходимые игре, пишутся на Lua. Lua — это быстрый и лёгкий язык, отлично подходящий для игровой логики. Есть много хороших ресурсов по изучению языка, например книга Programming in Lua и официальный Lua reference manual.
Defold добавляет поверх Lua набор API, а также систему message passing, которая позволяет программировать взаимодействие между game object. Подробнее читайте в руководстве Message passing.
Панели Assets Pane, Console и Outline в редакторе можно скрывать и показывать клавишами F6, F7 и F8 соответственно.
Теперь, когда файл скрипта готов, нужно добавить ссылку на него как на компонент в game object. Тогда скрипт будет выполняться в рамках жизненного цикла объекта. Мы сделаем это, создав новый game object в ground.collection и добавив к нему Script component, который ссылается на только что созданный Lua-файл:

Теперь при запуске игры game object “controller” будет выполнять скрипт из своего Script component, и земля будет плавно прокручиваться по экрану.
Герой будет game object, состоящим из следующих компонентов:
Начните с импорта изображений частей тела, затем добавьте их в новый atlas с именем hero.atlas:

Также нужно импортировать анимационные данные Spine и создать для них Spine Scene:

Файл hero.spinejson экспортирован в формате Spine JSON. Чтобы создавать такие файлы, вам понадобится программа Spine. Если вы хотите использовать другое ПО для анимации, можно экспортировать анимации в виде sprite sheet и использовать их как flip-book анимации из ресурсов Tile Source или Atlas. Подробнее — в руководстве Animation.
Теперь можно приступать к сборке game object героя:

Теперь пора добавить физику, чтобы работали столкновения:
Коллизия “Kinematic” означает, что столкновения должны регистрироваться, но physics engine не будет автоматически разрешать их и симулировать объекты. Physics engine поддерживает несколько разных типов collision object. Подробнее о них читайте в документации по Physics.
Важно указать, с чем именно должен взаимодействовать collision object:
В завершение создайте новый файл hero.script и добавьте его в game object.
handle_geometry_contact().)
Причина, по которой мы обрабатываем столкновения вручную, в том, что если вместо этого задать тип collision object персонажа как dynamic, движок будет выполнять ньютоновскую симуляцию задействованных тел. Для игры вроде этой такая симуляция далека от оптимума, поэтому вместо борьбы с physics engine через разные силы мы берём полный контроль на себя.
Чтобы сделать это и корректно обрабатывать столкновения, потребуется немного векторной математики. Подробное объяснение того, как решать kinematic collisions, есть в документации Physics.
-- gravity pulling the player down in pixel units/sˆ2
local gravity = -20
-- take-off speed when jumping in pixel units/s
local jump_takeoff_speed = 900
function init(self)
-- this tells the engine to send input to on_input() in this script
msg.post(".", "acquire_input_focus")
-- save the starting position
self.position = go.get_position()
-- keep track of movement vector and if there is ground contact
self.velocity = vmath.vector3(0, 0, 0)
self.ground_contact = false
end
function final(self)
-- Return input focus when the object is deleted
msg.post(".", "release_input_focus")
end
function update(self, dt)
local gravity = vmath.vector3(0, gravity, 0)
if not self.ground_contact then
-- Apply gravity if there's no ground contact
self.velocity = self.velocity + gravity
end
-- apply velocity to the player character
go.set_position(go.get_position() + self.velocity * dt)
-- reset volatile state
self.correction = vmath.vector3()
self.ground_contact = false
end
local function handle_geometry_contact(self, normal, distance)
-- project the correction vector onto the contact normal
-- (the correction vector is the 0-vector for the first contact point)
local proj = vmath.dot(self.correction, normal)
-- calculate the compensation we need to make for this contact point
local comp = (distance - proj) * normal
-- add it to the correction vector
self.correction = self.correction + comp
-- apply the compensation to the player character
go.set_position(go.get_position() + comp)
-- check if the normal points enough up to consider the player standing on the ground
-- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
if normal.y > 0.7 then
self.ground_contact = true
end
-- project the velocity onto the normal
proj = vmath.dot(self.velocity, normal)
-- if the projection is negative, it means that some of the velocity points towards the contact point
if proj < 0 then
-- remove that component in that case
self.velocity = self.velocity - proj * normal
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("contact_point_response") then
-- check if we received a contact point message. One message for each contact point
if message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
local function jump(self)
-- only allow jump from ground
if self.ground_contact then
-- set take-off speed
self.velocity.y = jump_takeoff_speed
end
end
local function abort_jump(self)
-- cut the jump short if we are still going up
if self.velocity.y > 0 then
-- scale down the upwards speed
self.velocity.y = self.velocity.y * 0.5
end
end
function on_input(self, action_id, action)
if action_id == hash("jump") or action_id == hash("touch") then
if action.pressed then
jump(self)
elseif action.released then
abort_jump(self)
end
end
end
Если хотите, можно временно добавить героя в основную коллекцию и запустить игру, чтобы увидеть, как он падает сквозь мир.
Последнее, что нужно герою для работоспособности, — это ввод. Скрипт выше уже содержит функцию on_input(), которая реагирует на действия “jump” и “touch” (для сенсорных экранов). Добавим input bindings для этих действий.

Теперь, когда герой настроен и столкновения работают, нужно добавить столкновения и к земле, чтобы герою было с чем взаимодействовать (или по чему бежать). Мы займёмся этим через секунду, но сначала немного порефакторим структуру и вынесем всё, относящееся к уровню, в отдельную collection:
Как вы, возможно, уже заметили, иерархия файлов в Assets pane не зависит напрямую от структуры содержимого, которое вы строите в collections. Отдельные файлы используются по ссылкам из collection- и game object-файлов, но их физическое расположение на диске может быть любым.
Если нужно переместить файл в новое место, Defold помогает автоматически обновлять ссылки на него (refactoring). При разработке сложного проекта, вроде игры, очень полезно иметь возможность менять структуру по мере роста проекта. Defold это поощряет и делает процесс плавным, так что не бойтесь перемещать файлы.
Также стоит добавить game object-контроллер со script component в level collection:
Откройте script file, вставьте следующий код и сохраните его:
-- controller.script
go.property("speed", 360) -- <1>
function init(self)
msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
end

Game object “controller” не существует в отдельном файле, а создаётся прямо внутри level collection. Это значит, что экземпляр объекта создаётся из in-place данных. Для одноцелевых объектов вроде этого это вполне нормально. Если же вам нужны несколько экземпляров какого-то объекта и вы хотите иметь возможность менять общий prototype/template, просто создайте game object file и добавляйте объект в collection из файла. Тогда game object будет ссылаться на файл как на prototype/template.
Назначение game object “controller” — управлять всем, что относится к запущенному уровню. Скоро этот скрипт будет отвечать за спаун платформ и монет, но пока он будет только задавать скорость уровня.
В функции init() level controller script отправляет сообщение script component объекта ground controller, адресуя его по id:
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
Идентификатор game object controller равен "ground/controller", поскольку он живёт внутри коллекции “ground”. После символа "#" мы добавляем id компонента — "controller", чтобы отделить object id от component id. Обратите внимание: ground script пока не умеет реагировать на сообщение set_speed, поэтому нужно добавить в ground.script функцию on_message() и соответствующую логику.
-- ground.script
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then -- <1>
self.speed = message.speed -- <2>
end
end

На этом этапе нужно добавить физические столкновения для земли:

Теперь можно попробовать запустить игру (Project ▸ Build). Герой должен бежать по земле, и должна появиться возможность прыгать клавишей Space. Если запускать игру на мобильном устройстве, прыгать можно касанием экрана.
Чтобы мир не был таким скучным, добавим платформы, на которые можно прыгать.
Создайте файл Script под названием platform.script (щёлкните правой кнопкой в Assets pane, затем New ▸ Script File) и вставьте в него следующий код, после чего сохраните:
-- platform.script
function init(self)
self.speed = 540 -- Default speed in pixels/s
end
function update(self, dt)
local pos = go.get_position()
if pos.x < -500 then
go.delete() -- <1>
end
pos.x = pos.x - self.speed * dt
go.set_position(pos)
end
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then
self.speed = message.speed
end
end

Обратите внимание: и platform.go, и platform_long.go содержат Script component, которые ссылаются на один и тот же script file. Это хорошо, потому что любые изменения script file повлияют на поведение и обычных, и длинных платформ.
По задумке игра должна быть простым бесконечным раннером. Это означает, что platform game object нельзя просто разместить в collection через editor. Вместо этого их нужно спаунить динамически:
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 } -- <1>
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
end
function update(self, dt) -- <2>
self.gridw = self.gridw + self.speed * dt
if self.gridw >= grid then
self.gridw = 0
-- Maybe spawn a platform at random height
if math.random() > 0.2 then
local h = platform_heights[math.random(#platform_heights)]
local f = "#platform_factory"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
msg.post(p, "set_speed", { speed = self.speed })
end
end
end
1- Предопределённые значения Y-позиции, на которых можно спаунить платформы.
2- Функция update() вызывается каждый кадр, и мы используем её, чтобы через определённые интервалы (во избежание перекрытий) и на определённых высотах решать, спаунить обычную или длинную платформу. Легко поэкспериментировать с другими алгоритмами спауна и получить разный геймплей.
Теперь запустите игру (Project ▸ Build).
Неплохо, уже почти похоже на что-то играбельное…

Первое, что мы сейчас сделаем, — оживим героя. Пока бедняга застрял в бесконечном цикле бега и никак не реагирует на прыжки и вообще на происходящее. Spine-файл, который мы добавили из набора ресурсов, на самом деле уже содержит набор нужных анимаций.
update(): -- hero.script
local function play_animation(self, anim)
-- only play animations which are not already playing
if self.anim ~= anim then
-- tell the spine model to play the animation
local anim_props = { blend_duration = 0.15 }
spine.play_anim("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, anim_props)
-- remember which animation is playing
self.anim = anim
end
end
local function update_animation(self)
-- make sure the right animation is playing
if self.ground_contact then
play_animation(self, hash("run"))
else
play_animation(self, hash("jump"))
end
end
update() и добавьте вызов update_animation: ...
-- apply it to the player character
go.set_position(go.get_position() + self.velocity * dt)
update_animation(self)
...

Lua использует “lexical scope” для локальных переменных и чувствителен к порядку, в котором объявлены local functions. Функция update() вызывает локальные функции update_animation() и play_animation(), а значит runtime должен уже знать о них, чтобы можно было их вызвать. Поэтому эти функции нужно разместить до update(). Если поменять порядок, вы получите ошибку. Заметьте, это касается только local variables. Подробнее о правилах области видимости Lua и локальных функциях можно прочитать на http://www.lua.org/pil/6.2.html
Этого достаточно, чтобы добавить герою анимации прыжка и падения. Если запустить игру, вы заметите, что играть уже гораздо приятнее. Возможно, вы также увидите, что платформы могут столкнуть героя за пределы экрана. Это побочный эффект обработки столкновений, но решение простое — добавим немного насилия и сделаем края платформ опасными.
Сохраните файл.

Откройте hero.go, выделите Collision Object и добавьте имя “danger” в свойство Mask. Затем сохраните файл.

Откройте hero.script и измените функцию on_message(), чтобы герой реагировал на столкновение с “danger”-краем:
-- hero.script
function on_message(self, message_id, message, sender)
if message_id == hash("reset") then
self.velocity = vmath.vector3(0, 0, 0)
self.correction = vmath.vector3()
self.ground_contact = false
self.anim = nil
go.set(".", "euler.z", 0)
go.set_position(self.position)
msg.post("#collisionobject", "enable")
elseif message_id == hash("contact_point_response") then
-- check if we received a contact point message
if message.group == hash("danger") then
-- Die and restart
play_animation(self, hash("death"))
msg.post("#collisionobject", "disable")
-- <1>
go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 160, go.EASING_LINEAR, 0.7)
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
function()
msg.post("#", "reset")
end)
elseif message.group == hash("geometry") then
handle_geometry_contact(self, message.normal, message.distance)
end
end
end
Измените функцию init(), чтобы она отправляла сообщение “reset” для инициализации объекта, затем сохраните файл:
-- hero.script
function init(self)
-- this lets us handle input in this script
msg.post(".", "acquire_input_focus")
-- save position
self.position = go.get_position()
msg.post("#", "reset")
end
Если сейчас попробовать игру, быстро становится ясно, что механизм reset не работает как надо. Сброс героя в порядке, но можно легко переродиться так, что герой тут же снова упадёт на край платформы и умрёт. Нам нужно корректно сбрасывать весь уровень при смерти. Поскольку уровень у нас — это просто цепочка заспавненных платформ, достаточно отслеживать все созданные платформы и удалять их при reset:
Откройте controller.script и измените код так, чтобы он сохранял id всех заспавненных платформ:
-- controller.script
go.property("speed", 360)
local grid = 460
local platform_heights = { 100, 200, 350 }
function init(self)
msg.post("ground/controller#controller", "set_speed", { speed = self.speed })
self.gridw = 0
self.spawns = {} -- <1>
end
function update(self, dt)
self.gridw = self.gridw + self.speed * dt
if self.gridw >= grid then
self.gridw = 0
-- Maybe spawn a platform at random height
if math.random() > 0.2 then
local h = platform_heights[math.random(#platform_heights)]
local f = "#platform_factory"
if math.random() > 0.5 then
f = "#platform_long_factory"
end
local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
msg.post(p, "set_speed", { speed = self.speed })
table.insert(self.spawns, p) -- <1>
end
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("reset") then -- <2>
-- Tell the hero to reset.
msg.post("hero#hero", "reset")
-- Delete all platforms
for i,p in ipairs(self.spawns) do
go.delete(p)
end
self.spawns = {}
elseif message_id == hash("delete_spawn") then -- <3>
for i,p in ipairs(self.spawns) do
if p == message.id then
table.remove(self.spawns, i)
go.delete(p)
end
end
end
end
Откройте platform.script и измените его так, чтобы вместо простого удаления платформы, дошедшей до левого края, скрипт отправлял сообщение level controller с просьбой удалить платформу:
-- platform.script
...
if pos.x < -500 then
msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
end
...

-- hero.script
...
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
function()
msg.post("controller#controller", "reset")
end)
...

И теперь основной цикл “умер — перезапустился — снова умер” готов.
Дальше — то, ради чего стоит жить: монеты.
Идея в том, чтобы разместить в уровне монеты, которые игрок сможет собирать. Первый вопрос: как именно их добавить в уровень? Например, можно разработать алгоритм спауна, как-то связанный с алгоритмом спауна платформ. Но в итоге мы выбрали гораздо более простой вариант и позволили самим платформам создавать монеты:
Создайте новый script file coin.script (щёлкните правой кнопкой по level в Assets pane и выберите New ▸ Script File). Замените шаблонный код следующим:
-- coin.script
function init(self)
self.collected = false
end
function on_message(self, message_id, message, sender)
if self.collected == false and message_id == hash("collision_response") then
self.collected = true
msg.post("#sprite", "disable")
elseif message_id == hash("start_animation") then
pos = go.get_position()
go.animate(go.get_id(), "position.y", go.PLAYBACK_LOOP_PINGPONG, pos.y + 24, go.EASING_INOUTSINE, 0.75, message.delay)
end
end
Добавьте script file как Script component к объекту coin (щёлкните правой кнопкой по корню в Outline и выберите Add Component from File).

План такой: монеты будут спауниться из объектов платформ, поэтому нужно добавить factories для монет в platform.go и platform_long.go.

Теперь нужно изменить platform.script, чтобы он создавал и удалял монеты:
-- platform.script
function init(self)
self.speed = 540 -- Default speed in pixels/s
self.coins = {}
end
function final(self)
for i,p in ipairs(self.coins) do
go.delete(p)
end
end
function update(self, dt)
local pos = go.get_position()
if pos.x < -500 then
msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
end
pos.x = pos.x - self.speed * dt
go.set_position(pos)
end
function create_coins(self, params)
local spacing = 56
local pos = go.get_position()
local x = pos.x - params.coins * (spacing*0.5) - 24
for i = 1, params.coins do
local coin = factory.create("#coin_factory", vmath.vector3(x + i * spacing , pos.y + 64, 1))
msg.post(coin, "set_parent", { parent_id = go.get_id() }) -- <1>
msg.post(coin, "start_animation", { delay = i/10 }) -- <2>
table.insert(self.coins, coin)
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("set_speed") then
self.speed = message.speed
elseif message_id == hash("create_coins") then
create_coins(self, message)
end
end
Отношения родитель-потомок — это исключительно модификация scene graph. Дочерний объект будет трансформироваться (перемещаться, масштабироваться или вращаться) вместе с родителем. Если вам нужны дополнительные отношения “владения” между game object, их нужно отдельно отслеживать в коде.
Последний шаг в этом учебнике — добавить несколько строк в controller.script:
-- controller.script
...
local platform_heights = { 100, 200, 350 }
local coins = 3 -- <1>
...
-- controller.script
...
local coins = coins
if math.random() > 0.5 then
f = "#platform_long_factory"
coins = coins * 2 -- Twice the number of coins on long platforms
end
...
-- controller.script
...
msg.post(p, "set_speed", { speed = self.speed })
msg.post(p, "create_coins", { coins = coins })
table.insert(self.spawns, p)
...

И теперь у нас есть простая, но работающая игра. Если вы дошли до этого момента, можно продолжить самостоятельно и добавить, например, следующее:
Скачать завершённую версию проекта можно здесь
На этом вводный учебник заканчивается. Дальше погружайтесь в Defold самостоятельно. У нас подготовлено множество manuals и tutorials, которые помогут вам двигаться дальше, а если возникнут трудности — добро пожаловать на форум.
Приятной работы с Defold!