Разрабатывая игру на Unity я столкнулся с интересной задачей: как сделать расширяемое время действия негативных или положительных эффектов на персонаже.
Вкратце у меня есть персонаж, к которому могут применяться некие эффекты, как ослабление, усиление, повышение скорости, снижение скорости и прочие. Чтобы оповещать игрока о действии того или иного эффекта, в игре предусмотрена статусная строка.
Первые версии этой строки содержали затемненные иконки всех статусов и при наступлении эффекта, загоралась нужная.
На каждом статусе была корутина, которая отменяла эффект через заданное количество времени.
В этом решении есть довольно важный минус. Если в результате неких событий в игре на персонажа накладывается одинаковый эффект через время меньшее, чем время действия предыдущего аналогичного эффекта, то здесь могут быть два варианта событий.
- Совсем неправильный: Запускается вторая корутина параллельно первой. Когда первая завершается, она возвращает исходные значения, то есть эффект снимается раньше, чем вторая корутина закончила работу.
- Тоже неправильный, но в некоторых случаях приемлемый: Отменять первую корутину и запускать вторую. В этом случае время действия эффекта будет равно времени действия первого эффекта до момента отмены корутины + время действия второй корутины.
Для моей задачи оба методы неприемлемы, так как мне надо, чтобы время действия эффекта продлевалось, чтобы я получил в итоге сумму по времени действия каждого наступившего эффекта вне зависимости от того, сколько раз эффект применялся.
Если персонаж наступает на шипы, у него условно повреждается нога и он не может продолжать двигаться с прежней скоростью. Допустим скорость снижается на 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 персонажах, я думаю, это не критично, но уверен, что при большем количестве персонажей, это может вызвать проблемы производительности. Стоит так же отметить, что метод защищен проверкой на наличие в словаре элементов, и при пустом словаре нагрузка должна снижаться.
Забегая в будущее (которое пока совершенно туманно для этой игры), я так же уверен в этом решении и в онлайн режиме, так как проверка на статусы игрока будет происходить только для текущего локального игрока, а не для всех инстанциированных игроков.
Автор: Mak5AM