Как стать автором
Обновить

G-Unity architecture

Время на прочтение5 мин
Количество просмотров4K

В этой статье я бы хотел поговорить об архитектурном решении для Unity - GUA, объяснить логику работы. Если мы перейдём по ссылке нас встречает великолепное readme, где описаны правила работы с данным решением, но я бы хотел разобрать их подробнее с примерами. Пусть это будет бесплатной рекламой для автора.

  • Чтобы начать работу достаточно просто создать проект, скачать и скопировать папку GUA в папку Assets проекта.


  • Одним из преимуществ этой архитектуры является единая точка входа, чего нам всем частенько не хватает в unity. Когда unity закончит подготовку, нам станет доступно окно редактора стартера (Create/GUA/Creator).

Окно редактора стартера
Окно редактора стартера

Оно упрощает процесс создания стартера. Думаю, интуитивно понятно, что делает каждая из настроек, кроме разве что Create Editor Script, но это мы разберём немного позже. По нажатию Create сгенерируется файл следующего содержания

using GUA.Invoke;
using GUA.System;
using UnityEngine;

namespace Example
{
    public class ExampleStarter : MonoBehaviour
    {
        private readonly GSystem _system = new GSystem();

        // [Header("Emitters")]
        // [SerializeField] private SomeEmitter someEmitter;

        // [Header("Data")] 
        // [SerializeField] private SomeData someData;

        private void Start()
        {
            Application.targetFrameRate = 60;
            InitializeAssistants();
        
            // GDataPool.Set(someEmitter);
        
            // GDataPool.Set(someData);

            // _system.Add(new SomeSystem());
      
            _system.Initialize();
        }

        private void InitializeAssistants()
        {
            _ = GInvoke.Instance;
        }

        private void Update() => _system.Run();

        private void FixedUpdate() => _system.FixedRun();

        private void OnApplicationQuit() => _system.ApplicationQuit();

        private void OnDestroy() => _system.Destroy();
    }
}

По сути в нашей системе только этот класс использует методы Monobehavior: Start() и Update(). Все скрипты, которые мы создаём, могут реализовывать 4 интерфейса:

  1. ISystem - не требует ничего, но позволяет встраивать систему в список систем.

  2. IStartSystem - то же, что и ISystem, но требует реализации функции Start()

  3. IRunSystem - то же, что и ISystem, но требует реализации функции Run()

  4. IFixedRunSystem - то же, что и ISystem, но требует реализации функции FixedRun()

Всё, что нам остаётся сделать, чтобы воспользоваться старым добрым Start, реализовать в нашем скрипте интерфейс IStartSystem и в стартере добавить нашу систему в список систем в стартере.

using GUA.System;

namespace Example
{
    public class ExampleSystem : IStartSystem
    {
        public void Start()
        {
            // Your start logic
        }
    }
}
private void Start()
{
    Application.targetFrameRate = 60;
    InitializeAssistants();

    _system.Add(new ExampleSystem());
      
    _system.Initialize();
}

Остальные интерфейсы реализуются аналогичным способом.

Такой способ реализации позволяет нам чётко контролировать, какие системы и в каком порядке будут вызваны на сцене.


На этом месте возникает вопрос. А как же наши систему будут оперировать объектами на сцене, если их нельзя наследовать от Monobehavior? Ответ прост...

  • Service Locator

    Архитектура предоставляет нам доступ к пулу данных. Чтобы им воспользоваться, мы создаём некую прослойку - Emitter, он просто хранит в себе ссылки на объекты на сцене, а системы получают объект-Emitter из пула и работают с данными через него. (чем-то похоже на логику scriptable object только для конкретной сцены)

    Давайте будем задавать hp нашему игроку на старте. Реализация будет состоять из GameStarter (Точка входа), PlayerHealthbarSystem (Система для контроля hp игрока), PlayerHealthbarEmitter (Прослойка с данными) и компонента PlayerHealthbar, который непосредственно ничего не выполняет, но назначен на некоторый объект на сцене. В коде это будет выглядеть примерно так:

using GUA.Data;
using GUA.Invoke;
using GUA.System;
using UnityEngine;

namespace Example
{
    public class GameStarter : MonoBehaviour
    {
        private readonly GSystem _system = new GSystem();

        [Header("Emitters")]
        [SerializeField] private PlayerHealthbarEmitter playerHealthbarEmitter;

        private void Start()
        {
            Application.targetFrameRate = 60;
            InitializeAssistants();

            GDataPool.Set(playerHealthbarEmitter);

            _system.Add(new PlayerHealthbarSystem());
      
            _system.Initialize();
        }

        private void InitializeAssistants()
        {
            _ = GInvoke.Instance;
        }

        private void Update() => _system.Run();

        private void FixedUpdate() => _system.FixedRun();

        private void OnApplicationQuit() => _system.ApplicationQuit();

        private void OnDestroy() => _system.Destroy();
    }
}
using GUA.Data;
using GUA.System;

namespace Example
{
    public class PlayerHealthbarSystem : IStartSystem
    {
        private readonly PlayerHealthbarEmitter _healthbarEmitter = 
        GDataPool.Get<PlayerHealthbarEmitter>();

        public void Start()
        {
            _healthbarEmitter.Healthbar.SetHealthPoints(100);
        }
    }
}
using UnityEngine;

namespace Example
{
    public class PlayerHealthbarEmitter : MonoBehaviour
    {
        public PlayerHealthbar Healthbar;
    }
}
using UnityEngine;

namespace Example
{
    public class PlayerHealthbar : MonoBehaviour
    {
        private int _healthPoints;

        public void SetHealthPoints(int healthPoints)
        {
            _healthPoints = healthPoints;
            Debug.Log(_healthPoints);
        }
    }
}

Результат работы

Система не находится непосредственно на сцене, но оперирует её объектами. Здесь стоит отметить, что GDataPool является словарём, поэтому несколько одинаковых компонентов положить туда не получится, однако можно укомплектовать их в другой уникальный компонент.

  • Аналог корутин

    Последнее из заменителей встроенных в Unity методов - это GInvoke, аналог корутин. Давайте вызовем вывод в консоль некоторого сообщения через 1.5 секунды после старта. Система будет выглядель так

using GUA.System;
using GUA.Invoke;
using UnityEngine;

namespace Example
{
    public class ExampleSystem : IStartSystem
    {
        public void Start()
        {
            Debug.Log("Start");
            GInvoke.Instance.Delay(() => SomeAction(), 1.5f);
            
            // Аналогичная запись
            GInvoke.Instance.Delay(() =>
            {
                SomeAction();
            }, 1.5f);
        }

        private void SomeAction()
        {
            Debug.Log("Action");
        }
    }
}

Если не забудем добавить систему в список в стартере, получим:

Сообщения выводятся в консоль через 1.5 секунды после старта
Сообщения выводятся в консоль через 1.5 секунды после старта
  • Общение между системами

    Существует аналог GDataPool, но для обмена данными между системами. GEventPool позволяет отправлять готовые объекты с данными, а все системы, подписанные на сообщения смогут их получить и обработать. В качестве событий настоятельно рекомендуется использовать структуры вместо классов.

    Для примера создадим систему врага и отправим сообщение об атаке, чтобы система игрока смогла принять урон.

using GUA.System;
using GUA.Invoke;
using GUA.Event;

namespace Example
{
    public class EnemySystem : IStartSystem
    {
        private readonly int _damageAmount = 10;

        public void Start()
        {
            GInvoke.Instance.Delay(() => CauseDamage(), 2f);
        }

        private void CauseDamage()
        {
            GEventPool.SendMessage(new DamageEvent { DamageAmount = _damageAmount });
        }
    }
}
using GUA.Data;
using GUA.System;
using GUA.Event;

namespace Example
{
    public struct DamageEvent
    {
        public int DamageAmount;
    }

    public class PlayerHealthbarSystem : IStartSystem
    {
        private int _health;
        private readonly PlayerHealthbarEmitter _healthbarEmitter = 
        GDataPool.Get<PlayerHealthbarEmitter>();

        public void Start()
        {
            _health = 100;
            UpdateHealth();

            GEventPool.AddListener<DamageEvent>(e => TakeDamage(e.DamageAmount));
        }

        private void TakeDamage(int amount)
        {
            _health -= amount;
            UpdateHealth();
        }

        private void UpdateHealth()
        {
            _healthbarEmitter.Healthbar.SetHealthPoints(_health);
        }
    }
}
Одна система наносит урон, а другая применяет этот урон
Одна система наносит урон, а другая применяет этот урон

Вот таким нехитрым способом реализуется система обмена сообщениями между системами.

  • Singleton

    Здесь всё очень просто, мы наследуем класс SomeClass от обёртки Singleton<SomeClass> и наслаждаемся готовым синглтоном.

using GUA.Extension;
using UnityEngine;

namespace Example
{
    public class SingletonExample : Singleton<SingletonExample>
    {
        public void SomeAction()
        {
            Debug.Log("Some action");
        }
    }
}
using GUA.System;

namespace Example
{
    public class ExampleSystem : IStartSystem
    {
        public void Start()
        {
            SingletonExample.Instance.SomeAction();
        }
    }
}
Результат работы singleton
Результат работы singleton

На этом прелести архитектуры заканчиваются. Надеюсь, я смог понятно и интересно описать работу с данной архитектурой. В одной из следующих статей будут рассмотрены плюсы и минусы такого подхода и мы ещё больше углубимся в проектирование систем Unity.

Теги:
Хабы:
Всего голосов 6: ↑3 и ↓3+2
Комментарии4

Публикации

Истории

Работа

Ближайшие события