全部课件 / GameBase / Lesson 02

谁负责真正的游戏结果,谁只负责把结果表现出来?

这次从一次真实的开火入手:按键进入游戏、关卡判断射线与墙、目标扣血、镜头震动、粒子发射。下面的调用追踪由上方小游戏的实际操作生成,不是额外编排的时序模拟器。

阅读边界:蓝色角色、关卡、碰撞和事件追踪均为课程独立实现。Deepnight 原版没有本文演示的统一事件总线;课程里的调用路径用于理解职责,不冒充源码逐行重放。

① 开火:哪些是游戏事实,哪些只是反馈?

📱 手机:按住左右按钮移动,点击「开火」;也可以直接轻点场景开火。

电脑:A/D 或 ←/→ 移动,空格、F 或点击游戏场景开火。

只关闭表现,不改变规则

关闭后镜头仍然可以跟随角色,只是不再添加开火震动。

关闭后不发射粒子;子弹是否撞墙、目标扣多少血,仍然由 Level/Entity 决定。

目标 HP100 / 100
命中次数0
关卡碰撞查询0
当前粒子0 / 64

推荐:先在关卡 A 开一枪命中目标;关闭两个反馈再开一枪,看看伤害是否仍然发生。接着切关卡 B,观察墙挡住射线时为什么不会扣血。

02

刚才那一枪经过了哪些系统?

每按一次开火,这里自动重建调用追踪。它读取刚才那次命中判定、血量变化、镜头反馈和粒子申请数量。

等待第一次开火尚未记录

先在上方游戏点击「开火」,这里就会显示“输入 → Game → Level → Entity → Camera → Fx → Render”的实际处理结果。

注意:“Game 调度”是课程构造的显式调用链。原版 Game 持有系统并调度更新,但没有通用的统一事件总线,也不是所有开火效果都由 Game 转发。

03

切换关卡:谁清理旧东西,谁创建新东西?

关卡 B 初始有一道墙,它会挡住射线。切换时课件模拟原版 Game.startLevel() 的主要清理职责,并记录真实发生的顺序。

关卡实例A
关卡画面重建次数0
已清理旧关卡0
关卡已变更次数0

点击上方「打开 / 关闭墙」:关卡会先将地图标记为 dirty,下一帧才重新构建绘制数据,碰撞规则则立即生效。

本课关卡数据是手写的教学网格;原版 Level 使用 LDtk 的 World_Level、MarkerMap,并在 postUpdate() 中按需重建关卡绘制。

04

性能审计:系统拆开后,成本在哪里?

这些数字来自本课实际运行,原版的容量与实现单独标注。

粒子池请求总数0
成功分配 / 复用0 / 0
满池拒绝0
粒子槽扫描次数0
本课粒子池固定 64 槽,每帧扫描全部槽位。每次发射也要寻找可用槽位,因此减少分配并不等于完全没有扫描成本。
原版 Fx 和 Level 的真实成本结构

Fx:Fx.new() 创建容量为 2048 的 ParticlePool,以及前景/背景 × Normal/Add 四组 SpriteBatch,Fx.update() 会调用 pool.update(game.tmod)。这意味着表现系统既要考虑粒子生命周期,也要考虑批次与透明覆盖。

Level:invalidate() 将关卡标记为需要重绘;postUpdate() 才调用 render(),避免每次查询碰撞都重建地图。但原版的 render() 会移除旧子对象并重新创建 TileGroup,因此高频 invalidate 仍然不免费。

Camera:每帧根据焦点、边界、缩放、震动更新 scroller。镜头效果不应通过修改 Entity 的物理坐标来实现,否则可能污染碰撞、AI 和联网同步。

架构代价:系统拆分也可能带来调用与状态同步成本,不能只从类的数量判断性能;应通过 profiler 分别测量逻辑更新、地图重建、批量粒子和过度绘制。

05

原版 Haxe:职责归属到底是什么?

源文件均来自 Deepnight 的固定 GameBase 快照,非本课教学代码。

Game.hx · Level.hx · Camera.hx · Fx.hx · 详细笔记

Game

持有 Level、Camera、Fx、Hud、scroller,启动/销毁关卡并分发 Entity 更新。

Level

保存 LDtk 关卡、碰撞查询、格子标记;按需重建地形显示。

Camera

跟随 Entity、限制地图边界、视口坐标变换、缩放和冲击反馈。

Fx

管理粒子池、SpriteBatch 和各类效果分配;明确表现与游戏事实的边界。

Game.startLevel() 的真实顺序
// 原版主要步骤的职责摘录(教学重排)
if (level != null) level.destroy();
fx.clear();
for (e in Entity.ALL) e.destroy();
garbageCollectEntities();
level = new Level(l);
camera.centerOnTarget();
hud.onLevelStart();
dn.Process.resizeAll();

这解释了为什么关卡切换属于 Game 编排,而不是让 Camera 或 Fx 自己销毁整张地图。原版同时有注释提示:销毁所有实体是模板做法,后续可以按项目要求保留玩家。

原版 Level 如何处理碰撞与重绘?
public inline function hasCollision(cx, cy) : Bool {
    return !isValid(cx,cy)
        ? true
        : marks.has(M_Coll_Wall, cx,cy);
}
public inline function invalidate() {
    invalidated = true;
}

碰撞依据数据/MarkerMap,而不是去询问 Camera 是否看得见,也不是根据粒子是否播放来判定。

四个系统是不是严格互相不依赖?

不是。真实源码中 Camera 会读取 Game.level 和 scroller;Fx 会引用 Game.scroller,也能通过 level.hasCollision 查询粒子碰撞;Game 则直接持有并编排它们。所谓“系统边界”不是绝对隔离,而是弄清谁拥有权威状态,谁负责查询,谁负责表现,谁负责生命周期。

06

三道自测:新功能应该放哪儿?

直接点击答案,看是否能根据职责定位代码。

学习进度仅保存在本机浏览器。