(十七)分开显示帧率与固定逻辑步
1. 多次显示不等于多次推进逻辑
完整游戏把逻辑步长固定为 1/120 秒。显示一帧花了 1/60 秒,就推进两次逻辑;花了 1/30 秒,就推进四次。显示比逻辑更频繁时,先把不足一步的时间留下来,等积累够了再更新。
# 项目文件:dodge_project/src/dodgegame/stepping.py
# 显示帧先积累秒数,够一个逻辑步才更新;暂停和切换场景时清空余量。
import math
from .config import FIXED_STEP
from .models import Mode
class Stepper:
def __init__(self):
self.accumulator = 0.0
def reset(self):
self.accumulator = 0.0
def advance(self, world, frame_seconds, axis):
if not math.isfinite(frame_seconds) or frame_seconds < 0:
raise ValueError("每帧经过秒数应为非负有限数")
if world.mode != Mode.PLAYING:
self.reset()
return []
if frame_seconds > 0.25:
# 拖动窗口或调试暂停很久后,进入暂停画面,由玩家主动恢复。
world.toggle_pause()
self.reset()
return ["lag_pause"]
self.accumulator += frame_seconds
events = []
while self.accumulator + 1e-12 >= FIXED_STEP:
events.extend(world.step(FIXED_STEP, axis))
self.accumulator = max(0.0, self.accumulator - FIXED_STEP)
if world.mode != Mode.PLAYING:
self.reset()
break
return eventswhile 每推进一步就扣除固定秒数,余量继续保留。很小的 1e-12 用来处理多次浮点加减在判断处留下的舍入误差,不会把一次显示帧直接当作一步。
暂停、结束与场景切换会清空余量。拖动窗口或调试停住很久后,若一次回来已经经过 0.8 秒,游戏直接进入暂停画面,让玩家主动继续,避免在恢复瞬间突然补走一大段。
2. 用同一秒输入核对不同显示分帧
# 项目文件:demos/fixed_steps.py
# 同一秒、同一持续方向和同一随机种子,30/60/144次显示都推进120个逻辑步。
from dodgegame.models import World
from dodgegame.stepping import Stepper
for fps in [30, 60, 144]:
world, stepper = World(seed=7), Stepper()
world.start()
for _ in range(fps):
stepper.advance(world, 1 / fps, 0.5)
print(fps, f"{world.player.x:.1f}", f"{world.elapsed:.3f}", len(world.obstacles))三行中玩家 x 都为 536.0,累计时间为 1.000,障碍数量为 1。初始 x=376,方向为 0.5,速度为每秒 320,经过一秒移动 320×0.5=160,最终为 536。
三次输入持续保持同一方向,并使用同样的随机种子,所以还可以比较生成的障碍位置与速度。实际操作中,事件在哪一帧到来仍然决定了输入何时被读取,不能用一个 FPS 数字代替整份输入过程。