Кратко ръководство за три популярни модела за управление на данни във вашите игри.

Продължаваща и разделяща дискусия в OOP е как да управлявате потока от данни чрез програма. Начинът, по който структурирате потока от данни през вашата игра или програма, е важен, тъй като, ако не се управлява правилно, диаграмата на това кои обекти комуникират с кои и по какъв начин ще стане неуправляема (Т.Е. като популярна вечеря с паста.) Тази малка справка ще премине в три модела, които могат да ви помогнат да контролирате трафика на данни: сингълтони, PubSub и събития/обратни повиквания.

Сингълтони

Във вашите дни на Unity може да сте виждали или писали код, който изглежда нещо подобно:

GameObject.Find("GameManager").GetComponent<GameManager>().StartGame();
var score = GameObject.Find("GameManager").GetComponent<GameManager>().Score

Проблемът с метода Find() е — работи бавно. Find() трябва да прегледа всеки GameObject във вашата сцена и да сравни името му с предоставения от вас низ. В най-лошия сценарий, той ще работи при O(n), където n е броят GameObjects във вашата сцена. Тъй като вашата сцена става по-сложна, ще отнеме повече време за изпълнение.

Също така, и това може да е лично мнение, трябва да избягвате да използвате имена, които можете да промените в редактора във вашия код. Името може да се промени в редактора и това ще развали кода ви. Може също да забравите името, което сте избрали за GameObject и да трябва да се върнете в редактора, за да го намерите (което е досадно).

За да се преборим с това, можем да използваме единичен модел за MonoBehaviours, който ще бъде в сцена само веднъж.

Сингълтън е клас, който може да бъде инстанциран само веднъж. В контекста на MonoBehaviour, това е компонент, който ще бъде инсталиран само веднъж в сцена. Планът за създаване на MonoBehaviour singleton е показан по-долу;

public static GameManager Instance { private set; get; } = null;
...
private void Awake() {
    if(Instance != null)
        throw new System.Exception("More than one instance");
    Instance = this;
}
private void OnDestroy() {
    Instance = null;
}

Това не само създава публична статична променлива, така че други обекти да могат да я препращат през пространството на имената на класовете, но също така проверява дали вече е имало инстанциониран единичен обект и грешка, ако е така. Два екземпляра на един и същи сингълтон могат да нарушат играта ви тежко, така че не забравяйте да проверите за това програмно.

За достъп до сингълтона можете да стартирате това навсякъде.

GameManager.Instance.StartGame();
var score = GameManager.Instance.Score;

Хубаво и просто, нали? Някои неща, които трябва да имате предвид.

  • Важно е да настроите свойството Instance на null, когато GameObject или MonoBehaviour бъде унищожен, тъй като статичните членове продължават да съществуват при зареждания на сцени. Куката MonoBehaviour OnDestroy() е подходяща воля за това.
  • Може да се изкушите да напишете това на OnEnable() и OnDisable() и въпреки че можете да направите това, бих ви предложил да не го правите, тъй като има ситуации, в които може да искате да получите достъп до деактивиран компонент.
  • Както всички скриптове за Unity, трябва да осъществявате достъп до Instance само от главната нишка, тъй като достъпът до нея от друга нишка може да причини сблъсъци. Например, не осъществявайте достъп до GameObject.Instance от обратно извикване Timer. Можете да проверите за това, но открих, че не е необходимо.

PubSub

PubSub действа като централен център за потока от данни. Обектите могат да публикуват съобщения до определен ключ и той ще изпрати съобщението до всички абонирани за този конкретен ключ. Намирам това за особено полезно за съобщения, които имат голям обхват и когато има много обекти, които трябва да го получат, като например смъртта на играч.

Класът PubSub може да бъде написан като статичен, така че да може да бъде лесно достъпен, или написан като singleton MonoBehaviour. Предпочитам подхода на статичния клас, тъй като не претрупва вашата сцена и не изисква настройка на редактора.

Трите поведения, необходими за PubSub са.

  • Позволете на обектите да се абонират за ключове.
  • Позволете на обектите да се отпишат от ключове.
  • Разрешаване на публикуване на обекти в ключове.

Ако използвате статичен клас, за да го приложите, вие също ще искате възможността за изчистване и нулиране, когато се зареди нова сцена, тъй като статичните членове продължават да съществуват при зареждане на сцена. SceneManager.sceneUnloaded е полезно за това.

Реализация, предназначена за Unity, може да бъде намерена тук. Чувствайте се свободни да използвате това във вашия код.

https://gist.github.com/ZacharyBuffone/df9d2fb0ab1a2032c9b205aa31eda10c

Имайте предвид, че класът PubSubID може да е пресилен, но ми харесва как не позволява на абониращите се обекти да променят стойността на ID (за разлика от uint).

Събития и обратни повиквания

Обратните повиквания са популярен начин за изпращане на съобщения в уеб разработката. Обратното извикване е метод, който се дава на друг обект, който след това може да бъде извикан от този обект, когато възникне събитие. Това може да бъде особено полезно при разработка на потребителски интерфейс, където искате кодът да се изпълнява при натискане на бутон. Популярен начин за постигане на това в C# е използването на делегати и събития.

delegate е персонализиран тип данни, който дефинира сигнатура на метод (което е това, което методът приема като аргументи и връща, т.е. void SomeMethod(int i)). С помощта на делегат можете да декларирате event, който съхранява методи, които да бъдат извиквани по-късно. Прост пример за делегат/събитие е показан по-долу:

public class ClassA {
    public delegate OnEventHandler(int amount);
    public event OnEventHandler OnEvent;
    public void Update() {
        OnEvent?.Invoke(1);
    }
}

За да се абонирате за събитие, можете да подадете метод към събитието, като използвате оператора +=. Можете да предавате ламбда функции и методи (и System.Action<>/System.Func<>, ако желаете).

objectA.OnEvent += (amount) => {
    //handle event
}
OR
objectA.OnEvent += MethodWithSameSignatureAsDelegate;

За да се отпишете от събитие, използвайте оператора -=. Обърнете внимание, че не можете да отпишете абонамент за ламбда функция, така че се уверете, че сте добре тя да остане, докато обектът не бъде изтрит. Ако искате да можете да прекратите абонамента за функцията, създайте метод и го предайте вместо това.

object.OnEvent -= MethodWithSameSignatureAsDelegate;

Unity също има страхотен клас, наречен UnityEvent, който е като c# събития, но може да бъде сериализиран и може да бъде закачен в редактора. Те са страхотни за събития на ниво, като например натискане на бутон или задействане на спусък. Всъщност всяка игра вероятно ще извлече голяма полза от customTriggerEvent компонент, който предава данни за задействане към сериализиран обект, дефиниран в редактора.

Друга възможност за обратно извикване е да използвате класа System.Action<>. System.Action<T> ще съхранява метод с параметър от тип T. Това е полезно, ако имате нужда само от един метод за обратно извикване. Можете обаче да съхранявате само един метод.

public class ClassA {
    private System.Action<int> callback = default;
    public void Update() {
        callback?.Invoke(1);
    }
    public void SetCallback(System.Action<int> cb) {
        callback = cb;
    }
}

В този сценарий можете да предадете метод или ламбда функция на objectA.SetCallback(), за да се абонирате, и да предадете null, за да прекратите абонамента.

Неща, които трябва да избягвате: Двупосочни препратки

Да кажем, че имате два класа, House и Road. Може да изглежда разумно обектите House да искат да се позовават на обекта Road, на който се намират, а обектът на пътя може да иска да се позовава на обектите House, които съществуват върху него. Така че може да имате нещо подобно...

public class House {
    private Road road;
    ...
}
public class Road {
    private House[] houses;
    ...
}

...и диаграмата на зависимостите ще изглежда така...

Къща -› Път
Път ‹- Къща

Това се нарича кръгово или двупосочно препращане. Проблемът с това е, че изтриването на един от обектите ще изисква актуализация на препратката в другия. Без актуализиране за изтриване ще получите MissingReferenceException и най-вероятно недефинирано поведение. Въпреки че е възможно да направите изтриването и актуализирането без грешки, това ще създаде ненужна сложност на вашия код.

За да решите двупосочното препращане, вижте дали наистина имате нужда и двата обекта да носят препратка към другия. Добър начин да мислите за рефериране е да пренасяте референции от най-големия обхват към най-малкия обхват. Вашата диаграма на зависимости за игра за изграждане на град може да изглежда така...

Град -› Квартал -› Път -› Къща -› Жител

...който е еднопосочен и може да се актуализира линейно. Можете също да добавите мениджъри за популярни обекти като Road и House, които могат да управляват операции като FindRoadForHouse(House h), ако трябва да се върнете и назад (те също могат да бъдат сингълтони 👍).

Заключение

Управлението на потока от данни в OOP е много трудно. Ако не е направено правилно, вашата кодова база може да се разрасне до неуправляема бъркотия от многопосочни зависимости, което прави грешките вероятни и внедряването на нови функции е неосъществимо. Като използвате някои от тези модели, можете да се надявате да избегнете това.