Работа со статусами персонажа. Эксперименты в Unity

Разрабатывая игру на Unity я столкнулся с интересной задачей: как сделать расширяемое время действия негативных или положительных эффектов на персонаже.

Вкратце у меня есть персонаж, к которому могут применяться некие эффекты, как ослабление, усиление, повышение скорости, снижение скорости и прочие. Чтобы оповещать игрока о действии того или иного эффекта, в игре предусмотрена статусная строка.

Первые версии этой строки содержали затемненные иконки всех статусов и при наступлении эффекта, загоралась нужная.

image

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

  1. Совсем неправильный: Запускается вторая корутина параллельно первой. Когда первая завершается, она возвращает исходные значения, то есть эффект снимается раньше, чем вторая корутина закончила работу.

    image
  2. Тоже неправильный, но в некоторых случаях приемлемый: Отменять первую корутину и запускать вторую. В этом случае время действия эффекта будет равно времени действия первого эффекта до момента отмены корутины + время действия второй корутины.

    image

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

Если персонаж наступает на шипы, у него условно повреждается нога и он не может продолжать двигаться с прежней скоростью. Допустим скорость снижается на 5 секунд. Если через 3 секунды персонаж наступает на другие шипы, то скорость должна быть снижена еще на 5 секунд. То есть 3 секунды прошло, 2 осталось + 5 секунд от новых шипов. Время действия эффекта должно продлиться еще 7 секунд с момента наступления на вторые шипы (10 в целом).

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

Решение, которое я нашел этой задаче, состоит в использовании словаря (Dictionary). Его преимущество перед List в том, что у словаря есть ключ и значение, а значит я могу обратиться к любому значению по ключу. Плюс это решение позволяет избавиться от постоянных статусных иконок в строке и включать нужные по мере необходимости и устанавливать их в позиции в строке в порядке их наступления.

Dictionary<string, float> statusTime = new Dictionary<string, float>();

Словарь в данном случае выгоднее использования очереди, так как очередь работает по принципам First In First Out, но время действия эффектов разное, а значит статус, который надо снять, может стоять не первым в очереди.

Для этого я добавил три метода.

AddStatus

Добавляем нужный статус в словарь, если такой статус в словаре уже есть, то прибавляем время действия. Если статуса нет, то вычисляем время окончания и добавляем в словарь.

private void AddStatus(string status, float duration)

{
    if (statusTime.ContainsKey(status))
    {

            statusTime[status] += duration;

    }

    else

    {

            float endTime = Time.timeSinceLevelLoad + duration;

            statusTime.Add(status, endTime);

    }
}

RemoveStatus

Удаляем статус из словаря, восстанавливаем исходные значения.

private void RemoveStatus(string status)

{

        statusTime.Remove(status);

        RestoreStats(status);

}

CheckStatus

Если в словаре есть статусы, то проверяем, не истекло ли время их действия.

Если истекло, то удаляем статус из словаря. Так как изменения словаря в цикле приводит к невозможности синхронизации значений словаря, то перекидываем тут ключи словаря в обычный List.

private void CheckStatuses()

    {

        if (statusTime.Count > 0)

        {

            float currTime = Time.timeSinceLevelLoad;

            List<string> statuses = new List<string>(statusTime.Keys);


            foreach (string stat in statuses)

            {

                if (currTime > statusTime[stat])

                {

                    RemoveStatus(stat);

                }

            }

        }

    }

Из плюсов, очевидно, это расширяемое время действия эффектов. То есть задача решена.
Но и довольно значительный минус тут присутствует. Проверка на наличие статусов осуществляется в Update каждый фрейм. В моей игре присутствует максимум 4 игрока, а значит этот метод будет выполняться каждый фрейм 4 раза параллельно. При 4 персонажах, я думаю, это не критично, но уверен, что при большем количестве персонажей, это может вызвать проблемы производительности. Стоит так же отметить, что метод защищен проверкой на наличие в словаре элементов, и при пустом словаре нагрузка должна снижаться.

Забегая в будущее (которое пока совершенно туманно для этой игры), я так же уверен в этом решении и в онлайн режиме, так как проверка на статусы игрока будет происходить только для текущего локального игрока, а не для всех инстанциированных игроков.
Share post

Similar posts

Comments 17

    +2
    Режим зануды
    Серьезно? Статья о том, как зажечь/погасить иконку? О взаимодействии потоков корутин?

    А как же MVP, например? Читать данные из модели и реагировать на события об ее изменении? И таких проблем бы не было… У вас вьюшка содержит игровую логику?
      –2
      Спасибо за комментарий. Статья не об иконках. Видимо начал неудачно. Иконки как раз зажигаются и гаснут по событию на персонаже.
      Задача была именно в том, чтобы правильно отслеживать время, сколько эффект должен длиться на персонаже.
      –1
      В текущей реализации сразу напрашивается CheckStatuses()
 сделать корутиной, пробегая по статусам определять минимальный период когда будет смена какого-то из статусов ака minStatusTime и засыпать на это время.
        +1
        Корутины не работают как потоки, так что ничего сэкономить тут не получится. Только проверок добавится.
        0
        А почему не реализовать каждый «статус» как компонент и не делать например так:
        _character.AddComponent<Curse>()

        Тогда и не нужны корутины — каждый «статус» в своём Start() начинает отсчёт времени, добавляет иконку если ее еще нет, в Update(), изменяет поведение персонажа в зависимости от своей логики и времени действия, а в OnDestroy() заканчивает своё действие.
          0
          То есть перед добавлением статуса проверять, есть ли уже такой компонент на персонаже и если есть, то добавлять время. А если нет, то добавлять компонент. И компонент сам будет управлять, сколько ему осталось жить.
          Очень любопытно, спасибо за идею. Подумаю над этим.
            +1
            Не производительный подход.
            OnDestroy вообще лучше в процессе игры не использовать, особенно на мобильных устройствах, лучше выключать компонент/объект, ибо OnDestroy требовательный + лишняя загрузка GC. Лучше инициировать в начале сцены, и затем использовать OnEnable/OnDisable — но не уверен что вызовы вызываются если только компонент выключается/включается, не было необходимости проверять это.
            Манипулировать уже инициализированными переменными намного проще и быстрее, чем создавать новые компоненты в процессе игры.
              +1
              Можно заранее добавить все компоненты на персонажа. Включать компонент enabled = true. И сделать метод на компоненте, который запускает отсчет времени и эффект. Когда время выходит: отключать эффект, enabled = false.
              Мне кажется это способ аналогичен вашему, так как каждый компонент будет иметь локальную переменную, отвечающую за время, с ограничением на то, что только необходимые компоненты будут включаться.
                0
                Такой вариант тоже жизнеспособен, тут уже много вариантов реализации — какой самый правильный и удобный уже каждый решит сам) Тут просто сам факт что проще завести локальную переменную и работать с ней, чем работать с массивом.
                  0
                  Спасибо за ценные идеи/советы! Этот кусок я буду переписывать наверное уже раз в четвертый.
            +1
            Если вы все равно проверяете это в Update, то почему нельзя завести переменную которая будет отвечать за время эффекта? Т.е. заводите float SlowTime. Пока она равна 0, то скорость обычная, как только накладывается эффект, то к ней прибавляется 5 (секунд), которые в Update уменьшаются по deltaTime. Как только ставиться 0, возвращаем скорость. Попадает на дополнительный эффект, просто прибавляем к SlowTime еще Х секунд.
            Это будет намного производительнее и проще.
              –1
              У меня была такая мысль. В этом случае в Update будут проверяться переменные статусов, которые могут ни разу не наступить за все время сцены. Это меня и пугает, что за время, проведенное на этой сцене, 8 из 13 эффектов могут не сработать, но в Update на 4х персонажах эти переменные все равно будут проверяться на ноль каждый фрейм.
                0
                Создай Event и подписывай нужный Update только тогда, когда на игрока применился эффект… как только эффект спал, отписывайся от обновления и все. В итоге проверка будет работать только в случае если на игроке какой либо эффект.
                  0
                  Например у меня в игре, в моем коде, ровно один Update, FixedUpdate и LateUpdate которые запускают каждый свой Event, и в других скриптах где надо я подписываюсь на обновления, а где надо, отписываюсь. Update сами по себе вызываются медленно, и чем меньше их в проекте тем лучше.
                    0
                    13 * 4 = 52 проверки переменной на ноль за кадр это не просто быстро, это фактически бесплатно на данный момент. Я даже не уверен, что поддержка мэпа не обойдется дороже, если там будут постоянно добавляться/удаляться элементы, не знаю, как быстро в шарпе память для элементов мэпа аллоцируется. Пока там не будет хотя бы сотни эффектов на пару десятков персонажей, влияние на производительность даже из статистической погрешности не выберется. Т.е. как способ уменьшения лапши в коде продуманная система баффов/дебаффов это хорошо и удобно, но с т.з. перфоманса скорее всего лучше будет сконцентрировать силы на чем-то еще
                  0
                  Советую автору в качестве моделей присмотреться к ESC (https://habr.com/company/pixonic/blog/413729/).
                    –1
                    Слышал об этом (https://unity3d.com/ru/learn/tutorials/topics/scripting/introduction-ecs?playlist=17117).
                    Спасибо за ссылку, почитаю. Пока для меня это Rocket Science. Мне бы в своем болоте порядок навести :)

                  Only users with full accounts can post comments. Log in, please.