Разрабатывая игру на Unity я столкнулся с интересной задачей: как сделать расширяемое время действия негативных или положительных эффектов на персонаже.
Вкратце у меня есть персонаж, к которому могут применяться некие эффекты, как ослабление, усиление, повышение скорости, снижение скорости и прочие. Чтобы оповещать игрока о действии того или иного эффекта, в игре предусмотрена статусная строка.
Первые версии этой строки содержали затемненные иконки всех статусов и при наступлении эффекта, загоралась нужная.
На каждом статусе была корутина, которая отменяла эффект через заданное количество времени.
В этом решении есть довольно важный минус. Если в результате неких событий в игре на персонажа накладывается одинаковый эффект через время меньшее, чем время действия предыдущего аналогичного эффекта, то здесь могут быть два варианта событий.
Для моей задачи оба методы неприемлемы, так как мне надо, чтобы время действия эффекта продлевалось, чтобы я получил в итоге сумму по времени действия каждого наступившего эффекта вне зависимости от того, сколько раз эффект применялся.
Если персонаж наступает на шипы, у него условно повреждается нога и он не может продолжать двигаться с прежней скоростью. Допустим скорость снижается на 5 секунд. Если через 3 секунды персонаж наступает на другие шипы, то скорость должна быть снижена еще на 5 секунд. То есть 3 секунды прошло, 2 осталось + 5 секунд от новых шипов. Время действия эффекта должно продлиться еще 7 секунд с момента наступления на вторые шипы (10 в целом).
И с помощью корутин я не нашел решения задачи, так как нельзя узнать оставшееся время до завершения корутины, чтобы прибавить его к новой корутине.
Решение, которое я нашел этой задаче, состоит в использовании словаря (Dictionary). Его преимущество перед List в том, что у словаря есть ключ и значение, а значит я могу обратиться к любому значению по ключу. Плюс это решение позволяет избавиться от постоянных статусных иконок в строке и включать нужные по мере необходимости и устанавливать их в позиции в строке в порядке их наступления.
Словарь в данном случае выгоднее использования очереди, так как очередь работает по принципам First In First Out, но время действия эффектов разное, а значит статус, который надо снять, может стоять не первым в очереди.
Для этого я добавил три метода.
AddStatus
Добавляем нужный статус в словарь, если такой статус в словаре уже есть, то прибавляем время действия. Если статуса нет, то вычисляем время окончания и добавляем в словарь.
RemoveStatus
Удаляем статус из словаря, восстанавливаем исходные значения.
CheckStatus
Если в словаре есть статусы, то проверяем, не истекло ли время их действия.
Если истекло, то удаляем статус из словаря. Так как изменения словаря в цикле приводит к невозможности синхронизации значений словаря, то перекидываем тут ключи словаря в обычный List.
Из плюсов, очевидно, это расширяемое время действия эффектов. То есть задача решена.
Но и довольно значительный минус тут присутствует. Проверка на наличие статусов осуществляется в Update каждый фрейм. В моей игре присутствует максимум 4 игрока, а значит этот метод будет выполняться каждый фрейм 4 раза параллельно. При 4 персонажах, я думаю, это не критично, но уверен, что при большем количестве персонажей, это может вызвать проблемы производительности. Стоит так же отметить, что метод защищен проверкой на наличие в словаре элементов, и при пустом словаре нагрузка должна снижаться.
Забегая в будущее (которое пока совершенно туманно для этой игры), я так же уверен в этом решении и в онлайн режиме, так как проверка на статусы игрока будет происходить только для текущего локального игрока, а не для всех инстанциированных игроков.
Вкратце у меня есть персонаж, к которому могут применяться некие эффекты, как ослабление, усиление, повышение скорости, снижение скорости и прочие. Чтобы оповещать игрока о действии того или иного эффекта, в игре предусмотрена статусная строка.
Первые версии этой строки содержали затемненные иконки всех статусов и при наступлении эффекта, загоралась нужная.
На каждом статусе была корутина, которая отменяла эффект через заданное количество времени.
В этом решении есть довольно важный минус. Если в результате неких событий в игре на персонажа накладывается одинаковый эффект через время меньшее, чем время действия предыдущего аналогичного эффекта, то здесь могут быть два варианта событий.
- Совсем неправильный: Запускается вторая корутина параллельно первой. Когда первая завершается, она возвращает исходные значения, то есть эффект снимается раньше, чем вторая корутина закончила работу.
- Тоже неправильный, но в некоторых случаях приемлемый: Отменять первую корутину и запускать вторую. В этом случае время действия эффекта будет равно времени действия первого эффекта до момента отмены корутины + время действия второй корутины.
Для моей задачи оба методы неприемлемы, так как мне надо, чтобы время действия эффекта продлевалось, чтобы я получил в итоге сумму по времени действия каждого наступившего эффекта вне зависимости от того, сколько раз эффект применялся.
Если персонаж наступает на шипы, у него условно повреждается нога и он не может продолжать двигаться с прежней скоростью. Допустим скорость снижается на 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 персонажах, я думаю, это не критично, но уверен, что при большем количестве персонажей, это может вызвать проблемы производительности. Стоит так же отметить, что метод защищен проверкой на наличие в словаре элементов, и при пустом словаре нагрузка должна снижаться.
Забегая в будущее (которое пока совершенно туманно для этой игры), я так же уверен в этом решении и в онлайн режиме, так как проверка на статусы игрока будет происходить только для текущего локального игрока, а не для всех инстанциированных игроков.
Tutanhomon
потоковкорутин?Mak5AM Автор
Спасибо за комментарий. Статья не об иконках. Видимо начал неудачно. Иконки как раз зажигаются и гаснут по событию на персонаже.
Задача была именно в том, чтобы правильно отслеживать время, сколько эффект должен длиться на персонаже.
Shortki
В текущей реализации сразу напрашивается CheckStatuses()? сделать корутиной, пробегая по статусам определять минимальный период когда будет смена какого-то из статусов ака minStatusTime и засыпать на это время.
FadeToBlack
Корутины не работают как потоки, так что ничего сэкономить тут не получится. Только проверок добавится.
rumyancevpavel
А почему не реализовать каждый «статус» как компонент и не делать например так:
Тогда и не нужны корутины — каждый «статус» в своём Start() начинает отсчёт времени, добавляет иконку если ее еще нет, в Update(), изменяет поведение персонажа в зависимости от своей логики и времени действия, а в OnDestroy() заканчивает своё действие.
Mak5AM Автор
То есть перед добавлением статуса проверять, есть ли уже такой компонент на персонаже и если есть, то добавлять время. А если нет, то добавлять компонент. И компонент сам будет управлять, сколько ему осталось жить.
Очень любопытно, спасибо за идею. Подумаю над этим.
Snakes911
Не производительный подход.
OnDestroy вообще лучше в процессе игры не использовать, особенно на мобильных устройствах, лучше выключать компонент/объект, ибо OnDestroy требовательный + лишняя загрузка GC. Лучше инициировать в начале сцены, и затем использовать OnEnable/OnDisable — но не уверен что вызовы вызываются если только компонент выключается/включается, не было необходимости проверять это.
Манипулировать уже инициализированными переменными намного проще и быстрее, чем создавать новые компоненты в процессе игры.
Mak5AM Автор
Можно заранее добавить все компоненты на персонажа. Включать компонент enabled = true. И сделать метод на компоненте, который запускает отсчет времени и эффект. Когда время выходит: отключать эффект, enabled = false.
Мне кажется это способ аналогичен вашему, так как каждый компонент будет иметь локальную переменную, отвечающую за время, с ограничением на то, что только необходимые компоненты будут включаться.
Snakes911
Такой вариант тоже жизнеспособен, тут уже много вариантов реализации — какой самый правильный и удобный уже каждый решит сам) Тут просто сам факт что проще завести локальную переменную и работать с ней, чем работать с массивом.
Mak5AM Автор
Спасибо за ценные идеи/советы! Этот кусок я буду переписывать наверное уже раз в четвертый.
Snakes911
Если вы все равно проверяете это в Update, то почему нельзя завести переменную которая будет отвечать за время эффекта? Т.е. заводите float SlowTime. Пока она равна 0, то скорость обычная, как только накладывается эффект, то к ней прибавляется 5 (секунд), которые в Update уменьшаются по deltaTime. Как только ставиться 0, возвращаем скорость. Попадает на дополнительный эффект, просто прибавляем к SlowTime еще Х секунд.
Это будет намного производительнее и проще.
Mak5AM Автор
У меня была такая мысль. В этом случае в Update будут проверяться переменные статусов, которые могут ни разу не наступить за все время сцены. Это меня и пугает, что за время, проведенное на этой сцене, 8 из 13 эффектов могут не сработать, но в Update на 4х персонажах эти переменные все равно будут проверяться на ноль каждый фрейм.
Snakes911
Создай Event и подписывай нужный Update только тогда, когда на игрока применился эффект… как только эффект спал, отписывайся от обновления и все. В итоге проверка будет работать только в случае если на игроке какой либо эффект.
Snakes911
Например у меня в игре, в моем коде, ровно один Update, FixedUpdate и LateUpdate которые запускают каждый свой Event, и в других скриптах где надо я подписываюсь на обновления, а где надо, отписываюсь. Update сами по себе вызываются медленно, и чем меньше их в проекте тем лучше.
SmallSnowball
13 * 4 = 52 проверки переменной на ноль за кадр это не просто быстро, это фактически бесплатно на данный момент. Я даже не уверен, что поддержка мэпа не обойдется дороже, если там будут постоянно добавляться/удаляться элементы, не знаю, как быстро в шарпе память для элементов мэпа аллоцируется. Пока там не будет хотя бы сотни эффектов на пару десятков персонажей, влияние на производительность даже из статистической погрешности не выберется. Т.е. как способ уменьшения лапши в коде продуманная система баффов/дебаффов это хорошо и удобно, но с т.з. перфоманса скорее всего лучше будет сконцентрировать силы на чем-то еще
CorvOrk
Советую автору в качестве моделей присмотреться к ESC (https://habr.com/company/pixonic/blog/413729/).
Mak5AM Автор
Слышал об этом (https://unity3d.com/ru/learn/tutorials/topics/scripting/introduction-ecs?playlist=17117).
Спасибо за ссылку, почитаю. Пока для меня это Rocket Science. Мне бы в своем болоте порядок навести :)