(十八)由当前状态绘制整幅游戏画面
1. 将模型与精灵逐项同步
现在 World 已经保存玩家、障碍和每块障碍的 uid,继续在 sprites.py 中加入场景同步:
# 续写同一文件:dodge_project/src/dodgegame/sprites.py
class SceneSprites:
def __init__(self, images, world):
self.images = images
self.obstacles = pygame.sprite.Group()
self.by_id = {}
self.player = BodySprite(world.player, images.get("player", (48, 36)))
self.players = pygame.sprite.Group(self.player)
def sync(self, world):
# 重开会建立新的 Player,显示层每帧连接当前模型,避免保留上一局对象。
self.player.body = world.player
self.player.sync()
live_ids = {body.uid for body in world.obstacles}
for uid in set(self.by_id) - live_ids:
self.by_id.pop(uid).kill()
for body in world.obstacles:
image = self.images.get("obstacle", (body.width, body.height))
if body.uid not in self.by_id:
self.by_id[body.uid] = BodySprite(body, image)
self.obstacles.add(self.by_id[body.uid])
sprite = self.by_id[body.uid]
sprite.body, sprite.image = body, image
sprite.rect.size = image.get_size()
sprite.sync()每帧先给玩家精灵连接本局的 Player,再同步坐标。重开建立新 Player 后,显示层不会继续拿着上一局对象。障碍按 uid 找到精灵,新出现就加入,已经离屏就从映射和组中移除。
逻辑可以在没有窗口时检查,画面也可以用同一份状态重复绘制。绘制一次不会让障碍多落一步,画两次也不会多加一分。
2. 先准备字体、区域与按钮
# 项目文件:dodge_project/src/dodgegame/view.py
# 每帧从当前世界重画背景、精灵、界面和覆盖层,绘制不修改分数或坐标。
import pygame
from .config import BOTTOM, HEIGHT, LEFT, RIGHT, TOP, WIDTH
from .models import Mode
from .sprites import SceneSprites
INK = "#24324A"
MUTED = "#637189"
INDIGO = "#4657B5"
TEAL = "#087F82"
AMBER = "#A85D11"
PAPER = "#F5F7FB"
LINE = "#D9E0EC"
class Renderer:
def __init__(self, images, world):
self.sprites = SceneSprites(images, world)
self.small = pygame.font.Font(None, 23)
self.body = pygame.font.Font(None, 29)
self.large = pygame.font.Font(None, 49)
self.arena = pygame.Rect(LEFT, TOP, RIGHT - LEFT, BOTTOM - TOP)
self.menu_button = pygame.Rect(258, 370, 284, 58)
self.pause_button = pygame.Rect(642, 28, 126, 40)
def text(self, screen, message, font, color, position, centered=False):
image = font.render(message, True, color)
rect = (
image.get_rect(center=position)
if centered
else image.get_rect(topleft=position)
)
screen.blit(image, rect)
def button(self, screen, rect, label, mouse):
fill = TEAL if rect.collidepoint(mouse) else INDIGO
pygame.draw.rect(screen, fill, rect, border_radius=12)
self.text(screen, label, self.body, "white", rect.center, centered=True)文字先 render 成图像,再按左上角或中心放置;按钮的绘制和点击判断使用同一 Rect。游戏区域上边为 108,下边为 568,顶部空间留给标题、分数和暂停按钮。
3. 清屏后按层次合成
# 续写同一文件:dodge_project/src/dodgegame/view.py
def draw(self, screen, world, muted=False, debug=False, fps=0, mouse=(-1, -1)):
screen.fill(PAPER)
self.text(screen, "NOTEBOOK ARCADE / 01", self.small, INDIGO, (32, 18))
self.text(screen, "DODGE THE FALL", self.large, INK, (30, 42))
self.text(
screen,
f"SCORE {world.score:02d} BEST {world.best:02d}",
self.body,
TEAL,
(365, 48),
)
self.button(
screen,
self.pause_button,
"RESUME" if world.mode == Mode.PAUSED else "PAUSE",
mouse,
)
pygame.draw.rect(screen, "white", self.arena, border_radius=16)
# 限定游戏区域的绘制,刚生成但还在上边缘之外的障碍不会盖住标题。
screen.set_clip(self.arena)
offset = int(world.elapsed * 45) % 32
for x in range(LEFT + 16, RIGHT, 32):
for y in range(TOP + offset, BOTTOM, 32):
pygame.draw.circle(screen, "#E6EBF3", (x, y), 1)
for x in (216, 400, 584):
pygame.draw.line(screen, "#F0F2F8", (x, TOP), (x, BOTTOM), 1)
self.sprites.sync(world)
self.sprites.obstacles.draw(screen)
self.sprites.players.draw(screen)
if debug:
for sprite in [self.sprites.player, *self.sprites.obstacles.sprites()]:
pygame.draw.rect(screen, INDIGO, sprite.rect, 1)
if world.hit_uid in self.sprites.by_id:
pygame.draw.rect(
screen,
"#AC4964",
self.sprites.by_id[world.hit_uid].rect.inflate(8, 8),
3,
border_radius=9,
)
screen.set_clip(None)
pygame.draw.rect(screen, LINE, self.arena, 2, border_radius=16)
self.text(
screen,
"A / D or ARROWS P: pause M: sound F1: hitboxes",
self.small,
MUTED,
(32, 577),
)
self.text(screen, f"{fps:3.0f} FPS", self.small, MUTED, (700, 577))
if world.mode != Mode.PLAYING:
self.draw_overlay(screen, world, mouse)
self.text(
screen, "SOUND OFF" if muted else "SOUND ON", self.small, MUTED, (645, 82)
)先画背景和界面区域,再同步并画障碍、玩家,随后画碰撞框和菜单。set_clip(arena) 限制游戏对象的绘制范围,刚生成但还没落进区域的障碍不会盖住标题;画完后清除限制,再绘制界面。
draw.circle(screen, 颜色, 中心坐标, 半径) 画点阵中的圆点,draw.line(screen, 颜色, 起点, 终点, 线宽) 画竖向参考线。两者都只改变给定 Surface 的像素,位置与经过时间仍由 World 提供。
背景点阵的偏移来自 world.elapsed,暂停时经过时间不更新,背景也一起停止。F1 只切换碰撞框是否显示,显示框不会改变逻辑使用的形状。
4. 暂停与结束用覆盖层表达
# 续写同一文件:dodge_project/src/dodgegame/view.py
# 先铺半透明遮罩,再按当前状态绘制暂停或结束提示,让底下的游戏画面仍然可见。
def draw_overlay(self, screen, world, mouse):
veil = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
veil.fill((245, 247, 251, 220))
screen.blit(veil, (0, 0))
panel = pygame.Rect(148, 166, 504, 304)
pygame.draw.rect(screen, "white", panel.move(0, 6), border_radius=22)
pygame.draw.rect(screen, "white", panel, border_radius=22)
pygame.draw.rect(screen, LINE, panel, 2, border_radius=22)
if world.mode == Mode.TITLE:
title, subtitle, button = (
"DODGE THE FALL",
"Move sideways. Let every block pass.",
"START / ENTER",
)
detail = "One passed obstacle = one point"
elif world.mode == Mode.PAUSED:
title, subtitle, button = (
"TAKE A BREATH",
"The world waits for your next move.",
"RESUME / P",
)
detail = f"Score {world.score} / Best {world.best}"
else:
title, subtitle, button = (
"ROUND OVER",
"A new round starts with a clear board.",
"TRY AGAIN / R",
)
detail = f"Score {world.score} / Best {world.best}"
self.text(screen, title, self.large, INK, (400, 221), centered=True)
self.text(screen, subtitle, self.body, MUTED, (400, 274), centered=True)
self.text(screen, detail, self.body, TEAL, (400, 320), centered=True)
self.button(screen, self.menu_button, button, mouse)
self.text(screen, "ESC to exit", self.small, MUTED, (400, 450), centered=True)先用半透明 Surface 淡化底下的画面,再画标题、说明和共同按钮。开始、暂停、结束分别选择自己的文字,按钮区域保持一致。状态保存在 World 中,绘制只是把当前状态表达出来。