Scriptable Objects в Unity: Ефективне управління об’єктами гри

Створення та управління різними типами ворогів може бути складним завданням. Використовуючи Scriptable Objects в Unity, розробники можуть створити гнучку та модульну систему для визначення характеристик ворогів. У цій статті я розгляну, як використовувати Scriptable Objects для покращення управління об'єктами ворогів у грі.

pic

scriptable objects spawning

Створення класу ScriptableObject

Спочатку створюю новий клас ScriptableObject у папці Enemy Scripts, щоб визначити EnemyType з їхніми префабами та налаштуваннями. Його завдання — зберігати дані для кожного типу ворога в грі. Щоб зробити клас ScriptableObject, достатньо додати це слово після двокрапки, замінивши ключове слово Monobehaviour.

Це визначення успадкування дозволяє класу EnemyType наслідувати клас ScriptableObject Unity. Таке успадкування дуже важливе, оскільки дозволяє створювати контейнери даних, які не залежать від конкретних ігрових об'єктів і можуть бути повторно використані в різних сценах чи навіть проектах. У цьому випадку кілька ворогів будуть використовувати одні й ті ж контейнери даних.

pic

public class EnemyData : ScriptableObject

Unity потрібно вказати, щоб створити пункт меню в вікні проекту для створення екземплярів цього скриптового об'єкта. Це дозволяє легко створювати та управляти активами даних ворогів. У класі потрібно оголосити атрибут:

  • [CreateAssetMenu(fileName = “New Enemy Data”, menuName = “Enemy Data”)]

pic

Кожен тип ворога потребує змінної для збереження посилання на префаб, який є візуальним та функціональним представленням ворога. Префаб — це тип даних GameObject, і оскільки це для префаба ворога, називатимемо його enemyPrefab.

  • public GameObject enemyPrefab;

Згодом, наприклад, якщо у мене є префаб "Basic Enemy", я перетягну цей префаб у це поле в Інспекторі, коли створюватиму скриптовий об'єкт "BasicEnemyType".

Кожен тип ворога також потребує значення для визначення його швидкості, яке буде використовуватися скриптом руху ворога для визначення швидкості його переміщення в грі. Для зберігання цієї змінної використовується тип даних float, і змінна матиме ім’я speed.

  • public float speed;

Кожен ворог може зіткнутися з лазером, і коли це відбувається, його здоров'я зменшується, а коли воно досягає нуля, ворог знищується. Для збереження кількості зіткнень, які він може витримати до знищення, використовуємо тип даних int і змінну health.

  • public int health;

До цього скриптового об'єкта можна додати ще багато змінних, щоб зберігати додаткові властивості ворогів, такі як сила атаки, показники захисту, спеціальні здібності або будь-які інші дані, важливі для гри.

Кожен тип ворога також потребує змінної для збереження посилання на префаб, який є візуальним та функціональним представленням лазера.
Прив'язка префабу до змінної типу GameObject дає можливість зберігати посилання на префаб. Оскільки це префаб для лазера ворога, називаємо його enemyLaserPrefab.

  • public GameObject enemyLaserPrefab;

Повертаючись до редактора Unity:

  • Клацніть правою кнопкою миші в Project View > Create > ScriptableObject, і з'явиться опція створити актив з назвою “Enemy Type”, що буде екземпляром класу EnemyType.

pic

Після створення класу я перейменовую клас з NewEnemyType на EnemyType.

pic

Створення активів ScriptableObject

Наступним кроком я створюю окремі Scriptable Object для кожного EnemyType і переіменовую їх так, щоб назва відповідала імені скрипта Enemy.

  • У вікні проекту Unity клацніть правою кнопкою і перейдіть до Create > ScriptableObjects > EnemyType.
  • Створіть окремий актив EnemyType для кожного ворога (ChasingEnemy, SideToSideEnemy, CirclingLeft, CirclingEnemy, SmartEnemy, HorizontalEnemy, Enemy).
  • Призначте відповідні префаби, лазери та стандартні властивості (як-от швидкість і здоров'я) для кожного активу.

pic

pic

налаштування префабів та змінних у Scriptable об'єкті

[

Створення модульної архітектури гри в Unity за допомогою ScriptableObjects

Цей посібник надає поради та хитрощі від професіоналів для впровадження ScriptableObjects у виробництво.

unity.com

](https://unity.com/resources/create-modular-game-architecture-with-scriptable-objects-ebook?source=post_page-----d87cf7491e1e--------------------------------)

Зміни в SpawnManager

Наступним кроком я змінюю SpawnManager, щоб використовувати активи ScriptableObject, які були створені у вікні проекту замість прямого посилання на префаби. На даний момент я закоментую всі окремі префаби ворогів і заміню їх на масив: private EnemyType[] enemyType;

pic

закоментування окремих префабів і заміна їх на масив

На даний момент у циклі while я закоментую умовний оператор, який перевіряє, чи зупинилися вороги, і оператор switch, який використовувався для створення кожного типу ворога.
Після того як я визначив нову поведінку за допомогою ScriptableObjects, я видалю старе визначення, яке було закоментовано.

pic

закоментування старого циклу while, який використовував оператор switch

Корутина IEnumerator SpawnEnemyRoutine() замінить старий оператор switch і кілька посилань на префаби масивом для більш ефективної системи ScriptableObject.

pic

використання масиву для обробки інстанціації типів ворогів ScriptableObjects

  • while (!_stopSpawning && enemiesSpawned < enemyCount) створює ворогів, поки умова _stopSpawning не спрацює і поки не буде досягнуто вказаної кількості ворогів.
  • EnemyType selectedEnemyType = enemyTypes[Random.Range(0, enemyTypes.Length)] випадковим чином вибирає ворога, використовуючи масив ScriptableObject, замінюючи оператор switch, який використовувався для випадкового вибору ворога за допомогою цілого числа (int enemyType = Random.Range(0, 7)).
  • GameObject spawnedEnemy = Instantiate(selectedEnemyType.enemyPrefab, positionToSpawn, Quaternion.identity); використовує одну команду для інстанціації префабу ворога з вибраного ScriptableObject в обраній точці появи без обертання, тоді як оператор switch раніше обробляв інстанціацію для кожного випадку.
  • Enemy enemyScript = spawnedEnemy.GetComponent(); отримує компонент Enemy з інстанційованого об'єкта.
  • if (enemyScript != null) { enemyScript.Initialize(selectedEnemyType.speed, selectedEnemyType.enemyLaserPrefab); } перевіряє, чи знайдений компонент enemyScript, після чого викликає метод Initialize(), щоб задати швидкість ворога та префаб лазера з ScriptableObject.
  • spawnedEnemy.transform.parent = _enemyContainer.transform; продовжує організовувати інстанційованого ворога під батьківським об'єктом контейнера ворогів для кращого управління ієрархією сцени.
  • enemiesSpawned++; збільшує лічильник для кожного створеного ворога.
  • yield return new WaitForSeconds(3.0f); додає затримку між появами ворогів, щоб встановити темп, який буде справедливим для гравця.

Модифікація кожного Scriptable Enemy Object

Тепер, коли кожен ворог став Scriptable Object завдяки визначенню скрипта EnemyType як Scriptable Object (тобто шаблон для об'єктів гри ворогів), і SpawnManager допомагає створювати екземпляри Scriptable Objects (selectedEnemyType), я можу почати модифікацію кожного конкретного скрипта ворога, щоб ініціалізувати змінні в шаблоні Scriptable Object (тобто EnemyType). Ось один приклад, але всі типи ворогів будуть мати це визначення або подібне. Метод Initialize() дозволяє мені налаштувати ворога з конкретною швидкістю руху та типом лазера.
Це схоже на надання ворогу його "налаштувань" перед тим, як він почне діяти в грі.

pic

ініціалізація швидкості та префабу лазера ворога

  • public void Initialize(float speed, GameObject laserPrefab)
    отримує значення speed (десяткове число) і префаб лазера, який буде перетягнуто та відпущено в інспектор кожного Scriptable Object.
  • _speed = speed; приймає надану швидкість і зберігає її в приватній змінній з ім'ям _speed.
  • _enemyLaserPrefab = laserPrefab; зберігає в приватній змінній _enemyLaserPrefab і дозволяє скрипту запам'ятовувати, який префаб лазера використовувати при стрільбі лазерами.

Виключення: Горизонтальні та Розумні вороги

Є два EnemyType(и), які не з'являються в тому ж місці і не використовують той самий префаб лазера; це Горизонтальний Ворог (case 5) і Розумний Ворог (case 6).

pic

випадок 5 і 6 мають конкретну точку появи і напрямок руху

Як керувати цими виключеннями? Я маю розширити EnemyType ScriptableObject, щоб включити особливу поведінку спауна та прапорці. Пізніше їх можна буде знайти в інспекторі ScriptableObjects і налаштувати відповідно до спеціальних ворогів, які потребують особливих налаштувань спауна та руху.

pic

нові прапорці для керування поведінкою спауна для конкретних ScriptableObjects

  • public bool requiresUniqueSpawnPosition = false; оголошується та за замовчуванням встановлюється в false, що вказує на те, чи потрібно ворогу спеціальне місце для спауна, чи він може використовувати стандартне.
  • public Vector3 uniqueSpawnPosition; зберігає точне місце, де ворог має з'явитися, коли змінна requiresUniqueSpawnPosition встановлена в true в інспекторі ScriptableObject.
  • public bool requiresRotation = false; встановлено в false і визначає, чи потрібно ворогу повертатися з його стандартної орієнтації при спаунінгу.
  • public Quaternion enemyRotation; визначає бажану орієнтацію ворога, якщо змінна requiresRotation встановлена в true в інспекторі ScriptableObject.
  • public bool isHorizontalEnemy = false; за замовчуванням встановлено в false і вказує, чи повинен ворог рухатися горизонтально, замість звичайного вертикального руху.

Повернувшись до редактора Unity, я можу застосувати змінні, оголошені в класі EnemyType : ScriptableObject:

  • вибрати ScriptableObject (Горизонтальний Ворог або Розумний Ворог)
  • встановити прапорці для змінних, які стосуються мого Горизонтального Ворога і Розумного Ворога відповідно

pic

налаштування ScriptableObject для Горизонтального Ворога щодо позиції спауна та відстеження до спауна другого

[

Поворот і орієнтація в Unity

Unity використовує систему координат лівої руки. У Unity ви можете використовувати як кути Ейлера, так і кватерніони для представлення…

docs.unity3d.com

](https://docs.unity3d.com/Manual/QuaternionAndEulerRotationsInUnity.html?source=post_page-----d87cf7491e1e--------------------------------)

Для Розумного Ворога є трохи інша конфігурація, оскільки він з'являється внизу сцени і повинен рухатися вгору. Для цього використовується тип даних Quaternion, щоб представити поворот в Unity, що відображається в спадному меню інспектора. При використанні всередині Quaternion уникатиме таких проблем, як блокування карданного валу, і вони відображаються як чотири компоненти: X, Y, Z, W. В Unity компонент W кватерніона не відповідає безпосередньо осі, як X, Y, Z.
Натомість він представляє скалярну частину кватерніона і працює разом з іншими компонентами, щоб визначити величину та напрямок обертання в 3D просторі. Щоб повернути SmartEnemy на 180 градусів навколо осі Z, встановіть значення Quaternion в інспекторі на: X: 0, Y: 0, Z: 1, W: 0

pic

налаштування ScriptableObject для Розумного Ворога щодо повороту і позиції спауна

Модифікація SpawnManager для виняткових ігрових об'єктів

Наступним кроком я маю модифікувати SpawnManager, щоб врахувати два спеціальні сценарії спауна та контролювати поведінку спауна ворогів та їх розташування. Ці рядки координують, коли, де і як з'являються вороги, при цьому уникнувши надмірного спауна горизонтальних ворогів та забезпечуючи правильний спаун кожного ворога.

pic

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

  • if (selectedEnemyType.isHorizontalEnemy && _horizontalEnemyActive) перевіряє, чи є вибраний ворог горизонтальним, і запобігає спауну іншого, якщо один вже активний, забезпечуючи, щоб одночасно був лише один горизонтальний ворог.
  • yield return null; і continue; працюють разом, щоб пропустити поточний кадр, коли горизонтальний ворог вже активний, гарантуючи, що подальші дії в цій ітерації циклу не виконуються, без зависання гри.

тернарні оператори

  • Vector3 positionToSpawn = selectedEnemyType.requiresUniqueSpawnPosition ? selectedEnemyType.uniqueSpawnPosition : new Vector3(Random.Range(-8f, 8f), 7, 0); визначає позицію спауна для ворога за допомогою тернарного умовного оператора. Якщо ворог вимагає унікальної позиції, використовується задане значення; в іншому випадку, він спауниться випадково вздовж осі x в межах і трохи вище екрану.
  • if (selectedEnemyType.isHorizontalEnemy) { _horizontalEnemyActive = true; } позначає прапорець _horizontalEnemyActive як true, щоб запобігти появі кількох горизонтальних ворогів.

Модифікація класів Горизонтальний Ворог і Розумний Ворог

Я додаю метод Initialize() як до Горизонтального Ворога, так і до Розумного Ворога, але оскільки вони використовують різні префаби лазерів, їм необхідні різні змінні для посилань на ці специфічні GameObject-и. У випадку з горизонтальним ворогом, префаб лазера — це _homingMisslePrefab, а для розумного ворога — _smartLaserPrefab.

pic

ініціалізація швидкості і префабу лазера для Горизонтального Ворога

pic

ініціалізація швидкості і префабу лазера для Розумного Ворога

У класі Горизонтальний Ворог я додам два рядки коду для керування інстанцією:

pic

скидання позиції Горизонтального Ворога по осі Y

  • float randomY = Random.Range(5f, 7f); ініціалізує його на випадковій вертикальній позиції між 5 і 7 одиницями, і …
  • transform.position = new Vector3(-14.85f, randomY, 0); телепортує ворога до лівого краю екрану з новою випадковою вертикальною позицією, створюючи ефект зациклення.

[

Object.FindObjectOfType

Suggest a change Thank you for helping us improve the quality of Unity Documentation.

Хоча ми не можемо прийняти всі…

docs.unity3d.com

](https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Object.FindObjectOfType.html?source=post_page-----d87cf7491e1e--------------------------------)

Я оголошую метод OnDestroy(), щоб повідомити SpawnManager, що тепер він може інстанціювати Горизонтального Ворога, оскільки попередній був знищений.

pic

пошук SpawnManager для повідомлення, що об'єкт гри був знищений

  • private void OnDestroy() автоматично викликається, коли об'єкт горизонтального ворога знищується, очищаючи і повідомляючи SpawnManager, коли ворог видаляється зі сцени.
  • FindObjectOfType().OnHorizontalEnemyDestroyed(); шукає сцену на наявність SpawnManager і викликає метод OnHorizontalEnemyDestroyed(), щоб повідомити SpawnManager, що ворог був знищений, щоб він міг коригувати кількість ворогів або ініціювати новий спаун.

Призначення ScriptableObject в Unity Inspector

  • відкрийте компонент SpawnManager в інспекторі.
  • лівою кнопкою миші клікніть на Enemy Types > введіть кількість типів ворогів; у моєму випадку їх є 7
  • перетягніть і вставте всі створені EnemyType ScriptableObjects в масив enemyTypes у SpawnManager.

pic

налаштування типів ворогів у масиві, визначеному в скрипті SpawnManager

Невірне призначення функцій ворогам

Під час тестування виникла проблема з Горизонтальним Ворогом і Розумним Ворогом, яким SpawnManager призначав EnemyShield. Щоб усунути цю помилку, потрібно знайти спосіб виключити HorizontalEnemy і SmartEnemy з логіки ApplyShield в оновленому SpawnManager. Знову ж таки, я повернувся до EnemyType : ScriptableObject і оголосив нову змінну.

pic

змінна для керування (включення/виключення), чи можна призначити EnemyShield для ScriptableObject

У SpawnManager метод ApplyShield потребує умовного виразу, що перевіряє, чи не є тип ворога таким, у якого в інспекторі встановлено прапорець, що вказує, що він може отримати щит, інакше return, щоб повторити процес перевірки умови на наступному спауненому об'єкті гри. Параметр типу даних EnemyType enemyType додається, щоб використовувати для застосування щита.

pic

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

Горизонтальний Ворог не матиме цієї опції увімкненою, але всі інші типи ворогів матимуть її включеною.

pic

pic

Горизонтальний Ворог не має щита, але Переслідуючий Ворог та інші типи ворогів матимуть його увімкненим

У методі SpawnEnemyRoutine() в SpawnManager змінна selectedEnemyType додається як параметр до методу ApplyShield() і передається логіка.

pic

передача логіки в метод, додаючи змінну як параметр

І, зрештою, момент істини

Повертаючись до редактора Unity, запустіть додаток, і хвиля ворогів з'являється, використовуючи масив для спауна ScriptableObjects з SpawnManager.

pic

спаун ScriptableObjects

Використання Scriptable Objects дає гнучку основу для ефективного управління кількома типами ворогів шляхом роз'єднання множини об'єктів гри подібного типу від SpawnManager.
Цей метод не лише спрощує управління об'єктами гри, але й покращує динаміку ігрового процесу.

Готові підняти свої навички розробки ігор на новий рівень? Підписуйтеся на більше порад щодо ефективного використання Unity!

[

Вступ до Scriptable Objects - Unity Learn

Scriptable Objects — це чудові контейнери даних. Їх не потрібно прикріплювати до GameObject у сцені. Вони можуть бути…

learn.unity.com

](https://learn.unity.com/tutorial/introduction-to-scriptable-objects?source=post_page-----d87cf7491e1e--------------------------------)

[

ScriptableObject

Перехід до програмування ScriptableObject — це контейнер даних, який можна використовувати для збереження великих обсягів даних, незалежно від…

docs.unity3d.com

](https://docs.unity3d.com/Manual/class-ScriptableObject.html?source=post_page-----d87cf7491e1e--------------------------------)

[

Архітектура вашого коду для ефективних змін та налагодження за допомогою ScriptableObjects | Unity

Залишайте свій код гнучким, зручним для керування та легким для налагодження, використовуючи архітектуру з ScriptableObjects.

unity.com

](https://unity.com/how-to/architect-game-code-scriptable-objects?source=post_page-----d87cf7491e1e--------------------------------)

Перекладено з: Scriptable Objects in Unity: Managing Game Objects Efficiently

Leave a Reply

Your email address will not be published. Required fields are marked *