G-Unity architecture

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В этой статье я бы хотел поговорить об архитектурном решении для 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? Ответ прост - Dependency injection.

  • Dependency Injection

    Архитектура предоставляет нам доступ к пулу данных. Чтобы им воспользоваться, мы создаём некую прослойку - 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.

Источник: https://habr.com/ru/post/571956/


Интересные статьи

Интересные статьи

Решения для больших компаний обычно должны выдерживать высокие нагрузки. Когда в штате много десятков тысяч человек, и значительная доля из них ежедневно пользуются ...
В прошлой части мы поговорили о советах директору по разработке бизнес-процесса в Битрикс24, сейчас же я постараюсь дать советы руководителям отделов, поскольку по моему опыту почти всегд...
Маркетплейс – это сервис от 1С-Битрикс, который позволяет разработчикам делиться своими решениями с широкой аудиторией, состоящей из клиентов и других разработчиков.
Если в вашей компании хотя бы два сотрудника, отвечающих за работу со сделками в Битрикс24, рано или поздно возникает вопрос распределения лидов между ними.
Существует традиция, долго и дорого разрабатывать интернет-магазин. :-) Лакировать все детали, придумывать, внедрять и полировать «фишечки» и делать это все до открытия магазина.