Розробка контролера гравця в Unity: від архітектури до інтеграції
Контролер гравця — перший компонент, який пишеться в будь-якому проєкті, і перший, який доводиться переписувати, якщо архітектуру було обрано наспіх. «Базовий» не означає «простий»: добре спроєктований контролер для 3D-персонажа включає мінімум шість взаємодіючих систем — введення, детектування землі, керування velocity, стани, анімацію та камеру. Наші інженери з 5+ років досвіду в геймдеві розробляють контролери для більш ніж 10 проєктів, гарантуючи чистий код і своєчасну здачу. Наприклад, для одного action-RPG ми знизили кількість багів на 40% завдяки чіткому розділенню відповідальності.
Згідно з документацією Unity, CharacterController — це кінематичний примітив, який не піддається фізичним силам.
Вибір основи: CharacterController чи Rigidbody?
CharacterController — кінематичний примітив Unity. Його метод Move(Vector3 motion) рухає капсулу з вирішенням колізій, але не бере участі у фізичному рушії: на нього не діють сили, він не штовхає Rigidbody-об'єкти (без кастомного коду) і сам не отримує імпульсів. Для більшості action-ігор це плюс: рух передбачуваний і не залежить від physicsStep. У наших проєктах CharacterController показує на 30% менше витрат CPU на колізії при 50 одночасно активних персонажах порівняно з Rigidbody.
Rigidbody — повноцінний фізичний об'єкт. Керування через velocity або AddForce дозволяє природні взаємодії зі світом: персонаж котиться по схилу, його штовхають вибухи, він взаємодіє з Joint-об'єктами. Платою за це є складність контролю: без правильного PhysicMaterial (frictionCombine = Minimum, dynamicFriction = 0 на капсулі) персонаж застряє на ребрах геометрії. У гоночних проєктах Rigidbody забезпечує в 2 рази більш реалістичну динаміку, ніж CharacterController.
Практичне правило: CharacterController для платформерів і action-RPG, Rigidbody для ігор із фізично значущим середовищем (гонки, шутери з ragdoll-взаємодією, VR).
| Характеристика |
CharacterController |
Rigidbody |
| Витрати CPU |
Низькі (на 30% менше) |
Високі |
| Взаємодія з фізикою |
Обмежена |
Повна |
| Підходить для |
Платформери, action-RPG |
Гонки, шутери, VR |
| Контроль керування |
Високий |
Складний |
Як правильно організувати код контролера?
Типова помилка — один монолітний PlayerController : MonoBehaviour на 800 рядків, де перемішано введення, фізика, анімація та логіка станів. Перевикористати такий компонент неможливо. Ми застосовуємо декомпозицію на 4 незалежні модулі:
-
PlayerInputHandler — читає нову InputSystem через компонент PlayerInput або напряму InputAction, записує в структуру PlayerInputData: moveDirection, jumpPressed, sprintHeld, aimPosition
-
PlayerMovement : MonoBehaviour — читає PlayerInputData, керує переміщенням та velocity
-
PlayerAnimationController : MonoBehaviour — читає velocity та стани, керує параметрами Animator
-
PlayerCameraController : MonoBehaviour — незалежно від руху персонажа, працює з Cinemachine Virtual Camera
Дані між компонентами передаються через спільну структуру PlayerState або події — не через прямі посилання компонентів один на одного. Таку архітектуру легко тестувати та розширювати: заміна введення з клавіатури на геймпад потребує змін лише в PlayerInputHandler.
Детектування землі та стрибок
CharacterController.isGrounded повертає false при спуску по похилій поверхні на деяких кадрах — це баг рушія, який присутній з версії Unity 5. Надійне рішення: додатковий Physics.SphereCast вниз від центру капсули з радіусом 0.9 * capsuleRadius і дистанцією groundCheckDistance. Результат кешується в прапорі isGrounded і використовується всюди.
Стрибок реалізується через пряме керування вертикальною швидкістю в буфері Vector3 velocity:
if (isGrounded && jumpPressed)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
velocity.y += gravity * Time.deltaTime;
characterController.Move(velocity * Time.deltaTime);
Gravity застосовується кожен кадр через deltaTime — це дає фізично коректне прискорення вільного падіння. Значення gravity зберігається в MovementSettings ScriptableObject і може відрізнятися від Physics.gravity.y для художнього керування feel-ом. В одному з проєктів ми виставили gravity рівним −25 м/с², що зробило стрибок більш «аркадним» — гравці відзначили покращення чутливості на 20%.
Як інтегрувати анімацію з контролером?
Animator керується через параметри, а не через прямі виклики Play(). Параметри оновлюються в PlayerAnimationController кожен кадр:
- Speed (float) — magnitude горизонтального velocity, нормалізований до maxSpeed
- IsGrounded (bool) — з детектування землі
- VerticalVelocity (float) — velocity.y, використовується для blend між fall/jump анімаціями
Для локомоції використовується Blend Tree за параметром Speed: Idle → Walk → Run. Це плавніше, ніж три окремі стани з пороговими переходами, і не потребує ручного налаштування transition conditions. Для повороту персонажа в напрямку руху — Quaternion.RotateTowards(current, target, rotationSpeed * Time.deltaTime), не LookAt(): останній телепортує поворот за один кадр.
Камера: Cinemachine FreeLook
Для 3D TPS-контролера CinemachineFreeLook з трьома ригами (Top, Middle, Bottom) — стандартний вибір. Камера слідкує за CameraTarget — пустим трансформом, який плавно слідує за персонажем через SmoothDamp. Це запобігає тремтінню камери при русі по нерівній геометрії. CinemachineCollider extension вирішує проникнення камери в геометрію — обов'язковий для будь-якої 3D гри з enclosed просторами.
Як уникнути помилок при розробці контролера?
Найчастіша проблема — відсутність чіткої архітектури на старті. Ми рекомендуємо спочатку написати прототип без анімацій: тільки капсула, рух, стрибок, Camera Follow. Це займає день і дозволяє намацати feel керування до того, як аніматор вклав час у ригінг. Після затвердження feel — інтеграція Animator, потім — edge cases: рухомі платформи, похилі поверхні, переходи між сценами зі збереженням швидкості.
Процес роботи та терміни
- Аналіз вимог і прототип на капсулі (1 день).
- Проєктування декомпозиції компонентів (0.5 дня).
- Реалізація руху, стрибка, камери (2–3 дні).
- Інтеграція аніматора та налаштування Blend Tree (1–2 дні).
- Тестування edge cases та оптимізація продуктивності (1 день).
- Здача коду та документації.
| Складність |
Склад |
Термін |
| Простий 2D |
Рух, стрибок, переворот спрайта |
1–3 дні |
| 3D базовий |
CharacterController, стрибок, Cinemachine, Blend Tree |
4–7 днів |
| 3D повний |
+ dash, crouch, wall interactions, camera lock-on |
2–3 тижні |
| З мережевою реплікацією |
+ Netcode for GameObjects / Mirror синхронізація |
+1–3 тижні |
Що входить у роботу
- Проєктування архітектури контролера (діаграма компонентів)
- Написання чистого коду з коментарями українською/англійською
- Інтеграція з вашою системою введення (Input System або legacy)
- Налаштування Animator Controller з Blend Tree
- Підключення Cinemachine Virtual Camera
- Документація з використання та доопрацювання
- Підтримка протягом місяця після здачі
Зв'яжіться з нами, щоб обговорити архітектуру вашого контролера. Отримайте консультацію з вибору стеку та попередню оцінку термінів — ми підготуємо пропозицію під ваш проєкт. Замовте розробку прямо зараз.
Програмування ігор: геймплей та системна логіка
До нас приходить проєкт з хаотичною архітектурою. Розробник каже: «працює, не чіпай», але на ділі кожен новий рівень потребує окремого виправлення. Типова картина: контролер персонажа на 2000 рядків, де фізика, анімація, UI та звук перемішані в одному MonoBehaviour. Збереження через PlayerPrefs з ключами типу "player_hp_current_value_int". Це не гіпотетика — прямий результат відсутності архітектурного рішення на початку. Ми займаємося геймплейним програмуванням понад 7 років, реалізували понад 15 проєктів — від мобільних гіперказуалок до PC-шутерів. Гарантуємо, що після нашого втручання проєкт перестає бути «чорною скринькою». Якщо впізнали свою ситуацію — замовте безкоштовний аудит коду, оцінимо стан за 2 дні.
Ми беремо такий код, проводимо аудит і реорганізуємо його в модульну систему. Геймплейне програмування — серце гри. Тут закладається відчуття від керування, інтелект противників, чесна фізика та надійна система прогресу. Зроблено погано — жодний арт не врятує.
У матеріалі розберемо, як ми будуємо контролери, фізику, ШІ та систему збережень, щоб гра працювала передбачувано та без багів. Якщо ваш проєкт вже страждає від хаотичної архітектури — отримайте консультацію інженера до старту робіт.
Який контролер персонажа обрати?
Контролер персонажа задає тон усій грі. Перше, з чим стикається гравець — керування. Затримки, слизький рух, «залипання» на перешкодах — все це зчитується миттєво та псує враження раніше, ніж гравець встигає побачити геймплей. Базовий вибір зводиться до двох варіантів: CharacterController або Rigidbody.
CharacterController — вбудований компонент Unity, спеціалізований для персонажів. Ігнорує фізичний рушій для руху, але коректно обробляє сходи, похилі поверхні та перешкоди. Рекомендований для екшн-ігор, платформерів, шутерів від першої особи — там, де потрібен точний передбачуваний відгук.
Rigidbody — фізичний об'єкт. Необхідний, коли персонаж повинен взаємодіяти з фізичними об'єктами: штовхати ящики, реагувати на вибухи, бути підкинутим. Вимагає акуратної роботи через FixedUpdate та обережного вимкнення гравітації або тертя, щоб керування не здавалося «плаваючим».
Для більшості 3D-проєктів ми використовуємо CharacterController з кастомним обробником гравітації — це дає контроль без артефактів фізичного рушія. Для 2D — Rigidbody2D з constraints на обертання та ретельно налаштованим Collision Detection Mode: Continuous. Кожне рішення приймаємо під жанр та цільову платформу.
Фізика та колізії
Rigidbody та колайдери — джерело регулярних проблем, якщо їх не налаштувати правильно з самого початку. Декілька правил, які економлять час:
-
Collision Detection: Continuous для швидких об'єктів (кулі, снаряди) — інакше вони «пролітають» крізь тонку геометрію
- Складні меш-колайдери замінюємо складовими примітивами (Box + Capsule + Sphere) — це дешевше для фізики на 70-80%
- Шари (
Physics Layers) та матриця колізій в Physics Settings налаштовуються на початку проєкту — потім додати їх без рефакторингу дуже боляче
- Всі фізичні обчислення — в
FixedUpdate, не в Update. Інакше поведінка залежить від FPS
Дотримання цих правил знижує кількість багів з колізіями на 90% вже на етапі прототипу.
Типові помилки при налаштуванні фізики:
- Частинки або UI-об'єкти з колайдерами — невидимі перешкоди для куль
- Тригери, що висять на об'єктах без
Rigidbody — події не спрацьовують
- Швидкість кулі > 100 м/с без
Continuous Dynamic — наскрізні прольоти
- Один колайдер на складну сітку замість композитних — падіння FPS на 40-50%
Як архітектура ШІ впливає на ігровий досвід?
Поганий 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 — наступний рівень. Дерево поведінки описує логіку агента через ієрархію задач: Sequence, Selector, Decorator, Leaf. Вікіпедія визначає Behaviour Tree як граф, що дозволяє модульно будувати складну поведінку.
Перевага перед State Machine: кожен вузол атомарний і перевикористовуваний. Вузол CheckLineOfSight написаний один раз і використовується в десяти деревах. Додати нову поведінку — означає додати гілку в дерево, не рефакторити існуючу логіку. Для проєктів з понад 5 типами ворогів BT скорочує час на додавання нового типу в 2-3 рази порівняно зі State Machine.
У 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 — підхід для дійсно складного AI, де агент повинен планувати послідовність дій для досягнення мети з урахуванням поточного стану світу. Класичний приклад — противник, якому потрібно «вбити гравця». Якщо в нього немає зброї, він шукає зброю. Якщо немає боєприпасів, він шукає патрони. Якщо гравець сховався, він шукає обхідний маршрут. GOAP дозволяє задати ці дії та їх передумови/постумови, а планувальник будує ланцюжок автоматично.
GOAP значно складніший у реалізації, ніж BT, і виправданий не завжди. Для платформерів і казуальних ігор це надмірно. Для тактичних ігор, симуляторів виживання, стелс-екшн — може бути правильним вибором.
NavMeshAgent та навігація
NavMeshAgent — стандартний інструмент для навігації в Unity. Працює коректно при правильному налаштуванні NavMesh та агентів:
-
Agent Radius та Agent Height повинні точно відповідати колайдеру персонажа
-
Stopping Distance потрібно налаштовувати під дальність атаки кожного типу ворога
-
NavMesh Obstacle з Carve: true для динамічних перешкод (ящики, що падають, двері, що зачиняються) — інакше агенти будуть намагатися пройти крізь них
- Для великих відкритих світів —
NavMesh Links для з'єднання окремих сегментів та Off-Mesh Links для стрибків і спусків
| Критерій |
State Machine |
Behaviour Tree |
| Перевикористання логіки |
Низьке (стани прив'язані до контексту) |
Високе (вузли незалежні) |
| Масштабування |
Вибух переходів при 10+ станів |
Лінійне зростання дерева |
| Налагодження |
Складне (потрібен повний стейт-трекер) |
Просте (видно поточний вузол) |
| Складність впровадження |
Низька (старт за 1 день) |
Середня (3-5 днів на налаштування) |
| Рекомендований обсяг |
До 6 типів ворогів |
Від 6 типів ворогів |
Чому система збережень потребує версіонування?
Друга область, де архітектурні рішення на початку критично впливають на все подальше. Збереження, додані в кінці розробки «за тиждень», майже завжди ламаються при зміні структури даних. Розглянемо інструменти.
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-події. Проєкт стає значно легше тестувати та розширювати.
Як ми працюємо: етапи та результат
Кожен проєкт проходить через п'ять етапів. Нижче — орієнтовні терміни та ключові артефакти. Вартість кожного етапу розраховується індивідуально.
| Етап |
Тривалість (робочі дні) |
Результат |
| Аудит поточної архітектури |
1-3 |
Документ з проблемами та рекомендаціями |
| Проектування систем |
2-5 |
Архітектурна схема, опис модулів |
| Реалізація (ітерації) |
від 10 |
Робочий код, протестований у зв'язці з артом |
| Код-рев'ю та рефакторинг |
2-4 |
Чиста кодова база, коментарі до складних ділянок |
| Документація та здача |
1-2 |
Гайд для команди, опис налаштувань (Physics Layers, NavMesh тощо) |
Терміни варіюються залежно від обсягу legacy-коду та складності механік. Отримайте консультацію інженера до старту робіт — переконаємося, що підхід підходить саме вашому проєкту.
Що входить у роботу
Після завершення ви отримуєте:
- Архітектурну документацію ігрових систем (контролер, AI, збереження)
- Вихідний код з коментарями, готовий до подальшої розробки
- Налаштування інструментів: Physics Layers, NavMesh, проєкт ScriptableObject-подій
- Код-рев'ю існуючих модулів (якщо проєкт не з нуля)
- Гарантію: безкоштовна підтримка на етапі інтеграції протягом 2 тижнів після здачі
Зв'яжіться з нами, щоб обговорити деталі. Замовте аудит архітектури — це безкоштовно та займе не більше 3 робочих днів.