Уявіть: гравець натискає кнопку в меню, а інтерфейс зависає на секунду — затримка через невдало написану прив'язку даних. Або при перетягуванні предмета в інвентарі ghost-іконка не слідує за курсором, тому що RectTransformUtility неправильно спроектував координати. Такі баги не впадають в очі на ранніх етапах, але на етапі збору зворотного зв'язку від тестувальників перетворюються на головний біль. Логіка відображення UI — це не просто візуал, а зв'язка патернів, подій та асинхронних операцій, яка займає до 30% часу розробки всього інтерфейсу. За 5 років ми реалізували UI-логіку для 40+ проєктів — від мобільних казуалок до PC-екшенів, і накопичили набір перевірених рішень, які скорочують час розробки на 20-40% та знижують GC Alloc на 60-80%. Оцініть бюджет вашого UI за 30 хвилин консультації — зв'яжіться з нами.
Програмування логіки відображення графіки інтерфейсу
Головний архітектурний вибір — як UI дізнається про зміни в стані гри. Три основні підходи:
Observer / Event System — UI підписується на події ігрової системи: HealthSystem.OnHealthChanged += UpdateHealthBar. Слабкий зв'язок, можна легко переключати UI-реалізації. Проблема: при знищенні об'єкта до відписки — NullReferenceException. Обов'язково відписуватися в OnDisable() або OnDestroy(). Це реалізація Observer pattern.
MVP (Model-View-Presenter) — Presenter відповідає за зв'язок між даними (Model) і відображенням (View). View не знає про ігрову логіку, тільки про свої візуальні компоненти. Presenter знає обидві сторони. Добре масштабується, добре тестується. Типова реалізація в Unity — абстрактний клас BaseView з методами Show(), Hide(), Bind(IModel model), і конкретні Presenter'и під кожен екран.
ScriptableObject-based подійна система — всі події як ScriptableObject з методом Raise() і списком слухачів. Популяризовано Ryan Hipple на Unite кілька років тому. Зручно для невеликих команд, легко дебажити в Inspector. Мінус: при великій кількості подій стає складно керувати залежностями.
Для більшості проєктів рекомендую MVP з EventSystem для крос-модульної комунікації. Це дає читабельний код, передбачувану поведінку та хорошу тестованість UI без запуску сцени.
Як керувати станами екранів?
Screen Manager — типова система, яка контролює, який екран відкрито, і керує переходами. Мінімальна реалізація: стек екранів (для Back navigation), словник screenId → IScreen, методи Push(), Pop(), Replace().
Для мобільних платформ Android Back Button повинен коректно оброблятися через Application.exitCancellationToken або через Input.GetKeyDown(KeyCode.Escape) — при натисканні повинен закриватися верхній екран у стеку, а не виходити з гри. Це те, що часто забувають при розробці та помічають при здачі в Google Play.
CanvasGroup — інструмент для керування видимістю та інтерактивністю групи елементів (див. документацію Unity). canvasGroup.alpha = 0 приховує візуально, але елементи продовжують отримувати події. Обов'язково також виставляти canvasGroup.interactable = false та canvasGroup.blocksRaycasts = false при приховуванні — інакше невидимі кнопки перехоплюють кліки крізь них.
Кейс з нашої практики: інвентар з drag & drop — програмування логіки відображення
Задача: drag & drop перетягування предметів між слотами інвентаря з підтримкою геймпада. На мишці — IBeginDragHandler, IDragHandler, IEndDragHandler. На геймпаді — зовсім інша логіка: курсорний режим навігації з вибором джерела та цілі через кнопки.
Реалізація drag & drop в uGUI вимагає створення ghost-об'єкта — копії перетягуваної іконки, яка слідує за курсором через RectTransformUtility.ScreenPointToLocalPointInRectangle(). Ghost повинен знаходитися в окремому Canvas поверх усього іншого (окремий Canvas з Sort Order вище основного) — інакше іконка буде перекриватися іншими елементами при перетягуванні.
Для геймпада реалізували окремий режим: перше натискання A вибирає слот (підсвічується selected state), навігація D-pad переміщує віртуальний курсор між слотами, друге натискання A завершує перетягування. Два режими керування — два різні кінцеві автомати в одному InventoryController.
Як async/await спрощує UI-логіку і в чому підводний камінь?
Сучасна Unity-розробка все активніше використовує async/await замість корутин. Для UI-логіки це особливо зручно при роботі з мережевими запитами, завантаженням даних, очікуванням анімації перед переходом екрана.
Головна пастка async в Unity: операції продовжуються навіть після знищення об'єкта. await Task.Delay(2000) в методі кнопки — і через 2 секунди код продовжує виконуватися, звертаючись до вже знищених компонентів. NullReferenceException гарантовано. Рішення: перевірка this == null після кожного await (Unity перевизначає оператор == для UnityEngine.Object), або використання CancellationToken, який скасовується в OnDestroy.
Другий нюанс: async-метод, кинутий без await (fire-and-forget), не обробляє виключення. LoadUserData() без await — виключення всередині методу просто проковтується, користувач бачить порожній екран, логів немає. Для fire-and-forget методів обов'язково додаємо глобальний обробник через TaskScheduler.UnobservedTaskException або обгортаємо виклик в try-catch всередині самого методу.
UniTask (Cysharp/UniTask) — суттєво кращий варіант, ніж стандартний Task для Unity. Працює без heap allocation для більшості операцій, інтегрується з PlayerLoop Unity, підтримує UniTask.Delay з прив'язкою до PlayerLoopTiming (Update, FixedUpdate, LateUpdate) та коректно обробляє cancellation при знищенні об'єкта через destroyCancellationToken. На проєктах з активним використанням async це знижує GC Alloc на 60–80% порівняно з System.Threading.Tasks — тобто в 2–3 рази ефективніше.
Як покроково реалізувати Screen Manager для навігації
- Визначте інтерфейс IScreen з методами
Show()таHide(). - Створіть ScreenManager як синглтон або сервіс в DI.
- Зберігайте стек екранів для підтримки Back.
- Реалізуйте методи
Push()(відкрити новий екран, приховавши поточний) таPop()(закрити поточний, повернути попередній). - Для мобільних пристроїв підпишіться на кнопку Back та викликайте
Pop(). - Додайте Replace для випадків, коли не потрібно повертатися до попереднього екрана.
Порівняння підходів до зв'язку UI та логіки
| Підхід | Зв'язність | Складність реалізації | Тестованість | Продуктивність |
|---|---|---|---|---|
| Event System | Низька | Середня | Середня | Висока |
| MVP | Середня | Висока | Висока | Висока |
| ScriptableObject Events | Низька | Низька | Низька | Середня |
Логіка відображення даних: типові проблеми
Оновлення UI кожен кадр — антипатерн. healthBar.fillAmount = player.health / player.maxHealth в Update() працює, але перестворює mesh для Image компонента кожен кадр навіть якщо значення не змінилося. Правильно: оновлювати тільки при зміні даних через event або property з setter.
Рядкова конкатенація в Update — гірше. levelText.text = "Level: " + player.level створює новий рядок кожен виклик, провокуючи GC Allocations. Використовуємо string.Format() або StringBuilder для часто оновлюваних текстів. У TextMeshPro є SetText(string, float) — перевантаження з float-аргументом, яке форматує число без GC allocation.
Z-fighting кнопок: два Button'и з однаковим Sort Order в одному Canvas, один поверх іншого. EventSystem надсилає подію на верхній за ієрархією, але якщо Raycast Target увімкнено в обох — кліки можуть провалюватися. Перевіряємо через UI Debugger (правий клік в EventSystem → UI Debug).
Відображення Loading State: при очікуванні відповіді від сервера UI повинен блокувати повторні натискання. Типова помилка — просто показати Spinner і забути вимкнути кнопку. Правильно: блокуємо CanvasGroup.interactable = false для всього екрана + показуємо Loading Overlay + в finally блоці async-методу відновлюємо interactable = true. Інакше при повільному з'єднанні користувач встигає натиснути кнопку кілька разів.
Що входить в роботу
- Архітектурне рішення патерну зв'язку UI з ігровою логікою
- Реалізація Screen Manager та навігації
- Логіка кожного екрана з юніт-тестами Presenter'ів
- Інтеграція з ігровими системами (події, дані)
- Налаштування CanvasGroup, обробка введення (миша, геймпад)
- Вирішення проблем з async/await та корутинами
- Тестування на цільових пристроях та профілювання
Процес та терміни
Розробка починається з архітектурного рішення: вибір патерну зв'язку з ігровою логікою, визначення Screen Manager архітектури, узгодження інтерфейсів між UI та ігровими системами. Потім — реалізація базової інфраструктури (Screen Manager, Base View, Event Bus). Далі — поекранна реалізація з юніт-тестами для Presenter'ів. Фінал — інтеграційне тестування на пристроях.
| Задача | Терміни |
|---|---|
| Логіка 1 екрана (відображення даних, кнопки) | 1–3 дні |
| Screen Manager + навігація для всього проєкту | 3–7 днів |
| Складна система (інвентар, drag & drop, геймпад) | 1–2 тижні |
| Повна UI-логіка інді-проєкту (10–15 екранів) | 4–10 тижнів |
Ми гарантуємо, що UI буде працювати стабільно на пристроях середнього класу та вище, а терміни та бюджет будуть узгоджені до початку робіт. Отримайте консультацію — зв'яжіться з нами для оцінки вашого проєкту.






