Код, який контролює гру під час виконання, повинен мати змогу досягти будь-який обʼєкт або компонент щоб рухати, масштабувати, анімувати, видаляти та маніпулювати все що гравець бачить та чує. Механізм адресації Defold робить це можливим.
Defold використовує адреси (або URL, але про це потім) щоб посилатися на обʼєкти та компоненти. Ці адреси складаються з ідентифікаторів. Далі наведені приклади того як Defold використовує адреси. В цьому посібнику ми детально розглянемо як вони працюють:
local id = factory.create("#enemy_factory")
label.set_text("my_gameobject#my_label", "Hello World!")
local pos = go.get_position("my_gameobject")
go.set_position(pos, "/level/stuff/other_gameobject")
msg.post("#", "hello_there")
local id = go.get_id(".")
Почнемо з простого прикладу. Уявіть що у вас є ігровий обʼєкт з одним компонентом-спрайтом. Також ви маєте компонент-скрипт, що контролює ігровий обʼєкт. Така структура в редакторі буде виглядати приблизно так:
Тепер ви хочете відключити спрайт коли гра починається, а пізніше показати його. Це без проблем робиться таким кодом в “controller.script”:
function init(self)
msg.post("#body", "disable") -- <1>
end
Цей код працюватиме, як і очікується. Коли гра починається, скрипт адресує спрайт за його ідентифікатором “body” та використовує цю адресу щоб відправити спрайту повідомлення “disable”. Це спеціальне повідомлення, і його ефект полягає в тому що спрайт сховає свою графіку. Схематично, структура виглядає так:
Ідентифікатори в цій структурі довільні. Ми вирішили назвати ігровий обʼєкт - “bean”, його спрайт компонент - “body”, а компонент-скрипт, що контролює персонажа, назвали “controller”.
Якщо ви не оберете назву, за вас це зробить редактор. Коли ви створюєте новий обʼєкт або компонент, властивості Id буде автоматично призначене унікальне значення.
Якщо хочете, можете залишити ці автоматично згенеровані назви, але ми заохочуємо вас змінювати їх на описові імена.
Тепер давайте додамо ще один спрайт, та дамо квасолі щит:
Новий компонент повинен мати уникальний ідентифікатор в межах ігрового обʼєкта. Якщо назвати його “body” тоді код скрипту не може визначити якому спрайту відправити повідомлення “disable”. Отож ми маємо обрати унікальний (та описовий) ідентифікатор “shield”. Тепер ми можемо вмикати та вимикати “body” та “shield” коли забажаємо.
Якщо ви спробуєте використати один і той же ідентифікатор більше одного разу, редактор повідомить про це помилкою, тому, на практиці, це ніколи не проблема:
А тепер давайте подивимося що станется, якщо ви додасте більше ігрових обʼєктів. Припустимо, що треба обʼєднати дві квасолі (“beans”) в маленьку команду. Ви вирішили один ігровий обʼєкт назвати “bean”, а інший - “buddy”. Окрім того, після деякого часу бездіяльності, “bean” має наказати свому другу “buddy” почати танцювати. Для цього треба відправити cвоє власне повідомлення “dance” від скрипту “controller”, що належить обʼєкту “bean”, до скрипту “controller”, який належить обʼєекту “buddy”:
Хоча ми й маємо два різних компоненти названих “controller”, це не буде помилкою, оскільки кожен ігровий обʼєкт створює окремий контекст іменування.
Через те, що адресат повідомлення знаходиться за межами ігрового обʼєкта, який відправляє повідомлення (“bean”), в коді треба вказати який з “controller”-ів має отримати повідомлення. Потрібно вказати ідентифікатор ігрового обʼєкта разом із ідентифікатором компонента, якому належить отримати повідомлення. Повна адреса компонента буде "buddy#controller"
, і вона складається з двох окремих частин.
Повертаючись до попереднього прикладу з єдиним обʼєктом, ми бачимо, що пропустивши ідентифікатор ігрового обʼєкта, код може адресувати компоненти в поточному ігровому обʼєкті.
Наприклад, "#body"
позначає адрес компонента “body” в поточному ігровому обʼєкті. Такий код буде працювати у будь-якому ігровому обʼєкті, до тих пір, поки в ньому є компонент “body”.
Колекції дозволяють створювати групи, або ієрархії, ігрових обʼєктів та повторно їх використовувати контрольованим чином. Колекції використовуються в якості шаблонів (або “прототипів”, або “prefabs”-ів) в редакторі коли ви наповнюєте свою гру змістом.
Наприклад, ви хочете створити велику кількість “bean/buddy” команд. Цього можна досягти створивши шаблон в новому файлі колекції (назвемо його “team.collection”). В цьому файлі треба побудувати ігрові обʼєкти команди та зберегти файл. Потім створити екземпляр змісту цього файла-колекції в вашій головній колекції (bootstrap collection) і надати новому екземпляру імʼя(наприклад, “team_1”):
З такою структурою, ігровий обʼєкт “bean” все ще може посилатися на компонент “controller” в обʼєкті “buddy” за адресою "buddy#controller"
.
А якщо ми додамо другий екземпляр “team.collection” (назвемо його “team_2”), то код, який виконується в скриптах “team_2” також буде працювати. Ігровий обʼєкт “bean”, що належить колекції “team_2”, все ще може адресувати компонетн “controller” в обʼєкті “buddy” за адресою "buddy#controller"
.
Адреса "buddy#controller"
працює для обох обʼєктів з обох колекцій тому що вона відносна. Обидві колекції “team_1” та “team_2” створюють новий контекст іменування, або “простір імен”. Defold враховує контекст іменування під час адресації, і таким чином уникає колізій в іменах:
Під час вирішення фінальної адреси через відносну адресацію, поточний контекст іменування буде автоматично доданий в початок відносної адреси. Це дуже корисно, бо дозволяє вам створювати групи ігрових обʼєктів з кодом, та ефективно використовувати їх скрізь у вашому проєкті.
В Defold є два скорочення, які можна використовувати для відправки повідомлень, не вказуючи повний URL:
.
#
Наприклад:
-- Let this game object acquire input focus
msg.post(".", "acquire_input_focus")
-- Post "reset" to the current script
msg.post("#", "reset")
Щоб зрозуміти механізм іменування, давайте подивимося що відбувається коли ви зберете та виконаєте проєкт:
Для нашого прикладу вище, гра буде виконуватися із наступними чотирьма ігровими обʼєктами:
Ідентифікатори зберігаються як хешовані значення. Середовище виконання зберігає хеші ідентифікаторів кожної колекції, та використовує їх при перетворенні відносних адрес на хеші абсолютних.
Групування в колекції не існує під час виконання. Немає можливості дізнатися до якої колекції належав обʼєкт до компіляції. Виконувати дії над усіма обʼєктами в колекції водночас теж неможливо. Якщо ви потребуєте таку можливість, то вам доведеться самостійно відстежувати всі бажані обʼєкти в коді. Ідентифікатор кожного обʼєкта статичний, і він гарантовано буде незмінним на протязі всього життєвого циклу обʼєкта. Це означає що ви можете без ризику зберігати ідентифікатор обʼєкта та використовувати його у будь-який час.
Під час адресації, можна використовувати повні ідентифікатори. В більшості випадків краще використовувати відносну адресацію, тому що вона дозволяє повторно використовувати частини проєкту, але існують випадки коли абсолютна адресаціє буде необхідною.
Наприклад, якщо ви хочете створити менеджер ШІ, який відстежує стан кожного обʼєкта “bean”. Ви хочете щоб “bean”-и повідомляли свій активний статус менеджеру, а менеджер приймав тактичні рішення та віддавав накази “bean”-ам в залежності від їх статусу. Логічно було б створити єдиний ігровий обʼєкт-менеджер із скриптом, та покласти його поряд з колекціями команд (“team”) в стартовій колекції.
Кожен “bean” тепер відповідає за відправку повідомлень менеджеру: “contact” якщо він подбачив ворога, або “ouch!” якщо він отримав ушкодження. Для цього скрипт “controller” в обʼєкті “bean” використовує абсолютну адресацію щоб віправляти повідомлення компоненту “controller” в обʼєкті “manager”.
Будь-яка адреса що починається з ‘/’ буде вирішуватись від кореня ігрового світу. А це відповідає кореню стартової колекції, яка завантажується на початку гри.
Абсолютною адресою скрипту менеджера буде "/manager#controller"
і ця адреса буде вказувати на вірний компонент незалежно від того, де вона використовується.
Рушій зберігає всі ідентифікатори як хешовані значення. Усі функції, що приймають компонент або ігровий обʼєкт в якості аргументу, приймають рядок, хеш або URL обʼєкта. Вище ми вже побачили як використовувати рядки для адресації.
Коли ви отримуєте ідентифікатор ігрового обʼєкта, рушій завжди поверне хеш абсолютного шляху:
local my_id = go.get_id()
print(my_id) --> hash: [/path/to/the/object]
local spawned_id = factory.create("#some_factory")
print(spawned_id) --> hash: [/instance42]
Такий хеш можна використовувати замість рядка ідентифікатора, або можна зібрати самому. Зауважте, що хешований ідентифікатор відповідає шляху до обʼєкта, тобто повній адресі:
Відносні адреси мають надаватися рядками, тому що рушій буде обчислювати новий хеш ідентифікатора шляхом хешування заданого рядка разом з хешом поточного контексту іменування (колекції).
local spawned_id = factory.create("#some_factory")
local pos = vmath.vector3(100, 100, 0)
go.set_position(pos, spawned_id)
local other_id = hash("/path/to/the/object")
go.set_position(pos, other_id)
-- This will not work! Relative addresses must be given as strings.
local relative_id = hash("my_object")
go.set_position(pos, relative_id)
Для повноти картини подивимося на загальний формат адрес в Defold: URL.
URL - це обʼєкт, зазвичай рядок, у спеціальному форматі. Звичайний URL складається з трьох частин:
[socket:][path][#fragment]
Як ми вже бачили, частину, або навіть більшість, цієї інформації часто можна пропускати. Вам майже ніколи не треба вказувати сокет, і вам часто, але не завжди, треба вказувати шлях. Сокет потрібно вказати, якщо вам треба адресувати речі в іншому ігровому світі. Наприклад, повний URL до скрипту “controller” в ігровому обʼєкті “manager” буде:
"main:/manager#controller"
а до “controller”-а в “buddy” в “team_2” буде:
"main:/team_2/buddy#controller"
Ми можемо відправляти їм повідомлення:
-- Send "hello" to the manager script and team buddy bean
msg.post("main:/manager#controller", "hello_manager")
msg.post("main:/team_2/buddy#controller", "hello_buddy")
URL обʼєкти можна створювати програмно в Lua коді:
-- Construct URL object from a string:
local my_url = msg.url("main:/manager#controller")
print(my_url) --> url: [main:/manager#controller]
print(my_url.socket) --> 786443 (internal numeric value)
print(my_url.path) --> hash: [/manager]
print(my_url.fragment) --> hash: [controller]
-- Construct URL from parameters:
local my_url = msg.url("main", "/manager", "controller")
print(my_url) --> url: [main:/manager#controller]
-- Build from empty URL object:
local my_url = msg.url()
my_url.socket = "main" -- specify by valid name
my_url.path = hash("/manager") -- specify as string or hash
my_url.fragment = "controller" -- specify as string or hash
-- Post to target specified by URL
msg.post(my_url, "hello_manager!")
Did you spot an error or do you have a suggestion? Please let us know on GitHub!
GITHUB