// УРОК 01 — MONOBEHAVIOUR

Unity та MonoBehaviour

Unity — це рушій гри. C# — мова якою ти керуєш цим рушієм. Кожен скрипт у Unity — це клас що успадковує MonoBehaviour, спеціальний Unity-клас з купою вбудованих можливостей.

🎮

Передумова: Цей курс передбачає що ти вже знаєш базовий C# (класи, методи, типи). Якщо ні — спочатку пройди курс "C# з нуля".

Структура Unity скрипта

MyScript.cs
// Unity автоматично додає ці using при створенні скрипта using UnityEngine; // основний namespace Unity using System.Collections; // для Coroutines using System.Collections.Generic; // для List, Dictionary // Клас ОБОВ'ЯЗКОВО має мати ту ж саму назву що й файл! // Файл: MyScript.cs → клас: MyScript public class MyScript : MonoBehaviour { // [SerializeField] — поле видне в інспекторі Unity, // але private (не доступне іншим скриптам) [SerializeField] private float speed = 5f; // public — теж видне в інспекторі public int health = 100; // [HideInInspector] — public але не в інспекторі [HideInInspector] public bool isReady; void Start() { // Виконується ОДИН РАЗ при появі об'єкта на сцені Debug.Log("Скрипт запущено!"); } void Update() { // Виконується КОЖЕН КАДР (зазвичай 60 разів/сек) // Тут основна ігрова логіка } }

Чому назва файлу = назва класу?

Unity знаходить скрипти за назвою файлу і зіставляє їх з класом. Якщо назви не збігаються — скрипт не працюватиме. Це найпоширеніша помилка новачків!

⚠️

Правило Unity: Файл PlayerController.cs → клас public class PlayerController. Завжди однакові назви. Навіть регістр важливий!

Debug.Log — твій найкращий друг

Debugging.cs
// Виводить текст у Console вікно Unity Debug.Log("Звичайне повідомлення"); Debug.LogWarning("Попередження!"); // жовтий трикутник Debug.LogError("Помилка!"); // червоний хрестик // З форматуванням: float hp = 75.5f; Debug.Log($"HP: {hp}"); // "HP: 75.5" // Показати об'єкт: Debug.Log($"Позиція: {transform.position}"); // ВАЖЛИВО: Debug.Log() у Update() = 60 повідомлень/сек! // Використовуй лише для налагодження, видаляй з готового коду.
// КВІЗ

Який атрибут робить приватне поле видимим в Unity Інспекторі?

// УРОК 02 — LIFECYCLE

Lifecycle методи Unity

Unity автоматично викликає певні методи твого MonoBehaviour у визначені моменти. Знати їх порядок — критично важливо для написання правильного коду.

AwakeПерший з усіх. Ще до Start. Ідеально для ініціалізації посилань між компонентами.× 1
OnEnableЩоразу коли об'єкт вмикається. Підписка на події.× N
StartОдин раз перед першим Update. Ініціалізація яка потребує інших компонентів.× 1
UpdateКожен кадр. Основна ігрова логіка. Рух, ввід, таймери.× frame
FixedUpdateФіксований інтервал (50/сек). Фізичні операції з Rigidbody.× fixed
LateUpdateПісля всіх Update. Ідеально для камери що слідкує за гравцем.× frame
OnDisableПри вимкненні. Відписка від подій.× N
OnDestroyПри знищенні об'єкта. Фінальне прибирання.× 1
LifecycleDemo.cs
public class LifecycleDemo : MonoBehaviour { private Rigidbody2D _rb; private float _timer; void Awake() { // GetComponent тут — бо Awake іде до Start інших скриптів _rb = GetComponent<Rigidbody2D>(); } void Start() { // Тут всі скрипти вже ініціалізовані, можна посилатись на них _timer = 0f; Debug.Log("Гравець готовий!"); } void Update() { // Time.deltaTime — час між кадрами (~0.016 сек при 60fps) // ЗАВЖДИ множ на deltaTime якщо хочеш fps-незалежний рух! _timer += Time.deltaTime; } void FixedUpdate() { // Фізика завжди тут, не в Update! _rb.AddForce(Vector2.up * 5f); } void LateUpdate() { // Камера слідує за гравцем — після того як гравець вже рухнувся } }

Time.deltaTime — ключ до fps-незалежності

DeltaTime.cs
// БЕЗ deltaTime — рух залежить від fps: transform.Translate(Vector3.right * 0.1f); // На 60fps: 0.1 × 60 = 6 units/сек // На 30fps: 0.1 × 30 = 3 units/сек ← вдвічі повільніше! // З deltaTime — однаково на будь-якому fps: transform.Translate(Vector3.right * speed * Time.deltaTime); // На 60fps: speed × 0.0166 × 60 = speed units/сек // На 30fps: speed × 0.0333 × 30 = speed units/сек ← однаково! // Таймер: private float _cooldown = 0f; void Update() { _cooldown -= Time.deltaTime; if (_cooldown <= 0f && Input.GetKeyDown(KeyCode.Space)) { Shoot(); _cooldown = 0.5f; // 0.5 секунди між пострілами } }
// КВІЗ

Де правильно застосовувати фізичні операції з Rigidbody?

// УРОК 03 — TRANSFORM

Transform та Vector3

Transform — це компонент є в КОЖНОМУ GameObject у Unity. Він зберігає позицію, поворот та масштаб. Без нього неможливо нічого розмістити або зрушити у сцені.

Vector3 та Vector2

Vectors.cs
// Vector3 — struct з трьох float: x, y, z Vector3 pos = new Vector3(1f, 2f, 0f); Vector3 dir = new Vector3(0f, 1f, 0f); // вгору // Зручні константи: Vector3.zero // (0, 0, 0) Vector3.one // (1, 1, 1) Vector3.up // (0, 1, 0) Vector3.down // (0, -1, 0) Vector3.left // (-1, 0, 0) Vector3.right // (1, 0, 0) Vector3.forward // (0, 0, 1) — тільки для 3D // Арифметика: Vector3 a = new Vector3(1, 0, 0); Vector3 b = new Vector3(0, 1, 0); Vector3 c = a + b; // (1, 1, 0) Vector3 d = a * 5f; // (5, 0, 0) // Довжина вектора (відстань від origin): float len = a.magnitude; // 1.0 // Нормалізований — той самий напрям, довжина = 1: Vector3 norm = a.normalized; // Відстань між двома точками: float dist = Vector3.Distance(a, b); // ~1.41

Маніпуляції з Transform

TransformOps.cs
// Отримати позицію (повертає КОПІЮ Vector3, бо struct!) Vector3 pos = transform.position; // Встановити позицію (завжди assign цілий Vector3): transform.position = new Vector3(5f, 0f, 0f); // ❌ transform.position.x = 5f; → НЕ ПРАЦЮЄ (struct = копія) // ✓ var p = transform.position; p.x = 5f; transform.position = p; // Переміщення відносно поточної позиції: transform.Translate(Vector3.right * speed * Time.deltaTime); // Поворот: transform.rotation = Quaternion.Euler(0f, 90f, 0f); // 90° по Y transform.Rotate(0f, 45f * Time.deltaTime, 0f); // обертатись transform.LookAt(target.transform); // дивитись на об'єкт // Масштаб: transform.localScale = new Vector3(2f, 2f, 2f); // вдвічі більший // Плавне переміщення (Lerp): transform.position = Vector3.Lerp( transform.position, targetPosition, 5f * Time.deltaTime // швидкість наближення );
⚠️

Vector3 — це struct! Пам'ятаєш урок про struct? transform.position повертає КОПІЮ. Тому transform.position.x = 5f; змінює копію, не оригінал. Завжди присвоюй цілий Vector3.

// КВІЗ

Чому transform.position.x = 5f; не працює в Unity?

// УРОК 04 — INPUT

Input — клавіатура і миша

Unity має два способи отримати ввід від гравця: старий Input (простий) та новий Input System (гнучкий). Починаємо зі старого — він є в кожному проекті за замовчуванням.

InputBasics.cs
// ══ КЛАВІАТУРА ══ // GetKey — утримується кнопка (кожен кадр поки натиснута) if (Input.GetKey(KeyCode.W)) MoveForward(); // GetKeyDown — тільки в момент натискання (один кадр) if (Input.GetKeyDown(KeyCode.Space)) Jump(); // GetKeyUp — тільки в момент відпускання if (Input.GetKeyUp(KeyCode.LeftShift)) StopSprint(); // ══ ОСІ (плавне значення -1 до 1) ══ float h = Input.GetAxis("Horizontal"); // A/D або ←/→ float v = Input.GetAxis("Vertical"); // W/S або ↑/↓ // Значення: -1 (ліво/низ), 0 (нічого), 1 (право/верх) // Плавно наростає/спадає (є інерція) // GetAxisRaw — без інерції, тільки -1, 0, 1: float rawH = Input.GetAxisRaw("Horizontal"); // ══ МИША ══ // Кнопки: 0=ліва, 1=права, 2=середня if (Input.GetMouseButtonDown(0)) Shoot(); // Позиція миші на екрані (пікселі): Vector3 mousePos = Input.mousePosition; // Позиція миші у світі (2D): Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);

Платформер — повний контролер руху

PlatformerMove.cs
public class PlatformerMove : MonoBehaviour { [SerializeField] private float moveSpeed = 5f; [SerializeField] private float jumpForce = 10f; [SerializeField] private LayerMask groundLayer; private Rigidbody2D _rb; private bool _isGrounded; void Awake() => _rb = GetComponent<Rigidbody2D>(); void Update() { // Горизонтальний рух float input = Input.GetAxisRaw("Horizontal"); _rb.velocity = new Vector2(input * moveSpeed, _rb.velocity.y); // Стрибок — тільки якщо на землі if (Input.GetKeyDown(KeyCode.Space) && _isGrounded) { _rb.velocity = new Vector2(_rb.velocity.x, jumpForce); _isGrounded = false; } // Flip спрайту при повороті if (input != 0) transform.localScale = new Vector3( input, 1, 1 // від'ємний x = дзеркало ); } void OnCollisionEnter2D(Collision2D col) { if (col.gameObject.layer == LayerMask.NameToLayer("Ground")) _isGrounded = true; } }
// КВІЗ

Яка функція повертає true лише в ОДИН кадр — у момент натискання кнопки?

// УРОК 05 — PHYSICS

Rigidbody та фізика

Rigidbody дозволяє Unity фізичному рушієві (PhysX/Box2D) керувати об'єктом: гравітація, зіткнення, сили. Замість вручну рахувати траєкторію — додаєш Rigidbody і Unity робить це сам.

PhysicsBasics.cs
public class PhysicsBasics : MonoBehaviour { private Rigidbody2D _rb; void Awake() => _rb = GetComponent<Rigidbody2D>(); void FixedUpdate() { // ══ ШВИДКІСТЬ (velocity) ══ // Прямо задати швидкість: _rb.velocity = new Vector2(3f, _rb.velocity.y); // Зберігаємо .y — щоб не скасувати гравітацію! // ══ СИЛА (force) ══ // Поступово прискорює (реалістично): _rb.AddForce(Vector2.right * 10f); // Миттєвий поштовх (стрибок, вибух): _rb.AddForce(Vector2.up * 500f, ForceMode2D.Impulse); // ══ TORQUE — обертальна сила ══ _rb.AddTorque(5f); // ══ ЗАМОРОЗИТИ ОСЬ ══ // Через інспектор або: _rb.constraints = RigidbodyConstraints2D.FreezeRotation; } void Update() { // Читати velocity можна в Update: Debug.Log($"Швидкість: {_rb.velocity.magnitude}"); // Обмежити максимальну швидкість: if (_rb.velocity.magnitude > 10f) _rb.velocity = _rb.velocity.normalized * 10f; } }
💡

Rigidbody2D vs Rigidbody: Для 2D ігор — Rigidbody2D та Vector2. Для 3D — Rigidbody та Vector3. Не мішай їх!

Raycast — промінь запиту

Raycast.cs
// Raycast = невидимий промінь, перевіряє що на шляху // 2D: чи є земля під гравцем? RaycastHit2D hit = Physics2D.Raycast( transform.position, // звідки Vector2.down, // напрям 0.1f, // довжина groundLayer // тільки шар "Ground" ); bool isGrounded = hit.collider != null; // Відлагодження — побачити промінь у Scene view: Debug.DrawRay(transform.position, Vector2.down * 0.1f, Color.red);
// УРОК 06 — COLLISIONS

Колізії та тригери

Collider — фізична форма об'єкта (прямокутник, коло, тощо). Тригер — Collider без фізичного блокування, лише визначення перекриття.

МетодКоли викликаєтьсяТип
OnCollisionEnter2DФізичне зіткнення почалосьCollision
OnCollisionStay2DЗіткнення триваєCollision
OnCollisionExit2DЗіткнення завершилосьCollision
OnTriggerEnter2DОб'єкт увійшов у тригерTrigger
OnTriggerStay2DОб'єкт всередині тригераTrigger
OnTriggerExit2DОб'єкт вийшов з тригераTrigger
CollisionDemo.cs
public class CollisionDemo : MonoBehaviour { // ══ COLLISION — фізичне зіткнення ══ void OnCollisionEnter2D(Collision2D col) { // col.gameObject — об'єкт з яким зіткнулись Debug.Log($"Зіткнення з: {col.gameObject.name}"); // Перевірка тегу — у Unity кожен об'єкт має тег: if (col.gameObject.CompareTag("Enemy")) { TakeDamage(10); } // Перевірка шару: if (col.gameObject.layer == LayerMask.NameToLayer("Ground")) { _isGrounded = true; } // Точка та нормаль зіткнення: ContactPoint2D contact = col.contacts[0]; Debug.Log($"Точка: {contact.point}"); } // ══ TRIGGER — зона без фізики ══ void OnTriggerEnter2D(Collider2D other) { // Підбір предмета: if (other.CompareTag("Coin")) { coins++; Destroy(other.gameObject); // знищити монету } // Портал — телепортація: if (other.CompareTag("Portal")) { transform.position = spawnPoint.position; } } private bool _isGrounded; private int coins; [SerializeField] private Transform spawnPoint; private void TakeDamage(int dmg) { } }
💡

CompareTag vs ==: Завжди використовуй CompareTag("Enemy") замість tag == "Enemy". CompareTag ефективніший і не генерує garbage collection.

// КВІЗ

Яку різницю між Collision і Trigger подіями?

// УРОК 07 — GETCOMPONENT

GetComponent та посилання між об'єктами

GetComponent — один з найважливіших методів Unity. Він дозволяє одному скрипту знайти та звернутись до іншого компонента або скрипта на тому ж або іншому GameObject.

GetComponentDemo.cs
public class PlayerAttack : MonoBehaviour { // ══ НА ТОМУ Ж GAMEOBJECT ══ private Rigidbody2D _rb; private Animator _anim; private PlayerHealth _health; // наш власний скрипт! void Awake() { // Кешуємо в Awake — GetComponent дорогий виклик! _rb = GetComponent<Rigidbody2D>(); _anim = GetComponent<Animator>(); _health = GetComponent<PlayerHealth>(); } // ══ НА ДОЧІРНЬОМУ ОБ'ЄКТІ ══ [SerializeField] private Transform weaponHolder; private SpriteRenderer _weaponSprite; void Start() { _weaponSprite = weaponHolder.GetComponent<SpriteRenderer>(); } // ══ НА БАТЬКІВСЬКОМУ ОБ'ЄКТІ ══ void FindParentScript() { var parent = GetComponentInParent<CharacterStats>(); } // ══ ЗНАЙТИ ПО СЦЕНІ ══ void FindOnScene() { // Знайти ОДИН об'єкт (повільно, не в Update!): GameManager gm = FindObjectOfType<GameManager>(); // Знайти за тегом: GameObject player = GameObject.FindWithTag("Player"); // Знайти за ім'ям: GameObject obj = GameObject.Find("SpawnPoint"); } void Attack() { // Використовуємо закешований компонент: _anim.SetTrigger("Attack"); Debug.Log($"HP перед атакою: {_health.CurrentHP}"); } }
⚠️

Кешуй GetComponent! Виклик GetComponent() в Update() кожен кадр — це повільно. Завжди кешуй у Awake() або Start() в приватну змінну.

// УРОК 08 — COROUTINES

Coroutines — асинхронні задачі

Coroutine — метод що може "призупинитись" і продовжитись пізніше. Ідеально для анімацій, таймерів, spawn хвиль ворогів — всього що потребує затримки без блокування гри.

Coroutines.cs
public class CoroutineDemo : MonoBehaviour { void Start() { // Запустити coroutine: StartCoroutine(SpawnWave()); StartCoroutine(FlashRed()); // Зупинити всі: // StopAllCoroutines(); // Зупинити конкретну: Coroutine c = StartCoroutine(SpawnWave()); StopCoroutine(c); } // ══ Coroutine ЗАВЖДИ повертає IEnumerator ══ IEnumerator SpawnWave() { Debug.Log("Хвиля 1..."); SpawnEnemy(); yield return new WaitForSeconds(2f); // ← чекаємо 2 секунди Debug.Log("Хвиля 2..."); SpawnEnemy(); SpawnEnemy(); yield return new WaitForSeconds(3f); Debug.Log("Боса хвиля!"); SpawnBoss(); } // Мигання при пораненні: IEnumerator FlashRed() { SpriteRenderer sr = GetComponent<SpriteRenderer>(); for (int i = 0; i < 3; i++) { sr.color = Color.red; yield return new WaitForSeconds(0.1f); sr.color = Color.white; yield return new WaitForSeconds(0.1f); } } // Типи yield: IEnumerator YieldTypes() { yield return new WaitForSeconds(1f); // чекати секунди yield return new WaitForFixedUpdate(); // чекати FixedUpdate yield return new WaitForEndOfFrame(); // кінець кадру yield return null; // наступний кадр yield return new WaitUntil(() => _hp <= 0); // до умови } private int _hp = 100; private void SpawnEnemy() {} private void SpawnBoss() {} }
// КВІЗ

Який тип повертає метод-coroutine?

// УРОК 09 — SCRIPTABLEOBJECTS

ScriptableObjects

ScriptableObject — спеціальний клас Unity для зберігання даних незалежно від сцени. Замість жорстко прописаних значень у коді — редаговані asset файли в Unity Editor.

WeaponData.cs
using UnityEngine; // CreateAssetMenu дозволяє створити цей SO через контекстне меню Unity [CreateAssetMenu(fileName = "NewWeapon", menuName = "Game/Weapon")] public class WeaponData : ScriptableObject { public string weaponName = "Меч"; public int damage = 25; public float attackSpeed = 1.0f; public float range = 1.5f; public Sprite icon; public AudioClip attackSound; } // ══════════════════════════════════════════ // Використання у скрипті гравця: public class PlayerWeapon : MonoBehaviour { // Перетягни SO asset прямо в інспекторі! [SerializeField] private WeaponData currentWeapon; void Attack() { Debug.Log($"Атака {currentWeapon.weaponName}: {currentWeapon.damage} шкоди"); // Тепер зміна damage відбувається в Unity Editor, // а не в коді — дизайнеру не треба вміти програмувати! } void SwapWeapon(WeaponData newWeapon) { currentWeapon = newWeapon; Debug.Log($"Змінили на: {newWeapon.weaponName}"); } }
🎮

Коли використовувати SO? Статичні дані що не змінюються під час гри: характеристики зброї, предметів, персонажів, рівнів. Це відокремлює дані від логіки — золоте правило хорошої архітектури!

// УРОК 10 — EVENTS

Events та делегати

Events (події) — механізм сповіщення. Один об'єкт "видає" подію, інші "слухають" і реагують. Це основа слабкого зв'язку між компонентами — кожен знає тільки що потрібно знати.

Events.cs
using System; using UnityEngine; using UnityEngine.Events; // ══ C# EVENTS ══ public class PlayerHealth : MonoBehaviour { // Action — делегат без параметрів або з параметрами public static event Action OnPlayerDied; public static event Action<int> OnHealthChanged; // передає int public static event Action<int, int> OnDamaged; // передає 2 int private int _hp = 100; public void TakeDamage(int dmg) { _hp -= dmg; OnHealthChanged?.Invoke(_hp); // ?. = null-безпечний виклик OnDamaged?.Invoke(dmg, _hp); if (_hp <= 0) { _hp = 0; OnPlayerDied?.Invoke(); } } } // ══ ПІДПИСКА НА ПОДІЇ ══ public class UIHealthBar : MonoBehaviour { void OnEnable() { // += для підписки PlayerHealth.OnHealthChanged += UpdateBar; PlayerHealth.OnPlayerDied += ShowGameOver; } void OnDisable() { // -= для відписки (ОБОВ'ЯЗКОВО щоб уникнути memory leak!) PlayerHealth.OnHealthChanged -= UpdateBar; PlayerHealth.OnPlayerDied -= ShowGameOver; } void UpdateBar(int newHp) { Debug.Log($"UI оновлено: {newHp} HP"); // оновити полоску здоров'я на екрані } void ShowGameOver() { Debug.Log("Game Over!"); } } // ══ UNITYEVENT — для Inspector ══ public class Button3D : MonoBehaviour { // UnityEvent видно і налаштовується прямо в Інспекторі! public UnityEvent onPressed; void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag("Player")) onPressed?.Invoke(); // викличе все що прив'язано в Інспекторі } }
// КВІЗ

Чому важливо відписатись від події (-=) в OnDisable?

// УРОК 11 — FINAL PROJECT

🎮 Фінал: архітектура реальної гри

Зведемо все разом. Ось повна архітектура простого 2D платформера що використовує всі концепції курсу: lifecycle, фізику, колізії, coroutines та events.

GameManager.cs — серце гри
using UnityEngine; using UnityEngine.SceneManagement; using System.Collections; using System.Collections.Generic; using System.Linq; // Singleton — один GameManager на всю гру public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } [Header("Game Settings")] [SerializeField] private int maxLives = 3; [SerializeField] private float respawnDelay = 2f; [SerializeField] private GameObject enemyPrefab; [SerializeField] private Transform[] spawnPoints; private int _score; private int _lives; private List<GameObject> _activeEnemies = new(); // Events для UI та інших систем public static event Action<int> OnScoreChanged; public static event Action<int> OnLivesChanged; public static event Action OnGameOver; void Awake() { // Singleton pattern if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // зберегти між сценами } void Start() { _lives = maxLives; PlayerHealth.OnPlayerDied += HandlePlayerDeath; StartCoroutine(SpawnLoop()); } void OnDestroy() { PlayerHealth.OnPlayerDied -= HandlePlayerDeath; } public void AddScore(int points) { _score += points; OnScoreChanged?.Invoke(_score); // LINQ: видалити мертвих ворогів зі списку _activeEnemies = _activeEnemies .Where(e => e != null) .ToList(); } private void HandlePlayerDeath() { _lives--; OnLivesChanged?.Invoke(_lives); if (_lives <= 0) { OnGameOver?.Invoke(); StartCoroutine(LoadGameOver()); } else { StartCoroutine(RespawnPlayer()); } } IEnumerator RespawnPlayer() { yield return new WaitForSeconds(respawnDelay); SceneManager.LoadScene(SceneManager.GetActiveScene().name); } IEnumerator LoadGameOver() { yield return new WaitForSeconds(2f); SceneManager.LoadScene("GameOver"); } IEnumerator SpawnLoop() { while (true) { yield return new WaitForSeconds(5f); if (_activeEnemies.Count < 5) { // Рандомна точка спавну: Transform spawn = spawnPoints[Random.Range(0, spawnPoints.Length)]; GameObject enemy = Instantiate(enemyPrefab, spawn.position, Quaternion.identity); _activeEnemies.Add(enemy); } } } }

Структура проекту

📁
Scripts/Player/

PlayerController.cs, PlayerHealth.cs, PlayerAttack.cs — все що стосується гравця

📁
Scripts/Enemies/

EnemyBase.cs (abstract), Goblin.cs, Orc.cs — наслідування в дії

📁
Scripts/Managers/

GameManager.cs, UIManager.cs, AudioManager.cs — сервіси гри

📁
ScriptableObjects/

WeaponData.cs, EnemyStats.cs, LevelConfig.cs — дані без логіки

📁
Prefabs/

Player.prefab, Goblin.prefab, Coin.prefab — готові ігрові об'єкти

🏆

Вітаємо з закінченням курсу Unity! Ти вивчив: MonoBehaviour, lifecycle, Transform/Vector3, Input, Rigidbody, колізії, GetComponent, Coroutines, ScriptableObjects та Events. Це вже достатньо щоб зробити свою першу справжню гру!

Наступні кроки

1
Зроби свою першу гру

Починай з малого: Pong, Snake, або простий платформер. Завершена маленька гра цінніша за незакінчений шедевр.

2
Вивчи Animator Controller

Анімації — важлива частина Unity. State Machine для анімацій повністю аналогічна до звичайної state machine в коді.

3
UI з Canvas та TextMeshPro

Кожна гра потребує меню, HUD, інвентар. Canvas + TMP — стандарт Unity.

4
Патерни: Singleton, Observer, State

Наступний курс в Academy — архітектура ігор та патерни проєктування.