Unity з нуля


Kanal geosi va tili: Ukraina, Ukraincha


🎮 Вивчаємо Unity з нуля - просто і без зайвої теорії
Тут ти знайдеш:
• покрокові уроки
• практичні приклади
• розбір типових помилок
• поради для новачків
Почни свій шлях у геймдеві вже сьогодні 🚀

Bog‘liq kanallar

Kanal geosi va tili
Ukraina, Ukraincha
Statistika
Postlar filtri


🎮 Четверта частина створення нескінченного ранера вже на YouTube

В цьому відео ми займемось візуальною частиною гри. Зробимо туман за допомогою particle system та будинки по краях карти

Приємного перегляду👇
https://youtu.be/IdJl9mNfpdU






🎮 Я розпочав на YouTube нову серію уроків по створенню нескінченного 3D ранера в Unity

У цій серії ми поступово створимо гру з нуля і в кінці опублікуємо її в Google Play

Перша частина вже вийшла - у ній підготуємо проект до створення основних механік

Приємного перегляду👇
https://youtu.be/-fu2xhdjiOQ




Кешування в unity. Що це таке і навіщо воно потрібне?

Під час розробки ігор часто виникає ситуація, коли нам потрібно багато разів отримувати одні й ті самі дані або звертатися до одних і тих самих об'єктів

Наприклад, нам потрібно постійно отримувати компонент:
GetComponent()
Якщо це робити у методі Update, тобто кожного кадру, це створить зайве навантаження на процесор. Тут і приходить на допомогу кешування

🔹 Що таке кешування?

Кешування - це збереження результату операції, щоб не виконувати її повторно

Наприклад, замість того щоб кожного разу шукати Rigidbody, ми можемо отримати його один раз і зберегти:
private Rigidbody _rb;

private void Awake()
{
_rb = GetComponent();
}
А потім використовувати вже збережене посилання:
_rb.AddForce(Vector3.up);

Тобто замість:
Знайти компонент → використати
Знайти компонент → використати
Знайти компонент → використати

ми робимо:
Знайти компонент → зберегти → використовувати багато разів

🔹 Навіщо це потрібно?

Основна причина - оптимізація. Якщо певну операцію потрібно виконати один раз - кешування може бути непотрібним. Але якщо вона виконується сотні або тисячі разів, особливо у Update(), FixedUpdate() або в циклах, кешування може значно зменшити кількість зайвих операцій

Кешування - один із найпростіших, але важливих прийомів оптимізації, який варто знати кожному Unity розробнику


Виклав на YouTube нове відео про події (Events) у C# та Unity

Якщо вам не зовсім зрозуміло як працюють події і для чого вони потрібні - можете переглянути це відео

В ньому я на прикладах показую як вони працюють і в яких випадках їх потрібно використовувати

EVENTS В UNITY | ЯК ПРАЦЮЮТЬ ПОДІЇ В C#


🎮 New Input System vs Old Input Manager в Unity - яку систему введення вибрати?

Керування гравцем - одна з найважливіших частин будь-якої гри. У Unity для цього існують дві системи:

🔹 Old Input Manager - стара система введення, яка використовувалась у Unity багато років

🔹 New Input System - сучасна система, створена Unity для заміни старого підходу

Давайте розберемося, чим вони відрізняються та яку краще використовувати у своїх проектах

🕹 Old Input Manager

Стара система працює через клас Input, який дозволяє напряму перевіряти стан клавіш, рух миші, чи взаємодію з екраном телефону. Наприклад, щоб перевірити натискання клавіші:
if (Input.GetKeyDown(KeyCode.Space))
{
Jump();
}

Або отримати рух гравця:
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");

Vector3 movement = new Vector3(horizontal, 0, vertical);
У цьому випадку Unity сама створює осі:
• Horizontal - A/D або стрілки
• Vertical - W/S або стрілки

Переваги Old Input Manager:
• Простий для розуміння. Для першого знайомства з Unity він дуже зручний
• Мінімум налаштувань. Не потрібно створювати додаткові файли
• Підходить для маленьких проектів

Недоліки Old Input Manager:
• Складно підтримувати багато пристроїв одночасно. Наприклад, якщо ви хочете додати клавіатуру і геймпад одночасно, доведеться писати багато додаткового коду

• Жорстка прив'язка до кнопок. Наприклад:
Input.GetKey(KeyCode.Space)
означає, що дія прив'язана саме до пробілу. Якщо гравець хоче змінити кнопку в налаштуваннях - потрібно створювати власну систему

• Не дуже зручний для великих ігор. У великому проекті можуть бути десятки дій (рух, стрибок, постріл, перезарядка, відкрити інвентар). І керувати всім через Input.GetKey() стає незручно

🚀 New Input System

New Input System - це новий підхід до керування, де ми розділяємо "Що робить гравець?" від "Якою кнопкою він це робить?"

Наприклад, у грі є дія стрибка. А вже потім ми можемо прив'язати до неї пробіл на клавіатурі та кнопку A на геймпаді. Тобто код більше не залежить від конкретної кнопки

Приклад:
public void OnJump(InputValue value)
{
if (value.isPressed)
{
Jump();
}
}
Тут ми не перевіряємо кнопку. Ми просто реагуємо на подію: "Гравець стрибнув"

⚙️ Як працює New Input System?

Основні поняття:

1) Input Actions - список дій гравця. Наприклад: Move, Jump, Attack, Interact

2) Action Maps - групи дій. Наприклад: Player, Car, UI. Це дуже зручно, коли у грі є різні режими

Одна з головних переваг New Input System - мультиплатформність. Одна дія Jump може працювати через клавіатуру, геймпад, чи мобільний пристрій. І код гри при цьому не змінюється

Що вибрати?

Якщо ви тільки починаєте вивчати Unity - Old Input Manager допоможе швидко зрозуміти основи. Але для реальних проектів краще використовувати New Input System

New Input System спочатку може здатися складнішою, але після звикання вона значно спрощує розробку


Я вже кілька разів розповідав про корутини в цьому каналі, але знаю, що ця тема не завжди стає зрозумілою з першого разу

Тому підготував окреме відео на YouTube, у якому пояснюю, як працюють корутини, на прикладах ігрових механік

Приємного перегляду👇
https://youtu.be/yszvJ5WSesI


Object Pooling - це спосіб оптимізації, при якому об'єкти не створюються і не видаляються щоразу, а повторно використовуються. Замість постійного створення нових об'єктів ми заздалегідь створюємо їх певну кількість і просто вмикаємо або вимикаємо за потреби

🔹 Навіщо це потрібно?

Уявімо, що у грі кожної секунди вилітають кулі. Якщо кожного разу використовувати:
Instantiate(bulletPrefab);
Destroy(bullet);
Unity постійно буде:
• створювати нові об'єкти
• виділяти пам'ять
• видаляти об'єкти

Це створює додаткове навантаження на процесор і може викликати просадки FPS

🔹 Як працює Object Pooling?

На початку гри створюється певна кількість об'єктів, наприклад 30 куль. Спочатку всі вони вимкнені

А коли потрібно зробити постріл:
• береться одна вимкнена куля
• переміщується у потрібне місце
• вмикається

Після того як куля влучила або пролетіла певну відстань вона не видаляється, а просто вимикається і повертається в пул

Під час наступного пострілу ця ж куля використовується повторно

🔹 Коли не варто використовувати?

Якщо об'єкт створюється лише один або кілька разів за всю гру, використовувати Object Pooling немає сенсу.

Наприклад:
• головний персонаж
• унікальний бос


Ще один спосіб оптимізації - Object Pooling


Video oldindan ko‘rish uchun mavjud emas
Telegram'da ko‘rish


Occlusion Culling - що це і як він покращує FPS

Occlusion Culling - це система оптимізації, яка дозволяє Unity не рендерити об'єкти, яких камера не бачить

Простими словами: якщо між камерою і об'єктом є стіна, Unity може не промальовувати цей об'єкт, що збільшує FPS

🔹 Навіщо це потрібно?

Уявімо, що у вас є будинок із десятьма кімнатами. Гравець знаходиться лише в одній кімнаті, але без Occlusion Culling Unity буде продовжувати рендерити меблі, стіни та інші об'єкти в усіх інших кімнатах. Це створює зайве навантаження на відеокарту

З Occlusion Culling Unity визначає, що ці об'єкти закриті стінами, і тимчасово перестає їх рендерити

🔹 Як увімкнути?

1. Виділіть статичні об'єкти сцени (об'єкти які не рухаються)
2. У Inspector поставте галочку Static
3. Відкрийте: Window → Rendering → Occlusion Culling
4. Натисніть Bake, щоб Unity прорахувала видимість об'єктів

Після цього система почне автоматично приховувати об'єкти, яких камера не бачить

🔹 Як перевірити, що він працює?

У вікні Occlusion Culling увімкніть режим візуалізації. Так можна побачити, які об'єкти Unity зараз рендерить, а які приховує


Switch в C# - зручна заміна великої кількості if

switch використовується тоді, коли потрібно виконати різні дії залежно від значення змінної. Якщо у вас багато перевірок через if, їх часто можна замінити на switch

🔹 Приклад через if

if (weaponId == 0)
{
Debug.Log("Pistol");
}
else if (weaponId == 1)
{
Debug.Log("Shotgun");
}
else if (weaponId == 2)
{
Debug.Log("Rifle");
}
Код працює, але коли перевірок стає багато - читати його стає незручно

🔹 Той самий приклад через switch

switch (weaponId)
{
case 0:
Debug.Log("Pistol");
break;

case 1:
Debug.Log("Shotgun");
break;

case 2:
Debug.Log("Rifle");
break;
}
Такий код легше читати та підтримувати

🔹 Що таке case?

case - це значення, яке перевіряється

Наприклад:
case 1:
Цей блок виконається тільки тоді, коли змінна дорівнює 1

🔹 Навіщо потрібен break?

case 1:
Debug.Log("Shotgun");
break;
break завершує виконання switch. Без нього програма продовжила б перевіряти наступні case

🔹 Default

Іноді значення не підходить під жоден case. Для цього використовується default
switch (weaponId)
{
case 0:
Debug.Log("Pistol");
break;

case 1:
Debug.Log("Shotgun");
break;

default:
Debug.Log("Unknown Weapon");
break;
}
default працює як else

🔹 Приклад з Enum

switch дуже часто використовують разом з enum
public enum EnemyType
{
Zombie,
Skeleton,
Boss
}
switch (_enemyType)
{
case EnemyType.Zombie:
Debug.Log("Zombie");
break;

case EnemyType.Skeleton:
Debug.Log("Skeleton");
break;

case EnemyType.Boss:
Debug.Log("Boss");
break;
}

🔹 Коли використовувати switch?

• Коли перевіряється одна змінна
• Коли можливих значень багато

🔹 Коли краще використовувати if?

• Для складних умов

Наприклад:
if (health > 50 && isAlive)
{
...
}
Такі перевірки через switch робити незручно


🚀 AddRelativeForce - рух відносно об'єкта

Якщо ви вже працювали з фізикою в Unity, то напевно використовували метод AddForce()
rb.AddForce(Vector3.forward * speed);
Але є одна проблема - Vector3.forward завжди означає напрямок по світових координатах

Тобто якщо об'єкт повернути, сила все одно буде прикладатися вперед по осі світу, а не по його власному напрямку

Для таких випадків існує AddRelativeForce()
rb.AddRelativeForce(Vector3.forward * speed);
Тепер сила прикладається відносно локальних осей об'єкта

Наприклад, якщо ракета повернута вправо на 90°, то AddForce:
rb.AddForce(Vector3.forward * speed);
рухатиме її вперед по світовій осі Z

А метод AddRelativeForce:
rb.AddRelativeForce(Vector3.forward * speed);
рухатиме її вперед саме в тому напрямку, куди дивиться ракета


💾 Як зберігати дані за допомогою JSON

У зберіганні даних через PlayerPrefs є кілька недоліків:
• Підтримуються лише типи int, float та string
• Дані легко знайти та змінити через редактор реєстру або спеціальні програми

Для більш серйозних проектів часто використовують JSON. За допомогою JSON можна зберігати будь-які дані: класи, списки, налаштування, прогрес гри і т.д.

Спочатку створюємо клас для даних:
[System.Serializable]
public class SaveData
{
public int coins;
public int level;
public float soundVolume;
}

Збереження даних:
SaveData data = new SaveData();
data.coins = 150;
data.level = 3;
data.soundVolume = 0.8f;

string json = JsonUtility.ToJson(data);

File.WriteAllText(
Application.persistentDataPath + "/save.json",
json
);

Завантаження даних:
string path = Application.persistentDataPath + "/save.json";

if (File.Exists(path))
{
string json = File.ReadAllText(path);
SaveData data = JsonUtility.FromJson(json);
}

Якщо потрібно захистити дані від редагування, JSON можна додатково шифрувати перед записом у файл і розшифровувати під час завантаження. Наприклад:
byte[] data = SerializeToBytes(progressData);
File.WriteAllBytes(_path, data);
private byte[] SerializeToBytes(ProgressData data)
{
using (MemoryStream memoryStream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, data);
return memoryStream.ToArray();
}
}

А щоби їх розшифрувати:
byte[] bytes = File.ReadAllBytes(_path);
DeserializeAndLoad(bytes);
private ProgressData DeserializeFromBytes(byte[] bytes)
{
using (MemoryStream memoryStream = new MemoryStream(bytes))
{
BinaryFormatter formatter = new BinaryFormatter();
return (ProgressData)formatter.Deserialize(memoryStream);
}
}
Тоді користувач не зможе просто відкрити файл і змінити кількість монет або рівень через блокнот

JSON - один із найпопулярніших способів зберігання даних у Unity, який підходить для більшості проектів


📝 Що таке enum у Unity?

enum (перерахування) - це тип даних, який дозволяє зберігати один із заздалегідь визначених варіантів

Наприклад, замість того щоб використовувати числа:
int gameState = 0;

краще створити enum:
public enum GameState
{
Menu,
Playing,
Paused,
GameOver
}

Тепер можна працювати із зрозумілими назвами:
GameState gameState = GameState.Playing;

if(gameState == GameState.GameOver)
{
Debug.Log("Гру завершено");
}

🔹 Переваги enum

• Код стає більш читабельним
• Менше шансів помилитися через неправильне число
• Зручно вибирати значення в Inspector
• Легко додавати нові стани

Наприклад, enum можна використовувати для:

Станів гри
public enum GameState
{
Menu,
Playing,
Paused,
GameOver
}

Типів ворогів
public enum EnemyType
{
Zombie,
Skeleton,
Boss
}

Типів бонусів
public enum BonusType
{
Coins,
Health,
Shield
}

Також enum можна бачити в Inspector:
[SerializeField] private EnemyType enemyType;
Після цього в Unity з'явиться зручний випадаючий список із усіма варіантами


SOLID в Unity простими словами

SOLID - це 5 принципів написання коду, які допомагають робити проект:
• чистішим
• зрозумілішим
• легшим у підтримці та масштабуванні

Це особливо важливо для великих проектів

🔹 S - Single Responsibility Principle

Принцип однієї відповідальності: Клас повинен відповідати лише за одну задачу

❌ Погано:
public class Player : MonoBehaviour
{
void Move() {}
void Shoot() {}
void Save() {}
void PlayAds() {}
}
Такий клас робить все підряд

✅ Краще:
PlayerMovement
PlayerShoot
SaveSystem
AdsManager
Кожен клас відповідає лише за свою логіку

🔹 O - Open/Closed Principle

Відкритий для розширення, закритий для змін: Код краще розширювати, а не постійно змінювати старий

Наприклад:
public class Enemy
{
public virtual void Attack()
{
}
}
public class Zombie : Enemy
{
public override void Attack()
{
Debug.Log("Zombie Attack");
}
}
Ми додаємо нову поведінку без зміни базового класу

🔹 L - Liskov Substitution Principle

Дочірній клас повинен нормально заміняти базовий. Якщо Zombie успадковує Enemy, він повинен поводитись як Enemy

❌ Поганий дизайн:
public class Bird
{
public virtual void Fly() {}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new Exception();
}
}
Пінгвін - не літає → така структура погана

🔹 I - Interface Segregation Principle

Не змушуй клас реалізовувати зайве

❌ Погано:
interface IEnemy
{
void Attack();
void Fly();
}
Не всі вороги літають

✅ Краще:
interface IAttack
{
void Attack();
}

interface IFly
{
void Fly();
}
Кожен клас бере лише те, що потрібно

🔹 D - Dependency Inversion Principle

Залежність від абстракцій, а не конкретних класів

❌ Погано:
Gun gun = new Gun();
Клас жорстко прив’язаний до Gun

✅ Краще:
IWeapon weapon;
Тепер можна легко міняти зброю:
• Gun
• Bow
• RocketLauncher

❗️ SOLID - це не правило "робити все складно". Не потрібно робити 100 інтерфейсів і абстракцій для маленької гри. Використовуй SOLID там, де він реально допомагає

SOLID - це:
• набір принципів для хорошого коду
• основа чистої архітектури
• дуже корисна штука для Unity розробника

Але головне - баланс


ScriptableObject в Unity - що це і навіщо він потрібен

ScriptableObject - це спеціальний тип класів у Unity, який дозволяє зберігати дані окремо від об’єктів сцени

Простими словами: це "контейнер" для даних, який існує як окремий asset у проекті

🔹 Навіщо потрібен ScriptableObject?

Без ScriptableObject дані часто зберігають прямо у MonoBehaviour:
public class Player : MonoBehaviour
{
public int Damage = 10;
public float Speed = 5;
}
Проблеми такого підходу:
• дані дублюються
• важче налаштовувати
• складніше перевикористовувати

🔹 Як працює ScriptableObject?

Ми створюємо окремий asset з даними:
[CreateAssetMenu(menuName = "Configs/Weapon")]
public class WeaponConfig : ScriptableObject
{
public int Damage;
public float FireRate;
}
[CreateAssetMenu] додає можливість створювати asset через: Right Click → Create

🔹 Створення asset

Після створення скрипта:
• Right Click у Project
• Create → Configs → Weapon

Тепер у тебе є окремий asset з даними зброї

🔹 Використання
public class Gun : MonoBehaviour
{
[SerializeField] private WeaponConfig _config;

private void Shoot()
{
Debug.Log(_config.Damage);
}
}
Тепер зброя бере дані з ScriptableObject

🔹 Чому це круто?

Дані не дублюються: один WeaponConfig можна використовувати:
• для багатьох ворогів
• різної зброї
• різних сцен

Зручно налаштовувати: змінюєш значення в одному asset і воно змінюється всюди

Чистіший код: логіка окремо - дані окремо. Це дуже хороший підхід для архітектури


Що таке static в Unity / C#

static - це ключове слово, яке робить змінну, метод або клас спільним для всіх об'єктів

Простими словами: static належить не об’єкту, а самому класу

🔹 Звичайна змінна
public int health = 100;
Кожен об’єкт має своє власне health

Наприклад: у одного ворога 100 HP, у другого 50 HP

🔹 Static змінна
public static int Coins;
Тепер змінна одна для всіх

Якщо її змінити:
Coins += 10;
Значення зміниться глобально

🔹 Як звертатись до static?

Для static не потрібно створювати об’єкт:
GameManager.Coins += 10;
Ми звертаємось прямо через назву класу

🔹 Де це використовується в Unity?

Глобальні дані
public static int Score;

Events
public static event Action OnPlayerDeath;

Singleton
public static GameManager Instance;

Utility методи
public static float CalculateDamage()
{
return 10;
}

🔹 Static method

Static метод можна викликати без об’єкта і він не має доступу до звичайних змінних класу
public static void Print()
{
Debug.Log("Hello");
}

Виклик:
MyClass.Print();

🔹 Важливий нюанс

Static змінні:
• існують весь час поки працює гра
• спільні для всіх об'єктів

Тому з ними потрібно бути обережними

Не потрібно робити майже все static

Через це:
• код стає заплутаним
• з’являється сильна залежність між системами
• важче дебажити

🔹 Простий приклад

Без static: кожен ворог має свої HP

Із static: усі вороги матимуть одне спільне HP. Якщо його змінити у одного ворога, зміниться і у всіх інших

20 ta oxirgi post ko‘rsatilgan.