Простой способ распределить обновления игры по нескольким фреймам

Некоторые части игровой логики могут не выполняться каждый кадр. Например, для неигровых персонажей может быть нормально, если их решения, их сенсоры или их навигационные запросы выполняются только один раз в несколько секунд. Это может помочь увеличить частоту кадров.

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

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

Проблема с этим подходом заключается в том, что нагрузка не распределяется по нескольким кадрам. Если количество объектов и стоимость задачи будут высокими, это приведет к скачкам частоты кадров. Другой способ - установить максимальное количество задач на кадр. Слева мы устанавливаем максимум 1 задачу на кадр, а справа - максимум 4 задачи на кадр:

Преимущество этого подхода заключается в распределении нагрузки, но основная проблема заключается в том, что время между обновлениями зависит от количества сущностей. Если количество сущностей изменится в ходе игры, изменится их поведение. Для обеспечения единообразия может быть лучше принудительно выполнять задачу каждые [x] секунд независимо от количества объектов (или каждые [x] кадров, если частота кадров фиксирована). На следующем изображении представлена ​​эта концепция с двумя объектами, обновляющимися каждые 0,5 секунды:

Иногда нам нужно выполнить 0 задач на фрейме. Когда количество объектов увеличивается, нам, возможно, придется выполнять несколько задач в каждом кадре:

E5 обновляется в кадре 0, потому что мы хотим, чтобы E5 обновлялся в кадре 4, и мы всегда хотим сохранить тот же интервал. Обратите внимание, что на самом деле не имеет значения, выбираем ли мы обновлять E1 и E2 в кадре 0 вместо E1 и E5. Для нашей цели следующее эквивалентно или лучше, поскольку оно правильно упорядочено:

На самом деле заставить это работать довольно просто. В следующем коде показана реализация в Unity.

// The interval in seconds at which we want the tasks to be performed 
float m_updateInterval = 0.5f;
// The tasks to execute. 
List<Action> m_items = new List<Action>();
// An index in m_items used to know which items we are going to executed next
int m_index = 0;
// An accumulator to keep track of the time and the number of tasks performed
float m_updateAccumulator = 0;

Вот логика обновления:

// If we have 10 items and m_updateInterval is 0.5s, we need to do 20 tasks per seconds.
float updatesPerSecond = (m_items.Count / m_updateInterval);
// If we need to do 20 tasks per second and the frame rate is 100, we need to do 0.2 tasks per frame.
float updatesPerFrame = updatesPerSecond * Time.deltaTime;
// We accumulate 0.2 every frame. After 5 frames the accumulator will be set to 1. Also make sure the accumulator do not increase permanently. We don't need to update more than all the items in one frame.
m_updateAccumulator = Mathf.Min(m_updateAccumulator + updatesPerFrame, m_items.Count);
// updateCount is the integer part of m_updateAccumulator. If the accumulator value is 2.4 we will update 2 tasks.
int updateCount = (int)m_updateAccumulator;
// Execute the right amount of tasks
for (int i = 0; i < updateCount; ++i)
{ 
    // Do a Modulo before accessing m_items because m_index might
    // be greater than m_items.Count if some items were removed.
    m_index = (m_index + 1) % m_items.Count;
    var action = m_items[m_index];
    action();
}
// Remove the integer part of the accumulator since we updated that amount of tasks. If the accumulator value is 2.4 it will become 0.4.
m_updateAccumulator -= updateCount;

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