Phaser 城建摆放系统:占地、旋转、道路连接和冲突提示

讲解 Phaser 城建或基地建造中的建筑摆放、格子占用、旋转预览、道路连接、资源扣除、撤销和调试。

为什么值得单独做成系统

一款轻量城建游戏里,玩家把水泵放在河边,把仓库接到道路,把住宅区绕开污染源。鼠标拖动建筑时,绿色预览表示可放,红色显示冲突,旋转后占地和道路口也要跟着变化。

建筑摆放看似是 UI 操作,实际是规则密集区:占地、地形、道路、电力、水源、资源、撤销和存档都在这里汇合。若只在 pointerup 时检查一个矩形,后续会很难支持复杂建筑。 本文会按一个可上线的小系统来拆,不追求炫技,而是把数据结构、状态流、玩家反馈、调试工具和发布检查说清楚。Phaser 的优势是让画面和交互快速成型,但越是快速,越需要把规则层和表现层分开。

核心架构

flowchart TD
  N1["PlacementIntent"] --> N2["FootprintModel"]
  N2["FootprintModel"] --> N3["GridOccupancy"]
  N3["GridOccupancy"] --> N4["ConnectionRule"]
  N4["ConnectionRule"] --> N5["CostPreview"]
  N5["CostPreview"] --> N6["PlacementGhost"]
  N6["PlacementGhost"] --> N7["BuildCommit"]
  N7["BuildCommit"] --> N8["UndoStack"]

这张图的重点是单向流动。PlacementIntent、FootprintModel、GridOccupancy、ConnectionRule、CostPreview、BuildCommit、UndoStack 不应该互相随意读写。输入或场景事件进入模型,模型输出快照或事件,Phaser 表现层再根据结果更新 Sprite、Graphics、Sound 和 UI。只要这条边界稳定,后续加内容、加难度、加存档或加多人同步,都不会把系统推倒重写。

建筑蓝图要描述占地

每个建筑 blueprint 不只包含贴图,还要包含 footprint、可旋转、入口方向、道路连接点、地形要求、成本和维护信息。旋转时 footprint 坐标和连接点都要一起变换。这样 2x3 仓库、L 形管道、河边水泵和道路节点都能用同一套预览系统。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

格子占用要独立于显示

GridOccupancy 记录每个格子当前被谁占用、是否可通过、是否保留给道路。建筑 Sprite 可以因为镜头裁剪被隐藏,但占用仍然存在。摆放预览只查询 GridOccupancy,不应该检查屏幕上有没有某个 Sprite。这个边界决定了存档、撤销和批量建造是否可靠。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

冲突提示要具体

红色预览只告诉玩家不能放,却没有告诉为什么。更好的反馈是把冲突分层:地形不符、和已有建筑重叠、缺少道路、资源不足、超出边界、科技未解锁。可以在预览旁显示最重要原因,调试模式显示所有原因。玩家理解原因后,建造体验会顺很多。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

道路连接不是装饰

仓库、住宅和工厂可能需要道路连接才能工作。ConnectionRule 应根据连接点和邻近道路判断有效性。道路自己也可以是建筑,放置时更新路网图。不要等生产系统发现没有道路才报错,摆放阶段就应该给出连接预览。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

资源扣除要等提交

拖动预览时只能显示成本,不应扣资源。点击确认后 BuildCommit 再做最终校验和扣费。因为玩家拖动期间资源可能被其他系统变化,提交前必须重新检查。提交失败要保持预览状态,让玩家调整,而不是直接取消操作。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

撤销要保存补丁

城建玩家常常连续摆错几个建筑。UndoStack 可以记录 build patch:新增建筑 id、占用格、扣除资源、路网变化。撤销时按 patch 逆操作。不要只删除 Sprite,否则资源和占用会残留。批量铺路也可以作为一个复合 patch。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

吸附手感影响效率

鼠标位置转格子时要考虑镜头缩放和等距或正交投影。预览建筑应该稳定吸附到格子中心,不能因为鼠标细微移动抖动。旋转快捷键、批量拖拽、按住 Shift 连续建造这些效率操作,要从第一版输入模型里预留。

实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。

TypeScript 实现骨架

interface FootprintCell { x: number; y: number }
interface Blueprint { id: string; cost: number; footprint: FootprintCell[]; roadPorts: FootprintCell[] }
function rotateFootprint(cells: FootprintCell[], turns: number) {
  let out = cells.map(c => ({ ...c }));
  for (let i = 0; i < turns % 4; i++) out = out.map(c => ({ x: -c.y, y: c.x }));
  return out;
}
function validatePlacement(origin: FootprintCell, blueprint: Blueprint, occupied: Set<string>, money: number) {
  const errors: string[] = [];
  if (money < blueprint.cost) errors.push("not-enough-money");
  for (const cell of blueprint.footprint) {
    const key = `${origin.x + cell.x},${origin.y + cell.y}`;
    if (occupied.has(key)) errors.push(`occupied:${key}`);
  }
  return errors;
}
function commitPlacement(origin: FootprintCell, blueprint: Blueprint, occupied: Set<string>) {
  for (const cell of blueprint.footprint) occupied.add(`${origin.x + cell.x},${origin.y + cell.y}`);
}

这段代码只展示核心边界,不是完整项目代码。真实项目里还需要补配置加载、错误码、事件派发、对象池、性能采样和测试。关键是让核心规则能独立运行,Phaser 层只是把规则结果变成玩家能感知的反馈。

落地步骤

  1. 第一,先把 PlacementIntent 和 FootprintModel 写成普通 TypeScript 模型。不要让它们依赖 Phaser Scene、Sprite 或 Camera。核心模型越普通,越容易写测试、做编辑器预览和复现玩家问题。
  2. 第二,Phaser 层只做适配:接收输入、播放动画、更新图形、触发音效。它可以很薄,但必须清楚。只要某段规则开始读取 Sprite 的 visible、alpha 或动画状态,就说明边界正在变脏。
  3. 第三,给 GridOccupancy 或同等复杂的中间结果做调试显示。开发模式里能看到状态、阈值、候选对象、失败原因和耗时,后续调参才不会靠猜。
  4. 第四,准备三组测试夹具:正常流程、边界流程、错误配置。正常流程验证体验,边界流程验证稳定性,错误配置验证系统会报出人能看懂的问题。

检查清单

  • 确认 PlacementIntent 的状态可以序列化,能写入存档或调试日志。
  • 确认 FootprintModel 的配置有默认值、版本号和校验错误。
  • 确认快速点击、暂停、切后台、读档和切场景不会重复提交关键事件。
  • 确认失败反馈足够具体,玩家能知道是条件不足、输入中断、资源不够还是规则禁止。
  • 确认低端机有降级策略,尤其是粒子、音效、动态对象和调试图层。
  • 确认开发模式可以导出最近关键事件,方便复现玩家反馈。

常见误区

第一类误区,是把表现当成事实。动画播完、按钮亮着、Sprite 存在,都只能说明表现层当前长什么样,不能说明规则已经完成。规则事实应该存在于模型和事件里。

第二类误区,是只为了第一个关卡写逻辑。第一个关卡对象少、输入慢、节奏简单,临时判断很难暴露问题。等内容增加,重复触发、配置错误和性能峰值会一起出现,早期的边界会决定后期成本。

第三类误区,是没有设计失败路径。复杂系统一定会遇到失败,好的失败路径会告诉玩家和开发者发生了什么;坏的失败路径只会留下一句操作失败,甚至什么都不显示。

发布前验证

发布前至少跑一次规则级测试和一次运行时冒烟。规则级测试不需要启动浏览器,直接喂数据,断言状态和事件。运行时冒烟则在 Phaser 场景里验证输入、反馈、暂停、重开和边界情况。若系统涉及经济、存档或排行榜,还要记录 requestId 或事件 id,保证重复提交不会造成重复奖励或重复扣费。

额外实践建议

  • 先把 footprint 和冲突原因调清楚,再做漂亮预览。
  • 道路连接要作为规则参与校验,不要只是视觉贴图。
  • 每次提交都重新校验,避免拖动预览期间状态变化。

运行时观测与调参

城建摆放系统要特别关注“取消”和“失败”数据。玩家拖出建筑后频繁取消,可能说明成本太高、预览不清楚或吸附不舒服;玩家反复在同一片区域收到道路连接错误,可能说明道路规则没有被视觉表达出来。建议记录每次预览的建筑 id、旋转、最终格子、失败原因和是否提交。不要只统计建成数量,失败原因分布更能暴露体验问题。调试工具里还可以提供占用热力图,显示哪些格子经常因为冲突被拒绝。对于后期内容团队,这比口头描述“这里不好放”有用得多。

结语

城建摆放系统:占地、旋转、道路连接和冲突提示 的关键不是某个 API,而是把可解释的规则交给模型,把可感知的反馈交给 Phaser。只要这条线清楚,项目就能持续扩展;如果所有逻辑都塞进 Scene 回调,第一版越快,后面的维护压力越大。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页