Забуте секретна зброя Unity — UnityEvents

Страхітливий венеріанський комар (фото Річарда Джонса)Наш відважний герой, випадково забредший в заборонену область отруйних венеріанських джунглів, оточена роєм з десяти тисяч голодних полуразумных комарів. Питання їстівності людини для інопланетних організмів, з наукової точки зору, взагалі-то, досить спірним — але венерианские комарі, як відомо, щиро поділяють позицію Маркса, що критерієм істинності судження є експеримент. Здавалося б, становище безнадійне — але герой, не розгубившись, дістає з широких штанин інвентарю похідний термоядерний фумігатор, красивим рухом включає його, і…
І на цьому моменті розробка гри серйозно забуксувала, стрімко обростаючи милицями і підпорами, оскільки розробник гри не був знайомий з одним маленьким, але вкрай важливим механізмом движка.
Механізм, якого не знав розробник, сплячий на клавіатурі після тривалого спринту з програмування абсолютно не потрібного по суті своїй менеджера управління комарами — це вбудована система подій.
Стривайте, але я-то знаю, що таке система подій!Як показало опитування, проведене нещодавно в російськомовному сегменті Unity-спільноти, лякаюче великий відсоток вітчизняних розробників взагалі не підозрює про те, що така річ, як події, була колись измыслена людським розумом. Почасти, цього загального прогалині в знаннях посприяла офіційна документація, хитрим протичовновим зигзагом оминає питання практичного застосування подій у Unity, частково — загальна заплутаність архітектури цієї частини простого і зручного, по суті, движка.
Крім того, Ви напевно користуєтесь одним з найбільш відомих механізмів, що надаються засобами C#. Це хороший вибір — але, може, Вам буде цікаво дізнатися і про можливості вбудованої в Unity системи і її власних «булочками»?
Лікнеп, або «Будильник проти таймера»
(якщо Ви знайомі з тим, що таке ігрові події — спокійно пропускайте цей розділ)
imageБільшість сучасних ігрових движків чимось схожі з звичайними механічними годинами. Об'єкти движка — шестерні великого часового механізму ігрового світу. І, як годинниковий механізм функціонує за рахунок того, що шестерінки хитро з'єднані між собою, так життя і рух ігрового світу здійснюються за рахунок взаємодії об'єктів один з одним.
Метафора не зовсім точна, проте, очевидно, що кожний об'єкт повинен мати дані не тільки про власному внутрішньому стані, але і розташовувати поруч відомостей про навколишній його світі. Способи одержання цих відомостей діляться на дві групи:
  • Активні — ініціатива в отриманні нових даних виходить від самого об'єкта, тобто об'єкт сам посилає запит.
  • Пасивні — інформація надходить у результаті зовнішнього (по відношенню до об'єкта) події — об'єкт для її одержання нічого толком і не робив.
Розглянемо приклад з реального світу. Уявімо собі, що ми голодні, і ставимо з цього приводу на вогонь велику каструлю варитися пельменів. Алгоритм цього дійства простий — закинути пачку пельменів в гарячу подсоленую воду, включити вогонь, відійти на десять хвилин (посидіти в інтернеті пів-годинки), зняти угольки пельмені з вогню.
Уявімо, що це гра, а ми в ній — ігровий об'єкт. Як же ми визначаємо, коли настане момент йти вимикати пельмені?
Можна використовувати активний спосіб отримання інформації про час — поставити перед носом таймер / годинник — і регулярно запитувати у них час, кидаючи на циферблат косі погляди. Як тільки, подивившись в черговий раз, ми побачимо, що годинник нарешті відрахували покладені десять хвилин з моменту початку готування, ми біжимо знімати каструлю з вогню.
Можна використовувати пасивний спосіб — завести будильник, поставивши його на той час, коли пельмені повинні за нашими прикидками зваритися. Коли будильник дзвонить, ми реагуємо на це подія і йдемо вживати заходів щодо їжі.
Однозначно, другий спосіб зручніше — не треба постійно відриватися від поточних справ, шукати навколо себе об'єкт «годинник», а потім з'ясовувати у нього цікаві нам дані. Крім того, небайдужі до долі близького обіду домашні можуть також підписатися на подію «будильник продзвонив» і йти займатися своїми справами, звільнені від необхідності регулярно підходити і піднімати очі наші улюблені годинник, або ж питати про обід у нас особисто.
У свою чергу, перший спосіб — спосіб регулярного активного звернення для отримання якихось відомостей — називається в літературі polling (від англ. «poll» — опитування). Викликає подія об'єкт називається відправником, що сприймає його об'єкт — одержувачем, процес стеження за подією називається прослуховуванням (listen), а функція, вызывающаяся у одержувача по отриманню події зветься обробником.
Годинник? Пельмені? Ась?!
image
Добре, інший приклад. Дивилися другого «Шрека»?
Сценка на YouTube
Згадайте момент, коли троє героїв їдуть в кареті в Далеке-Далеке Королівство. Чудова юморитическая сценка, де непосидючий Осел дістає героїв, регулярно цікавлячись:
-Ми вже приїхали?
-Ні.
-… ми вже приїхали?
-Ні, не приїхали!
-… ми вже приїхали?
-Неееееет!
Те, чим займається Осел — це хороший приклад polling-а в чистому вигляді. Як бачите, це здорово стомлює :)
Інші герої, у свою чергу — умовно — підписані на подію «приїзд», і тому не даремно витрачають системного часу, ресурсів, а також не вимагають додатково зусиль програміста на реалізацію додаткової жорсткого зв'язку між об'єктами.
Секрет, який у всіх на виду
Отже, як же реалізувати події на Unity? Давайте розглянемо на простому прикладі. Створимо в новій сцені два об'єкта — ми хочемо, щоб один генерував повідомлення по клацанню миші на ньому (або повз нього, або не миші взагалі), а другий — реагував на подію і, скажімо, перефарбовуватися. Назвемо об'єкти відповідно і створимо потрібні скрипти — Sender (генерує подію) і Receiver (приймаючий подія):
image 
Код скрипта Sender.js:
public var testEvent : Events.UnityEvent = new Events.UnityEvent ();

function Start () { }

function Update () 
{
// Після натискання будь-якої клавіші 
// викликаємо подія (якщо воно існує)
if (Input.anyKeyDown && testEvent != null)
testEvent.Invoke (); 
}

Особливу увагу зверніть на рядок:
public var testEvent: Events.UnityEvent = new Events.UnityEvent ();

І код скрипта Receiver.js:
function Start () { }

function Update () { }

// Функція перефарбування об'єкта в довільний колір
function Recolor()
{
renderer.material.color = Color(Random.value, Random.value, Random.value);
}

Тепер подивимося на об'єкт Sender і побачимо:
image
Ага! Наша подія, оголошене як публічна мінлива об'єкт типу UnityEvent — не забарилося здатися в редакторі властивостей. Правда, поки воно виглядає самотнім і загубленим, так як ні один з об'єктів на нього не підписаний. Виправити цей недолік, вказавши Receiver як слухає подія об'єкт, а як викликається з отримання обробника події — нашу функцію Recolor():
image
Запустимо, натиснемо пару раз яку кнопку..., результат досягнутий — об'єкт слухняно перефарбовується в самі несподівані кольору:
image
«Гей! Стоп-стоп! Але ми ж цим вже користувалися, коли працювали з Unity UI!» — скажете Ви… і опинитеся несподівано праві.
UI, що поставляється в Unity з версії 4.6, використовує той самий механізм подій. Ось тільки набір подій UI обмежений викликаються користувальницьким введенням, в той час як движок дозволяє програмісту настворювати їх на будь-який привід і викликати їх по будь-якому зручному приводу.
Але давайте рухатися далі!
«Додати другого одержувача? Не можна, всі ви брешете!»
Відмінно працює. Працює! Але з залу вже лунають голоси людей, які запустили середу, потестировавших, і початківців звинувачувати автора в… неточності. І правда — після недовгого вивчення, допитливий читач з'ясовує, що редактор Unity дозволяє зв'язати подія тільки з одним примірником Receiver і починає лаятися. Другий примірник просто нікуди виявляється вписати — поле приймає сигнал об'єкта не передбачає множинність записів.
І це провал, здавалося б.
Проте, все не так погано. Відставивши у бік «програмування мишкою», ми заглибимося в код. Виявляється, на рівні коду все легко, і цілком можна зробити слухачами однієї події кілька об'єктів.
Синтаксис команди для цього простий:
экземплярСобытия.AddListener(функцияОбработчик);

Якщо ж, забігаючи вперед, є бажання по ходу виконання коду перестати слухати подія, то допоможе функція:
экземплярСобытия.RemoveListener(функцияОбработчик);

Добре, тепер міць подій в наших руках, і, здавалося б, додавання другого одержувача — питання вирішене.
Але є нюанс — примірник події зберігається у Sender, Receiver про нього нічого не знає. Що ж робити? Не тягнути ж вказівник на Sender всередину Receiver-га?
Розумні книжки на цьому місці часто обмежуються формулюваннями на кшталт: «А рішення цього питання ми залишаємо на самостійне опрацювання читачеві». Однак, я покажу тут одне хитре, не зовсім очевидне (і не саме витончене рішення. Підходить воно не завжди і не всюди, але якщо Ви зумієте дістатися до ситуації, коли Вам потрібно щось більше — то до цього моменту Ви напевно вже володієте достатнім багажем знань, щоб впоратися з подібною проблемою самостійно.
Отже, рішення: подія, як будь-який поважає себе змінну, можна без особливих проблем зробити… статичним!
Змінимо код Sender наступним чином:
public static var testEvent: Events.UnityEvent = new Events.UnityEvent ();

Мінлива закономірно пропала з редактора, оскільки тепер вона не належить ні одному екземпляру класу. Зате тепер вона доступна глобально. Щоб її використовувати, змінимо код Receiver:
Receiver.js
function Start () {
OnEnabled();
}

function Update () { }

function Recolor()
{
renderer.material.color = Color(Random.value, Random.value, Random.value);
}

function OnEnabled()
{
Sender.testEvent.AddListener(Recolor);
}

function OnDisable()
{
Sender.testEvent.RemoveListener(Recolor);
}

Размножим об'єкт Receiver і запустимо гру:
image
Так, тепер ансамбль Receiver-ів дружно перефарбовується в різні кольори на будь-яке натиснення клавіші. Але що ж ми зробили всередині скрипта?
Все дуже просто — раз змінна статична, то звертаємося тепер не до конкретного екземпляру об'єкту, а по імені класу. Більш цікавий інший момент — функції OnEnabled OnDisable. Об'єкти, по ходу гри, мають тенденцію вимикатися або взагалі видалятися з рівня. Unity ж досить нервово реагує, коли на подію раніше був хтось підписаний, але жоден об'єкт тепер більше не слухає. Мовляв, гей, де всі?
та й коли об'єкт неактивний, йому зазвичай немає реальної необхідності продовжувати далі ловити події це, буде імплементовано, могло б привести до цікавих наслідків. Відповідно, як мінімум розумно прив'язувати/відв'язувати функції в той момент, коли об'єкт включається/відключається. А коли об'єкт віддаляється, то у нього попередньо автоматично викликається функція OnDisable — так що можна на цей рахунок і не турбуватися.

Бонусний рівень — видалення об'єкта в оброблювачі

Здавалося б, все елементарно — в тому ж Recolor тепер викликаємо Destroy(this.gameObject) і справу зроблено? Спробуємо, і...
Та не виходить. Видаляється тільки самий перший (інколи — два) з приймаючих подія об'єктів, а до решти чомусь після цього подія вже не доходить. Дивно? Дивно і незрозуміло. Може бути, хтось із гуру Unity підкаже мені ідеологічно правильний підхід, але оскільки я люблю вигадувати велосипеди поки виявив більш елегантне рішення, то поділюся, знову ж таки, своїм.
Якщо видалення обробного подія об'єкта в оброблювачі заважає подальшій обробці то давайте і не будемо об'єкт видаляти оброблювачі. Видалимо його в сопрограмме, що виконується після обробки події — для чого движок, до речі, має стандартний метод. Будемо видаляти об'єкт за допомогою команди Destroy(this.gameObject, 0.0001). Видалення відбудеться, але не відразу, а буде відкладено на 0.0001 секунди. Неозброєним оком цієї паузи не помітити, а от процес обробки події не затнеться на об'єкті і спокійно продовжиться далі.
Передача параметрів
Іноді буває потрібно передати подію, супроводжуючи його характеристикою події — радіусом, втратою, кількістю, підтипом, матірним коментарем гравця, і так далі. Для цих цілей придумані різновиди UnityEvent з числом аргументів від одного до чотирьох. Їх практичне застосування не складніше розглянутого вище.
Sender.js
class MyIntEvent extends Events.UnityEvent.<int> {}
public static var testEvent : MyIntEvent = new MyIntEvent ();

function Start () { }

function Update () 
{
// Після натискання будь-якої клавіші 
// викликаємо подія (якщо воно існує)
if (Input.anyKeyDown && testEvent != null)
testEvent.Invoke (15); 
}

Receiver.js
public var health : int = 100;

function Start () {
OnEnabled();
}

function Update () { }

function Recolor(damage: int)
{
health = health - damage;
if (health <=0)
Destroy(this.gameObject, 0.0001);
else
renderer.material.color = Color(1.0 f, health/100.0, health/100.0);
}

function OnEnabled()
{
Sender.testEvent.AddListener(Recolor);
}

function OnDisable()
{
Sender.testEvent.RemoveListener(Recolor);
}

Працює — ось кілька склеєних в один кадрів:
image
Як бачите, нічого особливо хитрого робити не треба.
Практичне застосування системи подій
Часто буває так, що об'єкту потрібно постійно мати інформацію про те, що сталося певну дію, або виконано певну умову. Сидить у засідці монстр перевіряє — чи перетнув хто-небудь з персонажів межу, за якою його дозволено бачити і атакувати? Венерианские комарі перевіряють — не включився чи фумігатор, під час роботи якого їм належить з криками розлетітися на всі боки? Безтілесний і нематеріальний тригер на карті перевіряє — знищив чи вже гравець десяток доручених йому з квесту ховрахів?
Якщо почати реалізовувати в грі всі подібні умови активним полінгом — тобто через регулярні перевірки величин самим об'єктом — то тут розробника чекає набір найцікавіших труднощів. Сидить у засідці монстр повинен регулярно «питати» у кожного предмету по цей бік лінії — а чи не є він, мовляв, персонажем? Комарі повинні «знати в обличчя» гравця з фумигатором (отримувати при створенні покажчик на нього?) і періодично уточнювати, не включив він це страшна зброя. Тригеру живеться ще складніше — він повинен зберігати /повний набір всіх ховрахів/ і регулярно перевіряти, скільки ще живі.
Звичайно, запропоновані приклади пропонують досить примітивну реалізацію. Комарів, наприклад, можна загнати при народженні в масив спеціального /менедежера комарів/, а у гравця зберігати вказівник на менеджер комарів. При включенні фумігатора буде викликатися метод менеджера, який пройде по масиву і викличе у кожного комара метод «лякайся-і-відлітай»… але зайві менеджери — воно нам так і треба?
UnityEvent краще альтернативних варіантів? І, до речі, а що з продуктивністю?
imageЯкщо Ви не перший рік працюєте в Unity, то напевно вже якось реалізовували в своїх проектах події. Тепер Ви знаєте про механізм подій у Unity. Чим же краще UnityEvent аналогічних засобів, наприклад, з числа стандартного функціоналу C#?
Є розробники, які пишуть на JavaScript. Так-так, саме для них, в першу чергу, в статті код і наводиться на цьому легковажному мовою. Зі зрозумілих причин, кошти, які надаються C#, їм недоступні. Для них UnityEvent — гідний і зручний механізм, доступний «з коробки» — бери і користуйся.
Зручно також те, що UnityEvent — один і той же код на JS і на C#. Подія може створюватися в коді C# і слухатися кодом на JS — що, знову ж таки, неможливо безпосередньо зі стандартними делегатами C#. Отже, цими подіями можна без докорів совісті зв'язати фрагменти коду на різних мовах, буде такий непотріб завелося у Вас в проекті (наприклад, після закупівлі в Unity Store особливо важливого і складного ассета).
Аналіз швидкодії подій різних типів Unity показав, що UnityEvent може бути від 2 40 разів повільніше (у частині витрат часу на виклик обробника), ніж та ж система делегатів C#. Що ж, різниця не така вже велика, оскільки це все одно ДУЖЕ ШВИДКО, якщо не зловживати.
Порівняйте вбудованим SendMessage і жахніться :)
Висновок
Сподіваюся, що ця стаття змогла додати ще один інструмент чиюсь скарбничку прийомів і підходів, і ще одна таємниця непоганого, по суті, движка стала трохи менш таємничою для кого-то. Події — дуже потужний інструмент в Ваших руках.
Звичайно, це — «срібна куля», що вирішує всі проблеми архітектури, але вони — зручний засіб, вирішальну«одну з найперших завдань розробки архітектури програми — пошук такого дроблення на компоненти і такої їх взаємодії, щоб кількість зв'язків було мінімальним. Тільки в такому разі можна буде потім доопрацьовувати компоненти окремо».
Події — потужний механізм у ваших руках. Ніколи не забувайте про його існування. Використовуйте його з розумом, і розробка Ваших проектів стане швидше, приємніше і успішніше.
Посилання
При складанні статті були, крім зазначених безпосередньо в тексті, у безлічі використані різні матеріали публікацій.
Ось найбільш цікаві з них, а також просто корисні посилання по темі:
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.