Не очевидно почему физика должна стать "плавнее". Начинать нужно с профайлинга, и понять сколько времени выполняются функции Update() и Render(), и уже от этого решать как параллелить, что параллелить, и нужно ли вообще. Так же нужно определиться как обновляется игровое состояние - всегда ли с фиксированным шагом dt (православно), или позволительно обновляться с плавающим dt (не православно) - в этом случае результаты симуляции не будут сходиться у игроков с разным ФПС, и это может сильно мешать прохождению игры.
Случай 1: время выполнения одного Update() много больше Render()
Тогда вынос Update() в отдельный поток профита практически не даст, т.к. почти весь кадр он же и съедает, а оверхэд связанный с многопоточностью перекроет весь теоретический прирост ФПС с вероятностью 146%. В этом случае надо распараллеливать внутри вызова Update() - обновлять игровой стейт в несколько потоков. Если каждый объект модифицирует только собственный стейт, то все параллелится элементарно.
Случай 2: время выполнения одного Render() много больше Update()
Здесь уже два варианта:
- можно попробовать обойтись без многопоточности, просто в начале кадра провернуть цикл с несколькими вызовами Update(), разбивая dt кадра на несколько шагов
- вынести Update() в отдельный поток, и там уже крутить цикл с фиксированным dt или плавающим. Там же ограничивать логический ФПС, чтобы не считать игровой стейт, например, с частотой 2000 Гц, и не жарить процессор юзера
В общем случае идеальная многопоточность - это когда потоки не конкурируют за данные, которые одновременно могут быть читаемы и модифицируемы, т.е. точек синхронизации практически нет, и нет блокировок потоков. А мьютекс - это как раз блокировка в случае одновременного доступа двумя и более потоками. Чем больше кусок кода под мьютексом и чем больше потоков за него конкурируют, тем больше ущерб перфомансу, и тем печальнее вся многопоточность работает, и такой результат получить очень легко. Планировать использование мьютексов на раннем этапе игрового движка - это тупик, на такой поточности далеко не уехать. Мьютекс годится для редких операций, либо очень коротких вычислений под ним, либо ожидание происходит в каких-то вторичных потока, которые не приведут к фризу на экране.
В случае игрового движка, оптимальная схема будет выглядеть так:
Есть два независимых стейта - логический и графический.
Логический стейт - это вся физика, скрипты, эвенты, и тд - то что сейчас и есть.
Графический стейт - это минимально достаточный набор данных по каждому объекту логики, плюс состояние окружения, погоды, эффектов, и т.п. Этот стейт полностью описывает графический кадр, передается как аргумент в Render() и позволяет нарисовать весь кадр с нуля без обращения к логическому стейту. Этот стейт должен быть сериализуем, чтобы его можно было отложить в сторонку и через какое-то время нарисовать.
Так же у нас есть два потока - основной логический и основной рендерящий. В логическом потоке мы обновляем логический стейт, по необходимости по нему формируем графический стейт, после чего через тройную буферизацию передаем графический стейт в поток графики. Поток графики же всегда рисует самый актуальный графический стейт (предыдущие рисовать не имеет смысла), либо ждет, когда появится новый, если логика сильно тормозит.
В итоге имеет два потока работающих независимо, и единственное место синхронизации - это свап указателей/индексов в triple buffer, который сделан через атомики, что практически бесплатно (плюс ожидание на семафоре или спинлоке в потоке графики, если логика не успевает). В зависимости от задачи можно сделать чуть более жесткую синхронизацию, чтобы логический поток также ждал поток график, как сделано в большинстве движков, когда логический поток считает N кадр, а графический поток рисует (N-1), в остальных случаях один из потоков будет висеть на семафоре...
Псевдокод:
LogicObject { position, velocity, script, health, ammo,... }
//соответствующий LogicObject графический объект:
GraphicObject { position, shape, color, opacity, animationPhase...}
LogicState { time, vector<LogicObject > entites,....}
GraphicState { logicTime, camerPosition, cameraFOV, cameraAspect, ... , skyBoxID, vector<GraphicObject> objects,...}
LogicState gameState;
TrippleBuffer<GraphicState> graphicStateBuffer;//3 графических стейта
//логический поток:
{
while(true)
{
Update(dt, gameState);
GraphicState graphState = buildGraphState(gameState);
graphicStateBuffer.write(graphState);
}
}
//графический поток:
{
while(true)
{
//для простоты считаем что метод блокирующий, когда нет нового стейта
bool bNewFrame = graphicStateBuffer.update();
if(bNewFrame)
Render(graphicStateBuffer.read());
}
}
Если делать прям совсем на красоту, то можно сделать экстраполяцию позиций графических объектов. У каждого GraphState мы знаем время логического стейта, для которого он был запечен, и это время всегда в прошлом. Соответственно позиции графических объектов в начале кадра графики экстраполируются на разницу между реальным временем начала рендеринга и временем, когда этот графический стейт был сформирован. В этом случае к графическому стейту прилипают скорости всех объектов...