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

Комментарии 15

По сути у вас получился Service Locator. Когда то пользовался подобным, но несколько извращенным подходом: вместо длинного dataBase писал A — application, данные сетал в статические поля и рефлексией чистил все при выходе из сцены. Получалось A.Player

И все таки есть более предпочтительные решения:

1) использование DI. Наиболее популярные github.com/modesttree/Zenject или github.com/intentor/adic. Но я бы например не стал ничего инжектить внутрь объектов которых несколько десятков на сцене, не проверив как это отразиться на перфомансе.
2) Строгая архитектура по типу «сверху-вниз». Пример — существуют GameController'ы (или Manager'ы, называйте как хочется) которые знают о игровых персонажах, и реагируют на события порождаемые ими. Каждый контроллер на старте регистрируется в службе контроллеров — это позволяет контроллерам получать доступ друг к другу через что нибудь типа Controller < TController .>

3) Сильно уменьшить связность могут помочь глобальные события. Одно из самых простейших решений которое я видел — unity3d.ru/distribution/viewtopic.php?f=13&t=24933 Я лично использую свой велосипед с блекджеком и визуализацией

Подходы можно комбинировать, у каждого есть свои плюсы и минусы. Целесообразность использования варьируется от проекта, его размера (пожалуй в первую очередь), числа программистов, факта использования плейтестов и просто личных предпочтений
Спасибо за ссылки, изучим!
Наткнулся на статью случайно, но она решает давно свербившую у меня проблему.
Спасибо!

Про огонек вопрос легко решается с помощью делегатов. Вполне в духе ООП, и работает исключительно быстро. В момент когда факел вспыхнет, все подписавшиеся на это событие узнают об этом, а там уже можно и корутину запустить, например проверять на видимость игрока мобом 10 раз в секунду на половиннм расстоянии видимости и 3 раза на максимальном. Экономия по сравнению с Update просто гигантская. Главное не забыть отписываться-подписываться при (де)активации

На дворе шёл 2018-ый год, в юнити продолжали обмазываться синглтонами.


Давайте не будем описываемое называть архитектурой?


  1. Ничего "элегантного" в вычислении характеристик персонажа я не вижу. Один класс яростно юзает во все дыры публичные поля структуры, лежащей в публичном поле, лежащей в публичном поле, лежащей в статической переменной. Оооочень элегантно.


  2. Awake. Awake'ов много, никакой уверенности что нужный уже успел сработать на момент когда мы полезли в database, нет. Или наоборот, что игрок еще существует когда мы полезли в database


  3. уничтожать своих двойников

    А, ну да, я забыл что в юнити и синглтон сделать по-человечески не могут. Сцен-то много, а "синглтоны" в каждую распиханы.



Как это всё потом тестировать, как наладить нормальную процедуру шатдауна сцены, с каких вообще пор класть ВСЁ состояние приложения в статическую переменную стало хорошо — адепты синглтонов скромно умалчивают.

  1. согласен
  2. Это же элементарно, Ватсон! записываем по Awake, считываем в Start \o/
  3. Наверное потому что каждая сцена в Unity это скорее. независимое приложение чем часть целого. Динамически подгружать сцены без выгрузки предыдущей только недавно научились. А. уничтожать копию — так в каждой сцене свой объект исключительно для удобства отладки.


  4. как как, как обычно, OnDisable и OnExit

Ваша неприязнь к синглтонам скорее говорит о неумении их готовить.

  1. Вхуж, одним эвентом которым можно безопасно пользоваться в нашей игре стало меньше.
  2. Или синглтон один в рамках приложения или не один и тогда это не синглтон. Не надо вот юлить и оправдывать это удобством. Просто юнити не даёт места где можно было бы заинитить всякое до загрузки сцены, хранить между сценами и уничтожить после выгрузки последней сцены, поэтому приходится городить костыли.
  3. Угу. И молиться что синглтон будет уничтожен последним (конечно нет, юнити ничего не обещает про порядок обработки сущностей)
  4. Моя неприязнь говорит о том что я видел более лучшие архитектурные решения.
  1. Не понятно что вы хотели сказать.
  2. Кто вам мешает сделать сцену-контейнер в которую будут подгружаться все остальные? Пусть в ней все и хранится "между сценами". Также никто вам не мешает удалить все экземпляры синглтона со всех сцен кроме той что первой загрузится.
  3. OnExit() ЕМНИП вызывается последним
  4. А еще лучше свой движок написать с казино и танцовщицами. Видели — опишите
Моя неприязнь говорит о том что я видел более лучшие архитектурные решения.

Не поделитесь знаниями?
с каких вообще пор класть ВСЁ состояние приложения в статическую переменную стало хорошо

К сожалению, я не смог найти ничего более красивого для хранения и сохранения информации о сценах (когда на них десятки предметов и объектов, состояния которых надо помнить). Если в юнити существуют встроенные решения — мне о них неизвестно.
Ничего «элегантного» в вычислении характеристик персонажа я не вижу. Один класс яростно юзает во все дыры публичные поля структуры, лежащей в публичном поле, лежащей в публичном поле, лежащей в статической переменной. Оооочень элегантно.

Если какую-либо информацию использует только один скрипт — не спорю, лучше прописывать её в этом скрипте и не заморачиваться. Но если таких скриптов МНОГО — то приходится либо пилить систему сообщений, либо настраивать паутину ссылок, либо юзать публичные поля публичных полей публичных полей.

К слову, не поясните, в чем природа вашего отторжения сиглтонов и статических переменных?
Сам пришел примерно к такой же связке GameController-ScriptableObject.

Представим, что у игрока есть заклинание огонька. Игрок его кастует, и игра говорит хранилищу: огонек скастован!

А не лучше ли здесь просто через event реализовать?

Написать ИИ? ScriptableObject!

Можно пример привести?
А не лучше ли здесь просто через event реализовать?

Лучше. Но как я понимаю, система ивентов несколько хромает в плане перформанса. Поэтому я использовал собственное решение.

Можно пример привести?

unity3d.com/learn/tutorials/topics/navigation/finite-state-ai-delegate-pattern

Если кратко — пишем машину состояний, где каждое действие машины хранится в виде отдельного ScriptableObject'a. И каждый переход между состояниями.

А потом создаем EnemyStateController, который управляет своими состояниями. Он подаёт информацию о себе в ScriptableObject'ы, они занимаются логикой и возвращают изменения обратно контроллеру.

Таким образом, можно комбинировать различные действия для получения различных состояний, править действия состояний отдельно друг от друга, создавать разные экземпляры действий для разных типов врагов…
Основные преимущества — модульность и удобство. Можно с написанной логикой работать прямо в инспекторе, создавая состояния и раскидывая по ним действия.
Но и тут есть свои проблемы и особенности реализации.

Плохо понимаете. Система встроенных ивентов довольно медленная да, делегаты намного быстрее. Тут даже статья есть на тему: https://m.habr.com/post/353780/
По сути, я так понимаю, это тот же прямой вызов перебором всех подписавшихся.

Довольно интересная система, как раз использую очень похожую.

В синглтоне будет удобно использовать такую конструкцию для хранения объектов, которые могут изменяться. Публичное событие позволяет всем заинтересованным подписываться на изменения.

public class ActionProperty<T>
{
    protected T mValue;
    public event Action<T> Changed;

    public virtual T Value
    {
        get { return mValue; }
        set
        {
            if (mValue == null || !mValue.Equals(value))
            {
                mValue = value;
                if (Changed != null)
                    Changed(mValue);
            }

        }
    }
}

public class GameController : MonoBehaviour
{
	public ActionProperty<MyType> MyParam;
        ...
}


Соответственно, на объекте это будет выглядеть примерно так:
public class MyClass: MonoBehaviour
{
    private void Start()
    {
        GameController.Instance.MyParam.Changed += OnMyParamChanged;
    }

    public void OnMyParamChanged(MyType value)
    {
        //Здесь происходит реакция на изменение значения параметра
    }

    private void OnDestroy()
    {
        GameController.Instance.MyParam.Changed -= OnMyParamChanged;
    }
}
Мне всегда казалось что ScriptableObject чисто для удобства работы геймдизайнеров. Разработчик написал GUI прямо в редакторе, а они сидят и клепают, например, предметы для игры, или мобов, как в конструкторе, настраивают характеристики, иконки, меши и т.д.
Сам использовал только раз для хранения данных об объектах из которых строился уровень, по определенному ключу (который был в сериализованном файле с данными уровня) искался объект и инстанцировался в нужной точке с опеределеным мешем, скином и другими характеристиками.
Да и с приходом официального ECS в юнити, нужно перестраивать немного мозги в сторону другой парадигмы.
Спасибо за статью, но всё же оставлю пару комментариев.

1. Используйте ECS.
2. Если есть возможность, не используйте MonoBehaviour Singleton в Unity. MonoBehaviour совсем плох для этого паттерна (а иногда и антипаттерна).
3. Код
if (GameController.Instance.database.playerData.isSprinting)
	ActivateTrap();
содержит в себе множество проблем. Не пишите так.
4. Ошибка в неправильно поставленой задаче:

Иными словами, я не могу создать ссылку на игрока

Зачем Вам ссылка на игрока? Вся статья про то, что Вы пытаетесь сохранить где-то ссылку, теряете её, сохраняете другим способом и т.д. Вероятнее всего, вам нужна не ссылка на игрока, а данные этого игрока и возможность вызывать методы этого игрока — ссылка на игрока для этого не обязательна.
5. Если нужно, чтобы данные Player можно было получить из любой точки кода и не хочется использовать какой-нибудь EventBus, то можно написать так:
public static Action<Player> OnPlayerInstantiated;

void Start()
{
    OnPlayerInstantiated?.Invoke(this);
}

public static Action<Player, int> OnHealthChanged;
private int health;
public int Health
{
    get
    {
        return health;
    }
    protected set
    {
        if (health != value)
        {
            health = value;
            OnHealthChanged?.Invoke(this, value);
        }
    }
}

6.
public List<Enemy> enemies;
	public List<Spell> spells;

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

а вот когда на переменную начинают воздействовать из разных мест — это потенциальная угроза.

Верно. Исключите эту возможность с помощью не публичного Setter.

не следует менять playerSpeed в хранилище, следует получать её, сохранять во временную

Просто следует сделать ей публичный Getter. Нужно писать код так, чтобы тот, кто не знает реализацию не мог использовать API неправильно.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.