Python 现代开发学习笔记

(十七)分开显示帧率与固定逻辑步

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 events

while 每推进一步就扣除固定秒数,余量继续保留。很小的 1e-12 用来处理多次浮点加减在判断处留下的舍入误差,不会把一次显示帧直接当作一步。

暂停、结束与场景切换会清空余量。拖动窗口或调试停住很久后,若一次回来已经经过 0.8 秒,游戏直接进入暂停画面,让玩家主动继续,避免在恢复瞬间突然补走一大段。

30帧每次消费四个逻辑步,60帧消费两个,144帧先保留不足一步的余量再推进

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 数字代替整份输入过程。

笔记正文与全部题解 · Ctrl / ⌘ K