Tutorials
Tutorials

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 в Defold».

Якщо цей урок здається вам заскладним для початку, перегляньте нашу сторінку уроків, де зібрано уроки різної складності.

Якщо ви віддаєте перевагу відеоурокам, перегляньте відеоверсію на Youtube.

Ми використовуємо ігрові ресурси з двох інших уроків з невеликими змінами. Урок поділено на кілька кроків, кожен з яких суттєво наближає нас до готової гри.

У результаті ви отримаєте гру, у якій керуватимете героєм, що біжить ігровим світом, збирає монети й уникає перешкод. Герой біжить зі сталою швидкістю, а гравець керує лише його стрибками, натискаючи одну кнопку (або торкаючись екрана на мобільному пристрої). Рівень складається з нескінченного потоку платформ, на які можна стрибати, і монет, які можна збирати.

Якщо на будь-якому етапі цього уроку або під час створення власної гри ви натрапите на труднощі, не соромтеся звернутися по допомогу на форумі Defold. На форумі ви можете обговорювати Defold, звертатися по допомогу до команди Defold, дізнаватися, як інші розробники ігор розв’язували свої проблеми, і знаходити нове натхнення. Долучайтеся вже зараз.

Упродовж уроку докладні описи понять і способів виконання окремих дій позначено так само, як цей абзац. Якщо ці розділи здаються вам надто докладними, можете їх пропускати.

Тож почнімо. Сподіваємося, цей урок буде для вас цікавим і допоможе розпочати роботу з Defold.

Завантажте ресурси для цього уроку тут.

КРОК 1 — Встановлення й налаштування

Спочатку завантажте ці файли.

Якщо ви ще не завантажили й не встановили редактор Defold, саме час це зробити:

Завантаження

Перейдіть на сторінку завантаження Defold, де ви знайдете кнопки Download для macOS, Windows і Linux (Ubuntu):

Завантаження редактора

Встановлення

Встановлення в macOS
Завантажений файл — це образ DMG, що містить програму.
  1. Знайдіть файл «Defold-x86_64-macos.dmg» і двічі клацніть його, щоб відкрити образ.
  2. Перетягніть застосунок «Defold» на посилання на папку «Applications».

Щоб запустити редактор, відкрийте папку «Applications» і двічі клацніть файл «Defold».

Defold у macOS

Встановлення у Windows
Завантажений файл — це архів ZIP, який потрібно розпакувати:
  1. Знайдіть файл архіву «Defold-x86_64-win32.zip», натисніть і утримуйте папку (або клацніть її правою кнопкою миші), виберіть Extract All і дотримуйтеся вказівок, щоб розпакувати архів у папку «Defold».
  2. Перемістіть папку «Defold» у зручне для вас місце (наприклад, D:\Defold). Не переміщуйте Defold у C:\Program Files (x86)\ або C:\Program Files\, оскільки це завадить редактору оновлюватися.

Щоб запустити редактор, відкрийте папку «Defold» і двічі клацніть файл «Defold.exe».

Defold у Windows

Встановлення в Linux
Завантажений файл — це архів ZIP, який потрібно розпакувати:
  1. У терміналі знайдіть файл архіву «Defold-x86_64-linux.zip» і розпакуйте його в цільовий каталог «Defold».

    $ unzip Defold-x86_64-linux.zip -d Defold
    

Щоб запустити редактор, перейдіть до каталогу, у який ви розпакували застосунок, і запустіть виконуваний файл Defold або двічі клацніть його в графічному середовищі.

$ cd Defold
$ ./Defold

У меню Help > Create Desktop Entry є засіб створення ярлика застосунку.

Якщо виникнуть проблеми із запуском редактора, відкриттям проєкту або запуском гри Defold, зверніться до розділу поширених запитань про Linux.

Встановлення старої версії

Усі бета-версії та стабільні версії Defold також доступні на GitHub.

Коли редактор встановлено й запущено, час створити новий проєкт і підготувати його до роботи. Створіть новий проєкт із шаблону “Empty Project”.

У цьому уроці використовуються можливості Spine з розширення Spine. Додайте розширення до розділу залежностей у game.project.

Редактор

Під час першого запуску редактор відкривається порожнім, без відкритого проєкту, тож виберіть у меню Open Project і вкажіть щойно створений проєкт. Вам також буде запропоновано створити «гілку» для проєкту.

Тепер на панелі Assets pane ви побачите всі файли, що належать до проєкту. Якщо двічі клацнути файл “main/main.collection”, він відкриється в центральній області редактора:

Огляд редактора

Редактор складається з таких основних областей:

Assets pane
Тут показано всі файли вашого проєкту. Різні типи файлів мають різні піктограми. Двічі клацніть файл, щоб відкрити його в редакторі, призначеному для цього типу файлів. Спеціальна тека builtins, доступна лише для читання, є спільною для всіх проєктів. Вона містить корисні ресурси: стандартний скрипт рендерингу, шрифт, матеріали для рендерингу різних компонентів тощо.
Головна область редактора
Залежно від типу файлу, який ви редагуєте, тут буде показано відповідний редактор. Найчастіше використовується редактор сцени, який ви бачите тут. Кожен відкритий файл показано на окремій вкладці.
Changed Files
Містить файли, які локально додано, змінено, перейменовано або видалено порівняно з поточним комітом Git. Для одного зміненого або перейменованого текстового файлу за раз можна переглянути відмінності; тут також можна скасувати вибрані локальні зміни. Для синхронізації з віддаленим репозиторієм використовуйте зовнішній клієнт Git або командний рядок.
Outline
Ієрархічне подання вмісту файлу, який ви зараз редагуєте. Через цю панель можна додавати, видаляти, змінювати й вибирати об’єкти та компоненти.
Properties
Властивості, задані для поточного вибраного об’єкта або компонента.
Console
Під час запуску гри ця панель показує виведення ігрового рушія (повідомлення журналу, помилки, налагоджувальну інформацію тощо), а також ваші налагоджувальні повідомлення print() і pprint() зі скриптів. Якщо застосунок або гра не запускається, насамперед перевірте консоль. Поруч із консоллю є вкладки з інформацією про помилки та редактором кривих, який використовується під час створення ефектів частинок.

Запуск гри

Шаблон проєкту “Empty” справді повністю порожній. Утім, виберіть Project ▸ Build, щоб зібрати проєкт і запустити гру.

Збирання

Чорний екран, мабуть, не дуже захоплює, але це запущений ігровий застосунок Defold, який можна легко перетворити на щось цікавіше. Тож зробімо це.

Редактор Defold працює з файлами. Двічі клацнувши файл на панелі Assets pane, ви відкриваєте його у відповідному редакторі. Після цього можна працювати з вмістом файлу.

Завершивши редагування файлу, збережіть його. Виберіть File ▸ Save у головному меню. Редактор нагадує про незбережені зміни, додаючи зірочку ‘*’ до імені файлу на його вкладці.

Файл із незбереженими змінами

Налаштування проєкту

Перш ніж почати, задаймо кілька налаштувань проєкту. Відкрийте ресурс game.project на панелі Assets Pane і прокрутіть до розділу Display. Установіть width і height проєкту в 1280 і 720 відповідно.

Також потрібно додати до проєкту розширення Spine, щоб анімувати героя. Додайте версію розширення Spine, сумісну зі встановленою у вас версією редактора Defold. Доступні версії Spine можна переглянути тут:

https://github.com/defold/extension-spine/releases

Клацніть правою кнопкою миші посилання на zip-файл випуску, який хочете використати:

Клацніть правою кнопкою миші й скопіюйте посилання на випуск

Додайте посилання на випуск до списку залежностей game.project. Після додавання розширення Spine потрібно також перезапустити редактор, щоб активувати інтеграцію з редактором, яка входить до розширення Spine.

КРОК 2 — Створення землі

Зробімо перші невеликі кроки й створімо майданчик для нашого персонажа, а точніше — ділянку землі, що прокручується. Це займе кілька кроків.

  1. Імпортуйте зображення до проєкту: перетягніть файли “ground01.png” і “ground02.png” (із підтеки “level-images” у пакеті ресурсів) у відповідне місце в проєкті, наприклад у теку “images” усередині теки “main”.
  2. Створіть новий файл Atlas для текстур землі (клацніть правою кнопкою миші відповідну теку, наприклад main, на панелі Assets pane і виберіть New ▸ Atlas File). Назвіть файл атласу level.atlas.

Atlas — це файл, який об’єднує набір окремих зображень в одне більше зображення. Це робиться для економії місця й підвищення продуктивності. Докладніше про атласи та інші можливості 2D-графіки читайте в документації з 2D-графіки.

  1. Додайте зображення землі до нового атласу: клацніть правою кнопкою миші кореневий вузол атласу на панелі Outline і виберіть Add Images. Виберіть імпортовані зображення та натисніть OK. Кожне зображення в атласі тепер доступне як однокадрова анімація (нерухоме зображення) для використання у спрайтах, ефектах частинок та інших візуальних елементах. Збережіть файл.

Створення нового атласу

Додавання зображень до атласу

Чому це не працює!? Поширена проблема тих, хто починає працювати з Defold, — забувати зберігати файли! Після додавання зображень до атласу потрібно зберегти файл, щоб ці зображення стали доступними.

  1. Створіть файл колекції (collection) ground.collection для землі й додайте до нього 7 ігрових об’єктів (game objects): клацніть правою кнопкою миші кореневий вузол колекції на панелі Outline і виберіть Add Game Object. Назвіть об’єкти “ground0”, “ground1”, “ground2” тощо, змінивши властивість Id на панелі Properties. Зверніть увагу, що Defold автоматично призначає новим ігровим об’єктам унікальні ідентифікатори.

  2. До кожного об’єкта додайте компонент (component) спрайта: клацніть правою кнопкою миші ігровий об’єкт на панелі Outline і виберіть Add Component, потім виберіть Sprite та натисніть OK. Установіть для властивості Image компонента спрайта щойно створений атлас, а як стандартну анімацію спрайта виберіть одне з двох зображень землі. Установіть позицію X компонента спрайта (а не ігрового об’єкта) в 190, а позицію Y — у 40. Оскільки ширина зображення становить 380 пікселів і ми зміщуємо його вбік на половину цієї відстані, опорна точка ігрового об’єкта опиниться на лівому краї зображення спрайта.

Створення колекції землі

  1. Використана графіка дещо завелика, тож зменште кожен ігровий об’єкт до 60% (масштаб 0.6 за X та Y дає ділянки землі завширшки 228 пікселів).

Масштабування землі

  1. Розташуйте всі ігрові об’єкти в один ряд. Установіть позиції X ігрових об’єктів (а не компонентів спрайтів) у 0, 228, 456, 684, 912, 1140 і 1368 (кратні ширині 228 пікселів).

Мабуть, найпростіше створити один повністю налаштований ігровий об’єкт із потрібним масштабом і компонентом спрайта, а потім скопіювати його. Виберіть його на панелі Outline, потім виберіть Edit ▸ Copy, а далі — Edit ▸ Paste.

Якщо вам потрібні більші або менші плитки, достатньо змінити масштаб. Однак у такому разі також потрібно змінити позиції X усіх ігрових об’єктів землі на значення, кратні новій ширині.

  1. Збережіть файл, а потім додайте ground.collection до файлу main.collection: спочатку двічі клацніть файл main.collection, потім клацніть правою кнопкою миші кореневий об’єкт на панелі Outline і виберіть Add Collection From File. У діалоговому вікні виберіть ground.collection і натисніть OK. Обов’язково розмістіть ground.collection у позиції 0, 0, 0, інакше вона буде візуально зміщена. Збережіть файл.

  2. Запустіть гру (Project ▸ Build), щоб переконатися, що все на своїх місцях.

Нерухома земля

Можливо, ви вже трохи заплуталися й замислилися, чим насправді є всі ці елементи, які ми створюємо. Тож зупинімося на мить і розгляньмо основні будівельні блоки будь-якого проєкту Defold:

Ігрові об’єкти
Це сутності, що існують у запущеній грі. Кожен ігровий об’єкт має позицію в 3D-просторі, поворот і масштаб. Він не обов’язково має бути видимим. Ігровий об’єкт містить довільну кількість компонентів, які додають можливості: графіку (спрайти, карти плиток, моделі, моделі Spine й ефекти частинок), звуки, фізику, фабрики (для створення об’єктів) тощо. Для надання ігровому об’єкту поведінки також можна додавати компоненти-скрипти Lua. Кожен ігровий об’єкт, що існує у вашій грі, має id, потрібний для взаємодії з ним через передавання повідомлень.
Колекції
Колекції самі по собі не існують у запущеній грі, але дають змогу статично іменувати ігрові об’єкти й водночас мати кілька екземплярів (instances) одного ігрового об’єкта. На практиці колекції слугують контейнерами для ігрових об’єктів та інших колекцій. Їх можна використовувати як прототипи (в інших рушіях також відомі як «префаби» або «шаблони») складних ієрархій ігрових об’єктів і колекцій. Під час запуску рушій завантажує головну колекцію й оживляє все, що ви до неї додали. Стандартно це файл main.collection у теці main вашого проєкту, але це можна змінити в налаштуваннях проєкту.

Наразі цих пояснень, імовірно, достатньо. Значно докладніше ці поняття розглянуто в посібнику «Будівельні блоки». Радимо повернутися до нього згодом, щоб глибше зрозуміти, як усе працює в Defold.

КРОК 3 — Рух землі

Тепер, коли всі ділянки землі розташовано на своїх місцях, змусити їх рухатися досить просто. Ідея така: рухати ділянки справа наліво, а коли ділянка досягне крайньої лівої позиції за межами екрана, переносити її в крайню праву позицію. Для руху всіх цих ігрових об’єктів потрібен скрипт Lua, тож створімо його:

  1. Клацніть правою кнопкою миші теку main на панелі Assets pane і виберіть New ▸ Script File. Назвіть новий файл ground.script.
  2. Двічі клацніть новий файл, щоб відкрити редактор скриптів Lua.
  3. Видаліть стандартний вміст файлу, скопіюйте до нього наведений нижче код 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
  1. Зберігаємо ідентифікатори ігрових об’єктів землі в таблиці Lua, щоб можна було перебирати їх.
  2. Функція init() викликається, коли ігровий об’єкт з’являється у грі. Ми ініціалізуємо локальну для об’єкта змінну-член, що містить швидкість руху землі.
  3. update() викликається один раз за кадр, зазвичай 60 разів на секунду. dt містить кількість секунд, що минули від попереднього виклику.
  4. Перебираємо всі ігрові об’єкти землі.
  5. Зберігаємо поточну позицію в локальній змінній, а якщо поточний об’єкт розташований біля крайнього лівого краю, переносимо його до крайнього правого краю.
  6. Зменшуємо поточну позицію X із заданою швидкістю. Множимо на dt, щоб отримати швидкість у пікселях за секунду, незалежну від частоти кадрів.
  7. Оновлюємо позицію об’єкта відповідно до нової швидкості.

Defold — це швидке ядро рушія, що керує вашими даними й ігровими об’єктами. Будь-яку логіку або поведінку, потрібну для гри, ви створюєте мовою Lua. Lua — швидка й легка мова програмування, яка чудово підходить для написання ігрової логіки. Для її вивчення є чудові матеріали, наприклад книга Programming in Lua та офіційний довідник Lua.

Defold доповнює Lua набором API та системою передавання повідомлень, за допомогою якої можна програмувати взаємодію між ігровими об’єктами. Докладніше про її роботу читайте в посібнику з передавання повідомлень.

Ви можете показувати й приховувати панелі Assets Pane, Console та Outline редактора клавішами F6, F7 і F8 відповідно

Тепер, коли файл скрипту готовий, потрібно додати посилання на нього до компонента ігрового об’єкта. Так скрипт виконуватиметься в межах життєвого циклу цього ігрового об’єкта. Для цього створимо новий ігровий об’єкт у ground.collection і додамо до нього компонент Script, що посилається на щойно створений файл скрипту Lua:

  1. Клацніть правою кнопкою миші кореневий вузол колекції й виберіть Add Game Object. Установіть id об’єкта в “controller”.
  2. Клацніть правою кнопкою миші об’єкт “controller” і виберіть Add Component from file, а потім файл ground.script.

Контролер землі

Тепер під час запуску гри ігровий об’єкт “controller” виконуватиме скрипт у своєму компоненті Script, і земля плавно прокручуватиметься екраном.

КРОК 4 — Створення героя

Герой буде ігровим об’єктом, що складається з таких компонентів:

Spine Model
Дає нам маленького героя, схожого на паперову ляльку, частини тіла якого можна плавно анімувати з невеликими витратами ресурсів.
Collision Object
Виявлятиме колізії між героєм та об’єктами рівня, якими можна бігати, які становлять небезпеку або які можна підбирати.
Script
Отримує введення користувача й реагує на нього, змушує героя стрибати, відтворює анімації та обробляє колізії.

Почніть з імпорту зображень частин тіла, а потім додайте їх до нового атласу, який назвемо hero.atlas:

  1. Створіть нову теку: клацніть правою кнопкою миші на панелі Assets pane і виберіть New ▸ Folder. Перед цим переконайтеся, що жодну теку не вибрано, інакше нову теку буде створено всередині вибраної. Назвіть теку “hero”.
  2. Створіть новий файл атласу: клацніть правою кнопкою миші теку hero і виберіть New ▸ Atlas File. Назвіть файл hero.atlas.
  3. Створіть нову підтеку images у теці hero. Клацніть правою кнопкою миші теку hero і виберіть New ▸ Folder.
  4. Перетягніть зображення частин тіла з теки hero-images у пакеті ресурсів до щойно створеної теки images на панелі Assets pane.
  5. Відкрийте hero.atlas, клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Images. Виберіть усі зображення частин тіла й натисніть OK.
  6. Збережіть файл атласу.

Атлас героя

Також потрібно імпортувати дані анімації Spine й налаштувати для них Spine Scene:

  1. Перетягніть файл hero.spinejson (він входить до пакета ресурсів) у теку hero на панелі Assets pane.
  2. Створіть файл Spine Scene. Клацніть правою кнопкою миші теку hero і виберіть New ▸ Spine Scene File. Назвіть файл hero.spinescene.
  3. Двічі клацніть новий файл, щоб відкрити й відредагувати Spine Scene.
  4. Установіть для властивості spine_json імпортований файл JSON hero.spinejson. Клацніть властивість, а потім кнопку вибору файлу , щоб відкрити браузер ресурсів.
  5. Установіть для властивості atlas посилання на файл hero.atlas.
  6. Збережіть файл.

Сцена Spine героя

Файл hero.spinejson експортовано у форматі Spine JSON. Для створення таких файлів потрібна програма анімації Spine. Якщо ви хочете використовувати іншу програму анімації, можна експортувати анімації як аркуші спрайтів і використовувати їх як покадрові анімації з ресурсів Tile Source або Atlas. Докладніше читайте в посібнику з анімації.

Створення ігрового об’єкта

Тепер можемо почати створювати ігровий об’єкт героя:

  1. Створіть новий файл hero.go (клацніть правою кнопкою миші теку hero і виберіть New ▸ Game Object File).
  2. Відкрийте файл ігрового об’єкта.
  3. Додайте до нього компонент Spine Model. (Клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component, а потім “Spine Model”.)
  4. Установіть для властивості Spine Scene компонента щойно створений файл hero.spinescene і виберіть “run_right” як стандартну анімацію (належно налаштуємо анімацію пізніше)
  5. Збережіть файл.

Властивості моделі Spine

Тепер час додати фізику, щоб запрацювали колізії:

  1. Додайте компонент Collision Object до ігрового об’єкта героя. (Клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component, а потім “Collision Object”)
  2. Клацніть правою кнопкою миші новий компонент і виберіть Add Shape. Додайте дві форми, що покриватимуть тіло персонажа. Підійдуть сфера й прямокутник.
  3. Клацніть форми й інструментом Move Tool (Scene ▸ Move Tool) перемістіть їх у відповідні позиції.
  4. Виберіть компонент Collision Object і встановіть його властивість Type у “Kinematic”.

Тип колізії “Kinematic” означає, що ми хочемо реєструвати колізії, але фізичний рушій не розв’язуватиме їх автоматично й не симулюватиме об’єкти. Фізичний рушій підтримує кілька різних типів об’єктів колізій. Докладніше про них читайте в документації з фізики.

Важливо вказати, із чим має взаємодіяти об’єкт колізій:

  1. Установіть для властивості Group нову групу колізій із назвою “hero”.
  2. Установіть для властивості Mask іншу групу, “geometry”, з якою цей об’єкт має реєструвати колізії. Зверніть увагу, що групи “geometry” ще не існує, але невдовзі ми додамо об’єкти колізій, що належать до неї.

Насамкінець створіть новий файл hero.script і додайте його до ігрового об’єкта.

  1. Клацніть правою кнопкою миші теку hero на панелі Assets pane і виберіть New ▸ Script File. Назвіть новий файл hero.script.
  2. Відкрийте новий файл, скопіюйте й вставте до нього наведений нижче код, а потім збережіть файл. (Код досить простий, окрім алгоритму, який відділяє форму колізії героя від того, із чим вона зіткнулася. Це робить функція handle_geometry_contact().)

Ігровий об’єкт героя

Ми обробляємо колізії самостійно, тому що якби натомість установили для об’єкта колізій персонажа динамічний тип, рушій виконував би ньютонівську симуляцію тіл, що взаємодіють. Для такої гри ця симуляція далеко не оптимальна, тож замість того, щоб боротися з фізичним рушієм за допомогою різних сил, ми беремо керування повністю на себе.

Для цього й належної обробки колізій потрібні певні знання векторної математики. Докладне пояснення розв’язання кінематичних колізій наведено в документації з фізики.

-- 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
  1. Додайте скрипт як компонент Script до об’єкта героя: клацніть правою кнопкою миші кореневий вузол hero.go на панелі Outline і виберіть Add Component from File, а потім файл hero.script.

За бажанням тепер можна тимчасово додати героя до головної колекції й запустити гру, щоб побачити, як він провалюється крізь світ.

Останнє, чого бракує герою для роботи, — введення. Наведений вище скрипт уже містить функцію on_input(), яка реагує на дії “jump” і “touch” (для сенсорних екранів). Додаймо прив’язки введення для цих дій.

  1. Відкрийте “input/game.input_bindings”
  2. Додайте тригер клавіші “KEY_SPACE” і назвіть дію “jump”
  3. Додайте тригер дотику “TOUCH_MULTI” і назвіть дію “touch”. (Назви дій довільні, але мають збігатися з назвами у вашому скрипті. Зверніть увагу, що не можна використовувати однакову назву дії для кількох тригерів)
  4. Збережіть файл.

Прив’язки введення

КРОК 5 — Рефакторинг рівня

Тепер, коли героя налаштовано, зокрема й колізії, потрібно додати колізії також до землі, щоб герою було із чим стикатися (або чим бігати). Зробимо це за мить, але спершу проведемо невеликий рефакторинг: помістимо все, що стосується рівня, в окрему колекцію й трохи впорядкуємо структуру файлів:

  1. Створіть новий файл level.collection: клацніть правою кнопкою миші main на панелі Assets pane і виберіть New ▸ Collection File.
  2. Відкрийте новий файл, клацніть правою кнопкою миші кореневий вузол на панелі Outline, виберіть Add Collection from File, а потім ground.collection.
  3. У level.collection клацніть правою кнопкою миші кореневий вузол на панелі Outline, виберіть Add Game Object File, а потім hero.go.
  4. Тепер створіть нову теку level у корені проєкту: клацніть правою кнопкою миші порожнє місце під game.project і виберіть New ▸ Folder. Потім перенесіть до неї вже створені ресурси рівня: файли level.collection, level.atlas, теку “images” із зображеннями для атласу рівня, а також файли ground.collection і ground.script.
  5. Відкрийте main.collection, видаліть ground.collection і натомість додайте level.collection (клацніть правою кнопкою миші й виберіть Add Collection from File), яка тепер містить ground.collection. Обов’язково розташуйте колекцію в позиції 0, 0, 0.

Як ви, можливо, вже помітили, ієрархія файлів на панелі Assets pane не пов’язана зі структурою вмісту, яку ви будуєте у своїх колекціях. На окремі файли посилаються файли колекцій та ігрових об’єктів, але їхнє розташування цілком довільне.

Якщо ви хочете перемістити файл в інше місце, Defold допоможе, автоматично оновивши посилання на нього (рефакторинг). Під час створення складного програмного забезпечення, як-от гри, надзвичайно корисно мати змогу змінювати структуру проєкту в міру його зростання й розвитку. Defold сприяє цьому й робить процес зручним, тож не бійтеся переміщувати файли!

Також потрібно додати до колекції рівня ігровий об’єкт контролера з компонентом-скриптом:

  1. Створіть новий файл скрипту. Клацніть правою кнопкою миші теку level на панелі Assets pane і виберіть New ▸ Script File. Назвіть файл controller.script.
  2. Відкрийте файл скрипту, скопіюйте до нього наведений нижче код і збережіть файл:

     -- controller.script
     go.property("speed", 360) -- <1>
    
     function init(self)
         msg.post("ground/controller#ground", "set_speed", { speed = self.speed })
     end
    
    1. Це властивість скрипту. Ми задаємо їй стандартне значення, але кожен розміщений екземпляр скрипту може перевизначити його безпосередньо на панелі властивостей у редакторі.
  3. Відкрийте файл level.collection.
  4. Клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Game Object.
  5. Установіть Id у “controller”.
  6. Клацніть правою кнопкою миші ігровий об’єкт “controller” на панелі Outline, виберіть Add Component from File, а потім файл controller.script у теці level.
  7. Збережіть файл.

Властивість скрипту

Ігровий об’єкт “controller” не існує в окремому файлі, а створений на місці (in-place) у колекції рівня. Це означає, що екземпляр ігрового об’єкта створюється з даних, заданих безпосередньо в колекції. Це підходить для об’єктів з одним конкретним призначенням, як цей. Якщо вам потрібні кілька екземплярів певного ігрового об’єкта й можливість змінювати прототип або шаблон, з якого створюється кожен екземпляр, просто створіть файл ігрового об’єкта й додайте ігровий об’єкт із файлу до колекції. Так буде створено ігровий об’єкт із посиланням на файл як на прототип або шаблон.

Призначення цього ігрового об’єкта “controller” — керувати всім, що стосується запущеного рівня. Невдовзі цей скрипт відповідатиме за створення платформ і монет, з якими взаємодіятиме герой, але поки що він лише задаватиме швидкість рівня.

У функції init() скрипт контролера рівня надсилає повідомлення компоненту-скрипту об’єкта контролера землі, адресуючи його за ідентифікатором:

msg.post("ground/controller#controller", "set_speed", { speed = self.speed })

Ідентифікатор ігрового об’єкта контролера — "ground/controller", оскільки він міститься в колекції “ground”. Далі додаємо ідентифікатор компонента "controller" після символу решітки "#", який відділяє ідентифікатор об’єкта від ідентифікатора компонента. Зверніть увагу, що у скрипті землі ще немає коду для реагування на повідомлення set_speed, тож потрібно додати до ground.script функцію on_message() і відповідну логіку.

  1. Відкрийте ground.script.
  2. Додайте наведений нижче код і збережіть файл:
-- 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
  1. Під час надсилання всі повідомлення хешуються всередині рушія, і порівнювати їх потрібно з хешованим значенням.
  2. Дані повідомлення — це таблиця Lua з даними, що надсилаються разом із повідомленням.

Додавання коду землі

КРОК 6 — Фізика землі й платформи

На цьому етапі потрібно додати фізичні колізії для землі:

  1. Відкрийте файл ground.collection.
  2. Додайте новий компонент Collision Object до відповідного ігрового об’єкта. Оскільки скрипт землі не реагує на колізії (уся ця логіка міститься у скрипті героя), компонент можна помістити в будь-який нерухомий ігровий об’єкт (об’єкти плиток землі рухаються, тож їх не використовуйте). Добре підійде ігровий об’єкт “controller”, але за бажанням можна створити для цього окремий об’єкт. Клацніть правою кнопкою миші ігровий об’єкт, виберіть Add Component, а потім Collision Object.
  3. Додайте прямокутну форму: клацніть правою кнопкою миші компонент Collision Object, виберіть Add Shape, а потім Box.
  4. За допомогою Move Tool і Scale Tool (Scene ▸ Move Tool та Scene ▸ Scale Tool) розмістіть і масштабуйте прямокутник так, щоб він покривав усі плитки землі.
  5. Установіть властивість Type об’єкта колізій у “Static”, оскільки фізичний об’єкт землі не рухатиметься.
  6. Установіть властивість Group об’єкта колізій у “geometry”, а Mask — у “hero”. Тепер цей об’єкт колізій та об’єкт колізій героя реєструватимуть колізії між собою.
  7. Збережіть файл.

Колізії землі

Тепер можете спробувати запустити гру (Project ▸ Build). Герой має бігти землею й стрибати після натискання клавіші Space. Якщо ви запускаєте гру на мобільному пристрої, стрибати можна, торкаючись екрана.

Щоб життя в нашому ігровому світі стало трохи цікавішим, додаймо платформи, на які можна стрибати.

  1. Перетягніть файл зображення rock_planks.png із пакета ресурсів до підтеки level/images.
  2. Відкрийте level.atlas і додайте нове зображення до атласу: клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Images.
  3. Збережіть файл.
  4. Створіть новий файл Game Object із назвою platform.go у теці level. (Клацніть правою кнопкою миші level на панелі Assets pane і виберіть New ▸ Game Object File.)
  5. Додайте компонент Sprite до ігрового об’єкта: клацніть правою кнопкою миші кореневий вузол на панелі Outline, виберіть Add Component, а потім Sprite.
  6. Установіть для властивості Image посилання на файл level.atlas, а Default Animation — у “rock_planks”. Для зручності зберігайте об’єкти рівня в підтеці “level/objects”.
  7. Додайте компонент Collision Object до ігрового об’єкта платформи: клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component.
  8. Обов’язково встановіть Type компонента в “Kinematic”, а Group і Mask — у “geometry” і “hero” відповідно.
  9. Додайте Box Shape до компонента Collision Object. (Клацніть правою кнопкою миші компонент на панелі Outline, виберіть Add Shape, а потім Box).
  10. За допомогою Move Tool і Scale Tool (Scene ▸ Move Tool та Scene ▸ Scale Tool) розмістіть і масштабуйте форму в компоненті Collision Object так, щоб вона покривала платформу.
  11. Створіть файл 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
    
    1. Просто видаляємо платформу, коли вона переміститься за правий край екрана
  12. Відкрийте platform.go і додайте новий скрипт як компонент: клацніть правою кнопкою миші кореневий вузол на панелі Outline, виберіть Add Component From File, а потім platform.script.
  13. Скопіюйте platform.go в новий файл: клацніть правою кнопкою миші файл на панелі Assets pane і виберіть Copy, потім ще раз клацніть правою кнопкою миші й виберіть Paste. Назвіть новий файл platform_long.go.
  14. Відкрийте platform_long.go і додайте другий компонент Sprite: клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component. Або просто скопіюйте наявний Sprite.
  15. За допомогою Move Tool (Scene ▸ Move Tool) розташуйте компоненти Sprite поряд.
  16. За допомогою Move Tool і Scale Tool розмістіть і масштабуйте форму в компоненті Collision Object так, щоб вона покривала обидві платформи.

Платформа

Зверніть увагу, що і platform.go, і platform_long.go мають компоненти Script, які посилаються на той самий файл скрипту. Це зручно, адже будь-які зміни в цьому файлі впливатимуть на поведінку як звичайних, так і довгих платформ.

Створення платформ під час гри

Наша гра має бути простим нескінченним раннером. Це означає, що ігрові об’єкти платформ не можна заздалегідь розмістити в колекції в редакторі. Натомість їх потрібно створювати динамічно:

  1. Відкрийте level.collection.
  2. Додайте два компоненти Factory до ігрового об’єкта “controller”: клацніть його правою кнопкою миші, виберіть Add Component, а потім Factory
  3. Установіть властивості Id компонентів у “platform_factory” і “platform_long_factory”.
  4. Установіть для властивості Prototype компонента “platform_factory” файл /level/objects/platform.go.
  5. Установіть для властивості Prototype компонента “platform_long_factory” файл /level/objects/platform_long.go.
  6. Збережіть файл.
  7. Відкрийте файл controller.script, який керує рівнем.
  8. Змініть скрипт так, щоб він містив наведений нижче код, а потім збережіть файл:
-- 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, {}, vmath.vector3(0.6, 0.6, 1))
            msg.post(p, "set_speed", { speed = self.speed })
        end
    end
end
  1. Заздалегідь визначені значення позиції Y для створення платформ.
  2. Функція update() викликається один раз за кадр. Ми використовуємо її, щоб вирішувати, чи створювати звичайну або довгу платформу з певними інтервалами (щоб уникати перекриття) і на певних висотах. Можна легко експериментувати з різними алгоритмами створення платформ, змінюючи ігровий процес.

Тепер запустіть гру (Project ▸ Build).

Ого, це вже починає перетворюватися на щось, у що (майже) можна грати…

Запуск гри

КРОК 7 — Анімація й смерть

Спочатку ми оживимо героя. Наразі бідолаха застряг у циклі бігу й належно не реагує ні на стрибки, ні на інші події. Файл Spine, який ми додали з пакета ресурсів, насправді містить набір потрібних для цього анімацій.

  1. Відкрийте файл hero.script і додайте наведені нижче функції перед наявною функцією 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
  1. Знайдіть функцію update() і додайте виклик update_animation:
    ...
    -- apply it to the player character
    go.set_position(go.get_position() + self.velocity * dt)

    update_animation(self)
    ...

Вставлення коду героя

У Lua локальні змінні мають «лексичну область видимості», тому порядок розташування функцій local має значення. Функція update() викликає локальні функції update_animation() і play_animation(), а отже, середовище виконання має вже знати про них, щоб їх викликати. Саме тому ці функції потрібно розмістити перед update(). Якщо змінити порядок функцій, виникне помилка. Зверніть увагу, що це стосується лише змінних local. Докладніше про правила областей видимості й локальні функції Lua читайте на http://www.lua.org/pil/6.2.html

Це все, що потрібно, аби додати герою анімації стрибка й падіння. Якщо запустити гру, ви помітите, що грати стало значно приємніше. Можливо, ви також помітите, що платформи, на жаль, можуть виштовхнути героя за межі екрана. Це побічний ефект обробки колізій, але виправити його просто — додамо трохи жорстокості й зробимо краї платформ небезпечними!

  1. Перетягніть spikes.png із пакета ресурсів до теки “level/images” на панелі Assets pane.
  2. Відкрийте level.atlas і додайте зображення: клацніть правою кнопкою миші й виберіть Add Images.
  3. Відкрийте platform.go і додайте кілька компонентів Sprite. Установіть Image у level.atlas, а Default Animation — у “spikes”.
  4. За допомогою Move Tool і Rotate Tool розташуйте шипи вздовж країв платформи.
  5. Щоб шипи відображалися позаду платформи, установіть позицію Z спрайтів шипів у -0.1.
  6. Додайте новий компонент Collision Object до платформ: клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component. Установіть властивість Group у “danger”. Також установіть Mask у “hero”.
  7. Додайте прямокутну форму до Collision Object (клацніть правою кнопкою миші й виберіть Add Shape). За допомогою Move Tool (Scene ▸ Move Tool) і Scale Tool розташуйте її так, щоб герой стикався з об’єктом “danger”, коли торкається платформи збоку або знизу.
  8. Збережіть файл.

    Шипи платформи

  9. Відкрийте hero.go, виберіть Collision Object і додайте назву “danger” до властивості Mask. Потім збережіть файл.

    Колізії героя

  10. Відкрийте 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
    
    1. Додаємо герою поворот і рух падіння під час смерті. Тут є багато можливостей для вдосконалення!
  11. Змініть функцію 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
    

КРОК 8 — Скидання рівня

Якщо зараз спробувати пограти, швидко стане зрозуміло, що механізм скидання не працює як слід. Герой скидається правильно, але легко може опинитися в ситуації, коли одразу впаде на край платформи й знову загине. Ми хочемо належно скидати весь рівень після смерті. Оскільки рівень — це лише послідовність створених платформ, достатньо відстежувати всі створені платформи, а під час скидання видаляти їх:

  1. Відкрийте файл controller.script і змініть код так, щоб зберігати ідентифікатори всіх створених платформ:

     -- 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, {}, vmath.vector3(0.6, 0.6, 1))
                 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
    
    1. Використовуємо таблицю для зберігання всіх створених платформ
    2. Повідомлення “reset” видаляє всі платформи, збережені в таблиці
    3. Повідомлення “delete_spawn” видаляє певну платформу й вилучає її з таблиці
  2. Збережіть файл.
  3. Відкрийте platform.script і змініть його так, щоб замість безпосереднього видалення платформи, яка досягла крайнього лівого краю, він надсилав контролеру рівня повідомлення із запитом на її видалення:

     -- platform.script
     ...
     if pos.x < -500 then
         msg.post("/level/controller#controller", "delete_spawn", { id = go.get_id() })
     end
     ...
    

    Вставлення коду платформи

  4. Збережіть файл.
  5. Відкрийте hero.script. Залишилося повідомити рівню, що потрібно виконати скидання. Ми перенесли повідомлення із запитом на скидання героя до скрипту контролера рівня. Таке централізоване керування скиданням доцільне, адже дає змогу, наприклад, простіше додати довшу сцену смерті з визначеною тривалістю:
-- 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)
...

Вставлення коду героя

Тепер основний цикл перезапуску й смерті готовий!

Далі — те, заради чого варто жити: монети!

КРОК 9 — Збирання монет

Ідея полягає в тому, щоб додати на рівень монети, які гравець збиратиме. Перше питання — як саме розміщувати їх на рівні. Наприклад, можна розробити схему створення монет, яка певним чином узгоджується з алгоритмом створення платформ. Утім, ми зрештою обрали значно простіший підхід: нехай самі платформи створюють монети:

  1. Перетягніть зображення coin.png із пакета ресурсів до “level/images” на панелі Assets pane.
  2. Відкрийте level.atlas і додайте зображення: клацніть правою кнопкою миші й виберіть Add Images.
  3. Створіть файл Game Object із назвою coin.go у теці level: клацніть правою кнопкою миші level на панелі Assets pane і виберіть New ▸ Game Object File.
  4. Відкрийте coin.go і додайте компонент Sprite: клацніть правою кнопкою миші на панелі Outline і виберіть Add Component. Установіть Image у level.atlas, а Default Animation — у “coin”.
  5. Додайте Collision Object: клацніть правою кнопкою миші на панелі Outline і виберіть Add Component та додайте форму Sphere, яка покриває зображення: клацніть правою кнопкою миші компонент і виберіть Add Shape.
  6. За допомогою Move Tool (Scene ▸ Move Tool) і Scale Tool розмістіть і масштабуйте сферу так, щоб вона покривала зображення монети.
  7. Установіть Type об’єкта колізій у “Kinematic”, його Group — у “pickup”, а Mask — у “hero”.
  8. Відкрийте hero.go і додайте “pickup” до властивості Mask компонента Collision Object, а потім збережіть файл.
  9. Створіть новий файл скрипту 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
    
  10. Додайте файл скрипту як компонент Script до об’єкта монети: клацніть правою кнопкою миші кореневий вузол на панелі Outline і виберіть Add Component from File.

    Ігровий об’єкт монети

Ми плануємо створювати монети з об’єктів платформ, тож додайте фабрики (factories) монет до platform.go і platform_long.go.

  1. Відкрийте platform.go і додайте компонент Factory: клацніть правою кнопкою миші на панелі Outline і виберіть Add Component.
  2. Установіть Id компонента Factory у “coin_factory”, а його Prototype — у файл coin.go.
  3. Тепер відкрийте platform_long.go і створіть такий самий компонент Factory.
  4. Збережіть обидва файли.

Фабрика монет

Тепер потрібно змінити 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
  1. Якщо призначити платформу батьківським об’єктом створеної монети, монета рухатиметься разом із платформою.
  2. Анімація змушує монети рухатися вгору й униз відносно платформи, яка тепер є їхнім батьківським об’єктом.

Батьківсько-дочірні зв’язки змінюють лише граф сцени. Дочірній об’єкт трансформується (переміщується, масштабується або повертається) разом зі своїм батьківським об’єктом. Якщо вам потрібні додаткові зв’язки «належності» між ігровими об’єктами, їх потрібно окремо відстежувати в коді.

Останній крок у цьому уроці — додати кілька рядків до controller.script:

-- controller.script
...
local platform_heights = { 100, 200, 350 }
local coins = 3 -- <1>
...
  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)
...

Вставлення коду контролера

Тепер у нас є проста, але робоча гра! Якщо ви дійшли до цього місця, можливо, захочете продовжити самостійно й додати:

  1. Підрахунок очок і лічильники життів
  2. Ефекти частинок для збирання предметів і смерті
  3. Гарні фонові зображення

Завантажте готову версію проєкту тут

На цьому вступний урок завершено. Тепер продовжуйте знайомство з Defold. Ми підготували багато посібників і уроків, що допоможуть вам, а якщо натрапите на труднощі — запрошуємо на форум.

Приємної роботи з Defold!