本文档基于 Unified Decision System 架构,深入解析 Project Vampirefall 中的索敌逻辑。索敌(Targeting)是塔防与 ARPG 的核心交互体验,它决定了玩家感受到的“智能”程度。
初级索敌逻辑通常是:“找到最近的敌人”。 但在本作中,索敌是一个 加权决策过程 (Weighted Decision Process)。
每一次索敌计算 (Tick) 都遵循以下管道:
Scorer,计算总分。我们在代码中通过组合不同的 IScorer 来实现复杂的战术逻辑。
Score = 1 / Distance (极度偏好近身)Score = Distance (偏好远程,如迫击炮无法攻击近身)Score = 1 - (CurrentHP / MaxHP)。Score = CurrentHP 或 Score = CurrentHP / MaxHP。EntityType 给定固定分值。Boss: 1000分Elite: 500分Special (炸弹人): 2000分 (绝对优先处理)Minion: 10分[Soaked] (湿润) 的目标评分 x 2.0。[Stunned] (眩晕) 的目标评分 x 3.0。以下是游戏中几种典型单位的索敌配置方案。
定位:清理冲到脸上的杂兵,防漏怪。
| 评分器 | 权重 (Weight) | 说明 |
|---|---|---|
| DistanceScorer | 3.0 | 极度优先攻击最近的单位。 |
| FixedPriorityScorer | 0.5 | 稍微偏好精英怪,但主要还是看距离。 |
| HealthScorer (Low) | 1.0 | 优先补刀残血。 |
定位:高单发伤害,极慢攻速。必须避免伤害溢出。
| 评分器 | 权重 (Weight) | 说明 |
|---|---|---|
| Filter: Overkill | N/A | [关键] 如果目标的 HP < 塔攻击力,直接剔除(防止大炮打蚊子)。 |
| FixedPriorityScorer | 5.0 | 必须优先打 Boss 和 Elite。 |
| HealthScorer (High) | 2.0 | 在同级怪物中,选血最多的打。 |
| DistanceScorer | -0.5 | 稍微偏好远处的(反向权重),避免转火频繁。 |
定位:AOE 连锁攻击,依赖元素反应。
| 评分器 | 权重 (Weight) | 说明 |
|---|---|---|
| TagSynergyScorer | 4.0 | 寻找带有 [Wet] 或 [Conductive] 标签的敌人。 |
| ClusterScorer | 2.0 | [高级] 寻找周围敌人最密集的那个点作为主目标(最大化弹射收益)。 |
定位:切后排,恶心玩家。
| 评分器 | 权重 (Weight) | 说明 |
|---|---|---|
| FixedPriorityScorer | 10.0 | Player > SupportTower > TankTower。 |
| HealthScorer (Low) | 3.0 | 专挑血少的打。 |
| DistanceScorer | 0.0 | 无视距离。哪怕要绕路,也要去切后排。 |
如何在 Unity 中配置一个“狙击塔”的决策引擎:
public class SniperTower : MonoBehaviour
{
private DecisionEngine<Enemy> _targetingSystem;
void Start() {
_targetingSystem = new DecisionEngine<Enemy>();
// 1. 过滤:必须在射程内,且活着
_targetingSystem.AddFilter(new RangeFilter(transform, 50f));
_targetingSystem.AddFilter(new AliveFilter());
// 2. 过滤:防止伤害溢出 (Overkill Protection)
// 假设塔攻击力是 500
_targetingSystem.AddFilter(new MinHealthFilter(500f));
// 3. 评分:优先打 Boss (权重极高)
_targetingSystem.AddScorer(new EntityTypeScorer()
.SetScore(EntityType.Boss, 1000f)
.SetScore(EntityType.Elite, 500f));
// 4. 评分:优先打满血的 (权重中等)
_targetingSystem.AddScorer(new HealthScorer(HealthScorerMode.Highest, 10f));
}
void Update() {
if (Time.frameCount % 10 == 0) { // 分帧优化
var enemies = EnemyManager.GetAllEnemies();
var target = _targetingSystem.SelectBest(enemies, GetContext());
if (target != null) Fire(target);
}
}
}
为了防止塔的炮口在两个分数相近的敌人之间疯狂抽搐(Ping-Pong),我们需要引入“粘性”。
LastTarget (上一次锁定的目标) 一个额外的分数加成。Score += (target == LastTarget) ? StickinessBonus : 0;千万不要遍历全图 EnemyManager.GetAllEnemies()。
Range 覆盖的 Grid 内的敌人列表作为 Candidates。索敌不需要每帧都跑。