Геймплейне програмування
Розробник передає проект зі словами «там архітектура дещо дивна, але працює». Відкриваєте — і бачите: контроллер персонажа на 2000 строк, де фізика, анімація, UI та звук перемішані в одному MonoBehaviour. У Update() — перевірки стану через десяток boolean-флагів. Збереження через PlayerPrefs з ключами типу "player_hp_current_value_int". Це не гіпотетика — це типовий стан коду в проектах, які росли органічно без архітектурного рішення на початку.
Геймплейне програмування — серце гри. Саме тут відчуття від управління, інтелект противників, чесна фізика і надійна система прогресу. Зроблено погано — ніякий арт не спасе.
Контроллер персонажа
Перший контакт гравця — управління. Затримки вводу, ковзке рух, «залипання» на перешкодах — усе це сприймається миттєво і шкодить перед тим, як гравець встигне побачити геймплей.
Базовий вибір: Character Controller або Rigidbody?
CharacterController — вбудований компонент Unity, спеціалізований для персонажів. Ігнорує фізичний рушій для руху, але коректно обробляє ступені, похилі поверхні та перешкоди. Рекомендований для екшн-ігор, платформерів, шутерів від першої особи — де потрібен точний передбачуваний відклик.
Rigidbody — фізичний об'єкт. Необхідний, коли персонаж повинен взаємодіяти з фізичними об'єктами: штовхати ящики, реагувати на вибухи, бути підкинутим. Вимагає обережної роботи через FixedUpdate та обережного вимкнення гравітації/тертя, щоб управління не здавалося «плаваючим».
Для більшості 3D-проектів ми використовуємо CharacterController з кастомним обробником гравітації — це дає контроль без артефактів фізичного рушія. Для 2D — Rigidbody2D з обмеженнями на вращення та ретельно налаштованим Collision Detection Mode: Continuous.
Фізика та колізії
Rigidbody та коллайдери — джерело регулярних проблем, якщо їх не налаштувати правильно з самого початку.
Кілька правил, які заощаджують час:
-
Collision Detection: Continuousдля швидких об'єктів (кулі, снаряди) — інакше вони «пролітають» крізь тонку геометрію - Складні mesh-коллайдери замінюємо складеними примітивами (Box + Capsule + Sphere) — це дешевше для фізики на 70–80%
- Шари (
Physics Layers) та матриця колізій уPhysics Settingsналаштовуються на початку проекту — потім додати їх без рефакторингу дуже болісно - Усі фізичні обчислення — у
FixedUpdate, не вUpdate. Інакше поведінка залежить від FPS
Глибше: архітектура ШІ противників
Це та область, де різниця між «працює» і «працює добре» найбільш відчутна. Поганий AI видно одразу: противники застрягають у кутках, атакують крізь стіни, передбачувано патрулюють по одному маршруту.
Конечні автомати (State Machines)
Найпоширеніший підхід — ієрархічний конечний автомат (HSM). Кожен стан: Idle, Patrol, Chase, Attack, Dead — це клас або метод з входом, оновленням та виходом.
public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }
private void UpdateStateMachine() {
switch (_currentState) {
case EnemyState.Patrol:
UpdatePatrol();
if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
break;
case EnemyState.Chase:
_navMeshAgent.SetDestination(_player.position);
if (InAttackRange()) TransitionTo(EnemyState.Attack);
if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
break;
// ...
}
}
State Machine добре працює для противників з невеликою кількістю станів (5–8). При зростанні складності — вибуховий ріст переходів між станами, код стає складно читати і тестувати.
Behaviour Trees
Behaviour Tree (BT) — наступний рівень. Дерево поведінки описує логіку агента через ієрархію задач: Sequence, Selector, Decorator, Leaf.
Перевага перед State Machine: кожен вузол атомарний і переиспользуємий. Вузол CheckLineOfSight написаний один раз і використовується в десяти деревах. Додати нову поведінку — означає додати гілку в дерево, не рефакторити існуючу логіку.
У Unity BT реалізується через ассети (NodeCanvas, Behaviour Designer) або кастомну реалізацію. Для великих проектів з кількома типами ворогів це окупається вже на етапі другого типу противника.
Приклад структури дерева для патрульного противника:
Root
└── Selector
├── Sequence (Combat)
│ ├── IsPlayerVisible
│ ├── IsPlayerInRange
│ └── AttackPlayer
├── Sequence (Alert)
│ ├── HeardSound
│ └── InvestigatePosition
└── Sequence (Patrol)
├── HasPatrolRoute
└── FollowPatrolRoute
GOAP — коли BT недостатньо
Goal-Oriented Action Planning (GOAP) — підхід для дійсно складного AI, де агент повинен планувати послідовність дій для досягнення мети з урахуванням поточного стану світу.
Класичний приклад — противник, якому потрібно «убити гравця». Якщо у нього немає зброї, він шукає зброю. Якщо немає боєприпасів, він шукає патрони. Якщо гравець укрився, він шукає обхідний маршрут. GOAP дозволяє задати ці дії та їхні передумови/постумови, а планувальник будує ланцюжок автоматично.
GOAP значно складніший у реалізації, ніж BT, і виправданий не завжди. Для платформерів та казуальних ігор це надлишково. Для тактичних ігор, симуляторів виживання, stealth-action — може бути правильним вибором.
NavMeshAgent та навігація
NavMeshAgent — стандартний інструмент для навігації у Unity. Працює коректно при правильному налаштуванні NavMesh та агентів:
-
Agent RadiusтаAgent Heightповинні точно відповідати коллайдеру персонажа -
Stopping Distanceпотребує налаштування під дальність атаки кожного типу ворога -
NavMesh ObstacleзCarve: trueдля динамічних перешкод (падаючі ящики, закриваючі двері) — інакше агенти будуть намагатися пройти крізь них - Для великих відкритих світів — NavMesh Links для з'єднання окремих сегментів та Off-Mesh Links для стрибків і спусків
Глибше: система збережень
Друга область, де архітектурні рішення на початку критично впливають на всю подальшу розробку. Збереження, додані в кінці розробки «за неділю», майже завжди ломаються при зміні структури даних.
PlayerPrefs — коли підходить і коли ні
PlayerPrefs — це сховище простих ключ-значення (string, int, float). Підходить строго для налаштувань (гучність, управління, мова). Використовувати його для збереження стану ігрового світу — помилка: немає типізації, немає версійності, немає зручного дебагу.
JSON-серіалізація
Робочий підхід для більшості проектів — серіалізація даних у JSON через JsonUtility (вбудований, швидкий, але обмежений) або Newtonsoft.Json (повнофункціональний, підтримує словники, спадкування, nullable типи).
Структура системи збережень:
[Serializable]
public class SaveData {
public int version = 1; // версійність
public PlayerSaveData player;
public WorldSaveData world;
public SettingsSaveData settings;
}
public class SaveSystem : MonoBehaviour {
private const string SAVE_FILE = "/save.json";
public void Save(SaveData data) {
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
}
public SaveData Load() {
string path = Application.persistentDataPath + SAVE_FILE;
if (!File.Exists(path)) return new SaveData();
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<SaveData>(json);
}
}
ScriptableObject як контейнер даних
ScriptableObject — недооцінений інструмент для збереження ігрових даних. Конфігурації предметів, характеристики противників, параметри рівнів — усе це зручніше тримати у ScriptableObject, ніж у JSON або константах у коді.
Для збережень ScriptableObject використовується в паттерні Runtime Set та Variable: значення зберігаються у ScriptableObject, збереження записує тільки дельту відносно дефолтних значень.
Версійність збережень
Поле version у корені SaveData — не бюрократія, а необхідність. Коли через три місяці після релізу додається нова механіка з новими полями, потрібно коректно мігрувати старі збереження. Метод міграції:
private SaveData MigrateSaveData(SaveData data) {
if (data.version < 2) {
data.player.newField = defaultValue;
data.version = 2;
}
if (data.version < 3) {
// наступна міграція
data.version = 3;
}
return data;
}
Без версійності доводиться вибирати між поламаними збереженнями гравців або відмовою від зміни структури даних.
ScriptableObject-архітектура
Для середніх і великих проектів ми використовуємо підхід, популяризований Ryan Hipple на GDC: ScriptableObject як шина подій та контейнер розділюваного стану.
// Змінна-подія
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
private List<GameEventListener> _listeners = new();
public void Raise() {
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised();
}
}
Це дозволяє системам у грі взаємодіяти без прямих посилань одна на одну. PlayerHealth не знає про UI, UI не знає про GameManager — все вони знають тільки про ScriptableObject-события. Проект стає значно легше тестувати та розширювати.
Що входить у послугу
- Проектування та реалізація контроллера персонажа (CharacterController або Rigidbody залежно від жанру)
- Налаштування фізики та системи колізій
- Реалізація AI: State Machine, Behaviour Tree, GOAP — залежно від складності ворогів
- Налаштування навігації: NavMesh, NavMeshAgent, динамічні перешкоди
- Проектування та реалізація системи збережень з версійністю
- Аудит існуючого коду: виявлення архітектурних проблем, рефакторинг вузьких місць
- Code review та написання документації по ігровим системам





