等距地图的问题通常不是数学,而是边界
等距视角很适合小型策略游戏:画面有空间感,角色站位清楚,关卡看起来比普通俯视图更精致。但 Phaser 项目第一次做等距网格时,最容易踩的坑不是公式不会写,而是坐标边界不清。屏幕坐标、世界坐标、格子坐标、排序深度、寻路成本、点击命中范围,如果全部混在 Scene 里,后续每一次加障碍、技能范围和高亮都会变得很痛。
我遇到过一个战棋 H5 原型,第一版把角色直接放到等距 tile 的中心点,点击时用鼠标坐标反推格子。看起来能玩,但玩家点击角色脚边经常选中后面的格子,移动路线穿过半高墙,两个角色站在相邻格子时遮挡顺序也会跳。团队最初以为是美术透视没对齐,后来发现是逻辑格和显示格没有分开。
等距网格要先承认一个事实:玩法运行在逻辑格子上,画面只是它的投影。Phaser 负责显示和输入,战棋规则负责决定谁能走、走到哪里、技能打到谁。只要这个边界清楚,等距公式本身很简单。
flowchart TD
A[Pointer 屏幕坐标] --> B[Camera 转世界坐标]
B --> C[世界坐标转逻辑格]
C --> D{格子是否合法}
D -->|否| E[忽略或提示]
D -->|是| F[查询单位/地形/阻挡]
F --> G[寻路和范围计算]
G --> H[生成高亮和路线预览]
H --> I[确认后移动单位]
三套坐标要分开命名
等距项目至少有三套坐标。第一套是逻辑格坐标,比如 q,row 或 x,y,用于地形、单位、技能范围和寻路。第二套是世界坐标,也就是 Phaser 场景里 sprite 的位置。第三套是屏幕坐标,来自 pointer 和 camera。代码里如果都叫 x、y,很快就会出错。
我通常会把类型写清楚:GridPos 表示格子,WorldPos 表示 Phaser 世界位置,ScreenPos 表示鼠标或触控点。即使 TypeScript 不能真正阻止所有混用,命名也能提醒读代码的人。战棋系统里大量 bug 都来自“这个 x 到底是格子 x 还是像素 x”。
逻辑格到世界坐标可以用固定 tileWidth 和 tileHeight 转换。世界坐标到逻辑格则要更谨慎,因为点击区域是菱形,不是矩形。简单做法是先用公式粗略反推,再根据菱形区域或 tile mask 做精确判断。对手机触控,可以适当扩大可点击区域,但扩大的是输入容错,不是改变逻辑格。
排序深度要统一
等距游戏里,遮挡顺序是第二个高频问题。角色、树、墙、宝箱、特效都在同一个斜视空间里,谁在前谁在后不能靠创建顺序碰运气。Phaser 提供 depth,但 depth 必须由统一规则计算。
常见规则是按 gridX + gridY 或世界 y 坐标排序,再加上对象类型偏移。地面 tile 最低,单位中间,前景遮挡和高树更高。大型对象还要以脚底格为排序基准,而不是图片中心。否则一棵高树可能在错误时机挡住角色。
排序规则要写成函数,不要散在每个类里。单位移动、传送、生成、变身、载具切换都会影响 depth。如果只有某些路径更新 depth,就会出现“走过去正常,传送过去遮挡错”的问题。
移动范围不是简单画圈
战棋移动范围看起来像从角色所在格子扩散几步,但实际要考虑地形成本、阻挡、敌方单位、友方单位、飞行、跳跃、地形 buff 和区域禁行。不要用曼哈顿距离直接画一个菱形就上线,除非玩法真的只有空地。
更稳的方式是用 Dijkstra 或 A* 的成本模型计算可达格。每个格子提供 moveCost 和 passable,单位提供移动能力。沼泽成本高,墙不可通过,飞行单位忽略某些阻挡,敌方单位可以阻挡移动但允许攻击。这样移动范围和寻路路线来自同一套规则,不会出现高亮显示可达但路线走不过去。
移动范围也要区分预览和确认。玩家选中单位时显示可达格,移动鼠标或手指时显示路线预览,点击后才提交移动。联机或服务端权威项目里,客户端可以先表现,但最终位置需要服务端确认。单机项目也应该通过同一个规则服务确认,避免 UI 直接改单位格子。
寻路结果要可解释
玩家最讨厌战棋单位绕远路或者不走明明能走的格子。寻路系统需要调试工具。开发版可以显示每个格子的成本、父节点、是否阻挡、是否被单位占用。点击一个不可达格时,面板告诉你失败原因:地形不可通过、行动力不足、被敌人阻挡,还是区域限制。
A* 本身不难,难的是成本一致性。移动范围用一套成本,实际路径用另一套成本,bug 会非常隐蔽。建议把 getMoveCost(unit, from, to) 作为唯一入口。范围计算、路线计算、AI 评估都调用它。新增地形效果时,只改这一处规则。
如果地图较小,每次选中单位重新计算范围完全可行。地图较大或单位多时,可以缓存静态地形成本,只把动态单位阻挡作为额外层。不要过早优化,但要知道哪些数据是静态,哪些是每回合变化。
高亮层不要污染玩法层
移动格、攻击格、选中边框、路线箭头、危险范围都属于表现层。它们不应该写回地图数据,也不应该参与碰撞或寻路。用一个独立 HighlightLayer 或 Scene 管理这些对象,选中变化时清空重画。
高亮也要有优先级。可移动格和可攻击格重叠时显示什么?危险范围和可移动范围重叠时怎么表达?手机屏幕小,颜色过多会让玩家看不懂。可以用颜色加图标组合:蓝色移动、红色攻击、黄色交互,路线用箭头,当前目标用描边。
色弱玩家也要考虑。不要只靠红绿区分敌我范围。图案、透明度、边框和图标都能帮忙。战棋游戏信息密度高,可读性比视觉炫技更重要。
输入要处理误触和拖动
等距格子在手机上很容易误触。玩家想点角色,结果点到角色后面的格子;想拖动地图,却触发移动确认。输入系统要区分点击、长按、拖动和二次确认。高风险操作,比如走进敌方攻击范围,可以要求再次确认或显示风险提示。
拖动地图时要暂停格子 hover,避免屏幕移动过程中不断切换选中格。点击单位和点击空地的优先级也要明确:如果某个屏幕点同时命中单位 sprite 和地面格,通常应该优先选中单位。单位脚底区域可以作为选择 hit area,不要用整张立绘矩形,否则高角色会挡住后排格子。
PC 和移动端可以共用规则,但交互细节不同。PC 有 hover,移动端没有稳定 hover;PC 可以右键取消,移动端需要关闭按钮或点击空白取消。不要把桌面战棋交互原封不动搬到手机。
一个坐标转换和范围计算切口
下面的示例只展示核心形状。真实项目要加入 camera、地图偏移、tile origin 和触控容错,但规则层应该保持简洁。
type GridPos = { gx: number; gy: number };
type WorldPos = { x: number; y: number };
function gridToWorld(pos: GridPos, tileW: number, tileH: number): WorldPos {
return {
x: (pos.gx - pos.gy) * tileW / 2,
y: (pos.gx + pos.gy) * tileH / 2
};
}
function reachable(start: GridPos, move: number, map: TacticsMap) {
return dijkstra(start, next => map.neighbors(next), edge => map.moveCost(edge), move);
}
关键是 reachable 返回的是逻辑格,不是 sprite。表现层拿到格子后再转世界坐标画高亮。这样移动规则、AI、UI 和回放都能复用同一份结果。
上线前检查清单
上线前要检查:坐标类型是否清楚,点击格子是否和视觉一致,单位 depth 是否统一更新,移动范围和实际路径是否同源,阻挡规则是否能解释,手机误触是否可接受,色弱模式下范围是否可读,大型对象是否按脚底排序。
还要做边界测试:地图边缘点击、两个单位相邻、角色站在高物体后、飞行单位越过障碍、移动后触发剧情、AI 和玩家争用同一格、页面缩放后点击是否偏移。战棋系统最怕“多数时候正确”,因为一次错误移动就会毁掉一局策略判断。
AI 和玩家要共享网格规则
战棋项目里,AI 经常是后加的。玩家移动用一套可达范围,敌人行动为了赶进度又写一套简单距离判断,最后就会出现敌人穿墙、站到被占用格、攻击到玩家无法攻击的位置。只要是格子玩法,AI 和玩家必须共享同一套地图查询、移动成本和目标合法性判断。
AI 可以有自己的策略,但不能有自己的物理规则。它可以选择更激进的目标,可以评估攻击收益,可以故意站在危险格里,但它走到那个格子的过程仍然要通过同一个 path service。这样玩家看到敌人行动时,才会觉得“它很聪明”,而不是“它作弊”。
共享规则还有一个好处:调试工具可以同时解释玩家和 AI。点击一个敌人,面板显示它当前可达格、首选目标、路径成本和无法攻击的原因。策划调 AI 时,不需要猜它为什么不动。很多战棋 AI 问题不是决策算法复杂,而是它看到的地图和玩家看到的地图不是同一张。
回放和撤销要从格子状态开始
战棋玩家常希望撤销移动,开发团队也需要回放一回合。撤销不能只把 sprite 移回去,还要恢复逻辑格、行动点、触发器、buff、视野和临时高亮。最稳的方式是每个行动提交一个 command,记录行动前后的格子状态和规则结果。
例如移动 command 记录 unitId、from、to、path、cost、触发的事件。撤销时按相反顺序恢复;回放时按原顺序执行。攻击、拾取、开门也可以是 command。这样战棋系统会自然形成一条可审计的行动日志。
如果是联机或带排行榜的玩法,撤销可能只在本地练习模式允许,正式模式必须由服务器确认。即便如此,本地 command 仍然有价值,因为它能让表现动画、战斗日志和问题复盘都围绕同一份行动数据。
关卡编辑和战棋规则要一起验收
等距战棋的关卡不是摆好地形就结束。每张地图都要验收可达性、遮挡、出生点安全、AI 起手距离和任务目标位置。策划在地图上放一个高墙,可能视觉很好,但会挡住后排角色;放一个宝箱在角落,可能需要飞行单位才能拿到;放一个敌人太靠近出生点,第一回合就会压迫玩家。
建议为每张战棋地图生成一份调试报告:玩家出生点到关键目标的最短路径,所有单位初始格是否合法,AI 第一回合可攻击范围,遮挡物是否覆盖可点击格,移动范围是否有孤岛。报告不需要很漂亮,只要能在上线前提醒团队“这张图可能有问题”。
地图越多,越要把这些验收自动化。否则每次活动新增两张战棋图,都靠人工走完整局,迟早会漏掉一个不可达宝箱或错误出生点。
验收报告还可以留作版本证据。某张地图上线后如果玩家反馈 AI 第一回合压制太强,团队能回看当时的移动范围和攻击范围,而不是只靠当前版本重新推测。战棋数值和地图会不断变化,保存当时的诊断结果很有必要。
如果项目有赛季或活动地图,这份证据还能帮助复盘难度。玩家输在策略选择,还是输在出生点设计,数据和报告能给出更清楚的答案。
最后还要把镜头因素纳入验收。等距战棋里,镜头缩放和拖动会影响点击精度。某些格子在默认镜头下很清楚,缩放后却被 UI 遮住。移动端尤其需要检查底部技能栏、任务提示和回合按钮是否挡住可操作格。地图规则正确但玩家点不到,也等于不可用。
结语
Phaser 做等距战棋,不需要神秘引擎。真正重要的是把逻辑格、世界显示和输入转换分开,把寻路、范围和高亮建立在同一套规则上。画面可以是等距的,玩法必须是可解释的。
等距视角给玩家空间感,也给工程带来坐标和排序复杂度。只要边界清楚、调试可视化、输入容错足够,Phaser 完全可以支撑小而精的战棋和策略玩法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。