游戏引擎的主循环设计
游戏主循环
在使用物理和渲染引擎的游戏中,虚拟的世界是不断变换的。通过一个模拟循环(simulation loop)来驱动这个世界。一个简单的循环是下面这个样子:
while (gameIsRunning)
{
float deltaTime = GetDeltaTime(); // 获取自上一帧以来的时间
ProcessInput(); // 获得输入
UpdateGameLogic(); // 更新游戏逻辑
physicsWorld->StepSimulation(deltaTime); // 驱动物理世界前进
RenderScene(); // 渲染
}
到目前为止,我们看到主循环非常自然:输入 -> 逻辑 -> 物理 -> 渲染。
Step Simulation
为了保证物理模拟的稳定性,要求物理模拟的时间步一定要稳定。但是,游戏的主循环,无法保证每次deltaTime稳定。需要通过StepSimulation把不稳定的真实的时间,转换成若干个稳定的物理小步。这样一次StepSimulation里面执行了很多次物理。
一个StepSimulation伪代码:
StepSimulation(deltaTime)
{
accumulatedTime += deltaTime;
int subSteps = floor(accumulatedTime / fixedDt);
// 限制物理模拟的最大步数。以防爆炸。
subSteps = min(subSteps, maxSubSteps);
for (int i = 0; i < subSteps; ++i)
{
// 物理世界单步模拟
internalSingleStepSimulation(fixedDt);
}
accumulatedTime -= subSteps * fixedDt;
}
这里fixedDt是每步物理模拟的固定时间。accumulatedTime是物理模拟积累的时间,从上一次物理更新,到目前没有被物理模拟消耗的累计时间。
物理世界的单帧
上面internalSingleStepSimulation是物理世界的单帧函数。现代游戏引擎(box2D,bullet3,PhysX)在实现细节上有所差异,但是几乎所有的刚体物理引擎都有相似的流程。这个流程可以概括成以下几步:
-
施加外力:游戏中,对物体和角色施加的力,如推力、重力和阻尼等。
-
碰撞检测:一般分成两个阶段。
- 宽相(Broad Phase):使用快速算法(AABB树,排序和扫描等)筛选可能发生的碰撞对,排除大量明显不相交的物体。
- 窄相(Narrow Phase): 对宽相筛选出的每一对物体,使用精确的几何算法(GJK/EPA,SAT)计算他们是否真的碰撞,并找出碰撞点、碰撞法线和穿透深度等详细信息。
-
构建孤岛:根据碰撞产生的接触关系和关节约束,将相互作用的物理构建成一个个独立的“孤岛”。孤岛之间相互不影响,可以进行并行优化。
-
约束求解:对于接触和关节约束,计算出冲量(Impulse)来处理物体穿透、摩擦和关节连接。求解的过程是迭代的,重复多次为了获得稳定解。
-
积分:根据求解器得到的冲量来更新速度,再更新所有活动物体的位置和朝向。
-
状态同步:将物体世界中物体的新状态(位置和朝向)同步到游戏世界的图形对象上,然后渲染。