Phaser 塔防索敌系统:炮塔、仇恨权重和弹道不要只写最近目标

从 Phaser 塔防实战出发,讲解炮塔索敌、仇恨权重、弹道预测、升级分支、调试工具和性能边界。

塔防的第一手感来自“塔为什么打它”

很多 Phaser 塔防原型一开始会用最简单的逻辑:遍历范围内敌人,选择距离炮塔最近的一个,然后发射子弹。这个规则可以让炮塔动起来,却很快会暴露问题。快到终点的敌人明明快漏了,炮塔还在打刚进入范围的小怪;高护甲敌人被低伤害塔一直锁定,浪费输出;飞行怪经过时,地面塔误打;玩家升级了减速塔,却发现它总是把减速浪费在已经被减速的目标上。塔防不是“有敌人就打”,而是玩家花钱建塔后,系统要用可解释的规则把这笔投资变成可靠输出。

索敌系统的质量会直接影响玩家对策略的判断。若玩家不能预测炮塔会打谁,摆塔和升级就会变成碰运气。一个成熟的索敌系统通常会支持多个策略:最近、最远、最靠近终点、血量最高、血量最低、未被减速、护甲最低、优先飞行、优先精英。更进一步,某些塔不是选一个目标,而是选一组目标、路径点或区域中心。Phaser 层面只是画炮塔旋转、播放发射动画和弹道,真正的索敌应该在独立的战斗模型里完成。

把索敌拆成候选、评分和锁定

直接在炮塔里写 findClosestEnemy() 太局限。更稳的结构是三步:先找候选,再评分,再锁定。候选阶段按硬条件过滤,比如是否在范围内、是否可被该塔攻击、是否隐身、是否存活。评分阶段按塔的策略给每个候选计算分数。锁定阶段决定是否切换目标、是否保持当前目标、是否等待下一次扫描。这个拆分能让规则扩展时不互相污染。

候选过滤要尽可能便宜。塔很多、敌人很多时,每帧让每座塔遍历所有敌人会很快变慢。可以按固定间隔扫描,比如每 100ms 或 200ms,而不是每帧;也可以把敌人按路径段或网格索引。多数塔防不需要每帧重新选目标,因为炮塔转向和攻击频率本身就有节奏。只有激光塔、跟踪射线这类连续攻击才需要更高频更新。

flowchart TD
  A["TowerTick:炮塔冷却完成或扫描间隔到达"] --> B["CandidateQuery:范围、类型、状态过滤"]
  B --> C["TargetScorer:按策略计算分数"]
  C --> D["TargetLock:保持当前目标或切换"]
  D --> E["FireController:创建弹道、射线或范围伤害"]
  E --> F["ProjectileSystem:飞行、命中、溅射"]
  F --> G["DamageResolver:扣血、减速、护甲修正"]
  G --> H["FeedbackLayer:炮口、命中、飘字"]

仇恨权重比单一规则更灵活

“优先终点”很直观,但真实塔防常需要复合规则。比如火炮想打聚集目标,狙击塔想打高价值精英,减速塔想打未被减速且靠前的敌人。可以把评分写成权重组合:距离炮塔、距离终点、当前血量、最大血量、敌人标签、已有状态、路径密度。每个塔配置一组权重,最终分数最高者成为目标。这样策划可以调塔的性格,而不是为每种塔写一套代码。

权重系统要保持可解释。调试模式下,选中炮塔后应该显示候选敌人的分数拆解:终点权重 40,未减速奖励 20,距离惩罚 -5,总分 55。玩家不需要看到这些,但设计师需要。否则某个塔“感觉不聪明”时,团队只能猜。权重过多也会难调,第一版建议控制在 4 到 6 个维度。

目标锁定避免炮塔抽搐

如果每次扫描都立刻切换到最高分目标,炮塔可能在两个敌人之间来回抖,弹道方向也显得不稳定。目标锁定可以设置粘性:当前目标仍然有效且分数没有明显落后时继续攻击;只有新目标分数超过当前目标一定阈值,或当前目标离开范围、死亡、不可攻击时才切换。激光塔和持续减速塔尤其需要锁定,否则效果会断断续续。

锁定时间也要考虑。狙击塔瞄准 0.5 秒后开枪,瞄准期间目标死亡时可以重新选择;目标只是稍微离开最佳位置,不一定要重瞄。炮塔转向速度也能参与锁定,炮塔当前面向左侧时,右侧新目标虽然分数高,但转过去需要时间,未必值得切换。这样炮塔表现更像实体,而不是全知的数学函数。

弹道预测不能只瞄当前位置

慢速炮弹打移动敌人时,如果只瞄当前位置,会经常落空。可以做简单预测:根据敌人当前速度和炮弹速度估算拦截点。塔防路径通常固定,敌人的未来位置可以沿路径推进,而不是用直线速度。对于抛物线炮弹或范围炮,可以瞄准一群敌人未来经过的路径点。预测不需要完美,重点是让炮弹看起来合理。

有些塔故意不预测,比如廉价箭塔;有些塔升级后获得预测能力,这会成为可感知的升级点。弹道预测也要和命中规则一致。如果表现上炮弹擦过敌人却没命中,玩家会不满;如果表现上落在空地却造成伤害,也会困惑。ProjectileSystem 应负责碰撞半径、命中时机和溅射范围的可视化。

一个可扩展的索敌评分器

下面的代码展示一个简化评分器。真实项目中,路径进度、敌人状态和塔配置会更复杂,但结构足够清晰。

interface EnemySnapshot {
  id: string;
  x: number;
  y: number;
  hp: number;
  maxHp: number;
  pathProgress: number;
  tags: Set<string>;
  states: Set<string>;
}

interface TowerTargetWeights {
  progress: number;
  lowHp: number;
  highHp: number;
  notSlowed: number;
  elite: number;
}

export function scoreTarget(enemy: EnemySnapshot, weights: TowerTargetWeights) {
  let score = 0;
  score += enemy.pathProgress * weights.progress;
  score += (1 - enemy.hp / enemy.maxHp) * weights.lowHp;
  score += enemy.maxHp * weights.highHp;
  if (!enemy.states.has("slowed")) score += weights.notSlowed;
  if (enemy.tags.has("elite")) score += weights.elite;
  return score;
}

这段函数没有接触 Phaser 对象,只用快照数据。炮塔扫描时把敌人状态转换为快照,评分器返回数字。这样索敌可以单元测试,也能在调试面板复用。若以后加入隐身、护盾、飞行、召唤物,只需要扩展快照和权重,而不是把 Phaser Sprite 传进业务逻辑。

升级分支应该改变规则而不只是加伤害

塔防升级如果只是伤害加 10%,很快会乏味。索敌系统能为升级提供更有趣的分支:箭塔升级为穿透箭后优先直线敌人多的路径;冰塔升级后优先未减速目标;火炮升级后优先密集区域;狙击塔升级后优先精英并获得弹道预测。规则变化会让玩家重新思考摆放位置,比单纯数值成长更有策略感。

升级描述要写清楚。比如“优先攻击最接近出口的未减速敌人”,比“智能索敌”更可信。玩家知道规则,才会主动利用规则。调试时也要确认升级确实改变评分配置,而不是只改 UI 文案。

性能与可视化

塔多敌多时,索敌是热点。可以采用空间索引、路径段索引、扫描分帧和缓存。比如每座塔不在同一帧扫描,把扫描分散到 5 帧;敌人进入或离开路径段时更新索引;飞行敌人和地面敌人分开列表。不要过早写复杂四叉树,但要避免最朴素的全量遍历长期存在。

可视化非常重要。开发模式选中炮塔后显示攻击范围、当前目标线、候选敌人分数、下一次开火时间和弹道预测点。很多“塔不打人”的 bug 其实是目标标签不匹配、范围中心错位、路径进度没更新或敌人已经死亡但没从列表移除。没有可视化,只能在代码里猜。

上线前检查清单

确认索敌分为候选、评分、锁定;确认每种塔有明确目标策略;确认当前目标有粘性,避免频繁抖动;确认弹道表现和命中规则一致;确认升级分支能改变索敌或弹道行为;确认飞行、隐身、护盾、精英等标签参与过滤;确认扫描频率和敌人数量在低端机上可承受;确认调试模式能显示范围、目标和分数;确认失败漏怪日志能记录当时炮塔目标和敌人路径进度。

塔防的策略感来自玩家对规则的预期。Phaser 能很快把炮塔和敌人画出来,但真正让游戏耐玩的是索敌、弹道和升级之间的逻辑。让炮塔打得聪明,而且让这种聪明能被解释,玩家才会愿意继续优化布局。

路径系统和索敌的关系

塔防敌人通常沿路径移动,路径进度是很重要的索敌信息。不要只用敌人的世界坐标判断“离终点最近”,因为路径可能绕圈,世界距离近不代表快到终点。每个敌人应保存 pathId、segmentIndex、segmentProgress 和 totalProgress。索敌评分使用 totalProgress,UI 调试也显示它。分叉路径更要注意,某些敌人走短路,某些敌人走长路,进度必须按各自路径归一化。

如果关卡中有传送门、加速带、减速区,路径进度和实际威胁还不完全等价。快到终点但被强减速的敌人,威胁可能低于后方高速冲刺怪。可以在评分里加入 estimatedTimeToExit,而不是只看 progress。这个值由路径剩余距离和当前速度估算。高阶炮塔使用时间威胁索敌,会显得更聪明。

范围塔和区域目标

不是所有塔都选单个敌人。火炮、毒雾、冰冻塔更适合选择区域中心。区域目标可以从候选敌人聚类得到:找到范围内敌人最密集的一段路径,或选择未来 0.8 秒内敌人会聚集的位置。实现上可以先采样若干候选点,例如敌人当前位置、路径前方点、关键弯道点,再计算每个点覆盖的敌人数量和总价值。分数最高的点成为目标。

区域塔要给玩家反馈。选中火炮时,显示预计爆炸圈和当前会命中的敌人数量;开火前有短暂落点警示。这样玩家能理解为什么火炮没有打最近敌人,而是在前方弯道预判。范围塔的乐趣来自“我把塔放在这里能炸一群”,系统要把这个逻辑表现出来。

内容配置和回归测试

每种塔的索敌策略都应进入配置。配置字段不要太底层,可以提供模板:firstlaststrongestweakestunslowedcluster。高级配置再覆盖权重。这样策划能快速做常见塔,程序也保留扩展空间。配置导出时做校验:地面塔不能优先飞行但又不能攻击飞行,权重不能全为 0,锁定阈值不能为负。

回归测试可以构造固定路径和敌人队列,断言某座塔在某时刻选择哪个目标。塔防索敌 bug 很适合自动测试,因为输入和输出明确。每次改评分权重或路径数据后,跑一遍这些测试,能避免“修了冰塔,狙击塔开始乱打”的连锁问题。Phaser 负责表现,但索敌本身完全可以脱离浏览器测试。

玩家可控策略和自动策略

高级塔防可以允许玩家切换炮塔策略,比如“优先出口”“优先精英”“优先血少”。这会增加策略深度,但也会增加 UI 负担。建议只给少数关键塔开放策略切换,普通塔使用固定逻辑。策略切换要有即时反馈,选中炮塔后显示当前策略和目标线。不要把策略藏在二级菜单里,否则大多数玩家不会用。

策略切换还要保存。玩家精心设置了一排塔,切场景或读档后不能全部回默认。保存时记录 towerId 和 targetingMode。升级后如果策略不再适用,比如普通箭塔升级成范围塔,系统应迁移到最接近的策略,并提示变化。自动策略和玩家选择冲突时,玩家选择优先。

线上问题如何定位

塔防线上反馈常常是“塔不打怪”“明明漏怪了”。日志可以记录漏怪前 5 秒内附近炮塔的目标、冷却、范围、拒绝原因。拒绝原因包括不在范围、类型不匹配、隐身、当前目标锁定、冷却未完成。这样开发能判断是规则问题、配置问题还是玩家误解。没有日志时,索敌问题很难从录屏判断。

如果有关卡回放,索敌日志还可以叠加显示。重放时看到每座塔当时为什么选择某个目标,设计师能更快调整。塔防的策略争议很多,透明工具能减少无效讨论。

继续阅读

探索更多技术文章

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

全部文章 返回首页