Python 现代开发学习笔记

(十九)将事件与连续输入交给同一状态逻辑

1. 输入层只组合动作

Controller 接收前面已经建立的 World、Stepper、Renderer 与 SoundBank。先定义开始或恢复的共同入口:

# 项目文件:dodge_project/src/dodgegame/controller.py
# 离散事件改变场景与开关,连续轴值交给固定逻辑步;绘制由另一层负责。
import pygame

from .models import Mode


class Controller:
    def __init__(self, world, stepper, renderer, sounds):
        self.world, self.stepper = world, stepper
        self.renderer, self.sounds = renderer, sounds
        self.running = True
        self.debug = False
        self.changed = False

    def activate(self):
        if self.world.mode in {Mode.TITLE, Mode.GAME_OVER}:
            self.world.start()
        elif self.world.mode == Mode.PAUSED:
            self.world.toggle_pause()

activate 让 TITLE、GAME_OVER 进入一局,让 PAUSED 恢复;PLAYING 中按开始键不重置本局。键盘和鼠标继续调用同一个动作。

2. 离散事件更新状态与开关

# 续写同一文件:dodge_project/src/dodgegame/controller.py
    # 把键盘、鼠标和失焦事件转成状态变化;切换状态时同时清掉累计的步进时间。
    def handle_events(self, events):
        for event in events:
            before = self.world.mode
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.running = False
                elif event.key == pygame.K_p:
                    self.world.toggle_pause()
                elif event.key in {pygame.K_RETURN, pygame.K_SPACE}:
                    self.activate()
                elif event.key == pygame.K_r and self.world.mode == Mode.GAME_OVER:
                    self.activate()
                elif event.key == pygame.K_m:
                    self.sounds.toggle()
                elif event.key == pygame.K_F1:
                    self.debug = not self.debug
            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
                if self.renderer.pause_button.collidepoint(event.pos):
                    self.world.toggle_pause()
                elif (
                    self.world.mode != Mode.PLAYING
                    and self.renderer.menu_button.collidepoint(event.pos)
                ):
                    self.activate()
            elif (
                event.type == pygame.WINDOWFOCUSLOST and self.world.mode == Mode.PLAYING
            ):
                self.world.toggle_pause()
            if self.world.mode != before:
                self.changed = True
                self.stepper.reset()
            if not self.running:
                break

每项事件处理前后比较场景状态。状态发生变化就清空逻辑余量,并标记这一帧刚切换过。失去窗口焦点时,正在进行的游戏也进入暂停,下一次需要玩家主动恢复。

菜单暂停只停止 World 的运动,事件循环继续执行。退出事件则让 running 为 False,并停止处理本轮余下事件;不会在退出之后又响应一次暂停或重开。

3. 恢复时不补算菜单停留时间

# 续写同一文件:dodge_project/src/dodgegame/controller.py
    def advance(self, seconds, axis):
        if not self.running:
            return []
        if self.changed:
            # 开始或恢复这一帧,不把刚才停在菜单里的时间加到游戏运动中。
            self.changed = False
            seconds = 0
        events = self.stepper.advance(self.world, seconds, axis)
        for event in events:
            self.sounds.play(event)
        return events

开始或恢复这一帧,把传入的经过秒数置为 0,下一帧再正常推进。Stepper 返回的 score、hit 消息只在本步消费一次,再交给声音层。

按P先改变状态并清空逻辑余量,恢复当帧不计菜单时间,下一帧再按持续方向更新

先用一份已知状态检查结束与重开:

# 项目文件:demos/round_states.py
# 先用已知分数和相交障碍检查结束,再重开,观察本局清空而最高分保留。
from dodgegame.models import Obstacle, World

world = World(seed=7, best=5)
print(world.mode.name)
world.start()
world.score = 3
world.toggle_pause()
print(world.mode.name, world.score)
world.toggle_pause()
world.obstacles = [Obstacle(1, world.player.x, world.player.y - 20, 48, 24, 200)]
print(world.step(0.01, 0), world.mode.name, world.score)
world.start()
print(world.mode.name, world.score, world.best, len(world.obstacles))

示例把本局分数设为 3、最高分设为 5,作为检查输入。状态先由 TITLE 进入一局,暂停保留 3 分;放入相交障碍后得到 ['hit'] GAME_OVER 3;再次开始得到 PLAYING 0 5 0,分别表示场景、本局分数、最高分和障碍数。

重开清空分数障碍计时碰撞记录,但保留最高分,并把显示层连接到新Player

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