仇恨系统(Aggro/Threat System)是连接怪物 AI 与玩家行为的桥梁。在 Project Vampirefall 中,由于融合了 Action Roguelike(玩家移动)与 Tower Defense(固定防御),仇恨系统必须在“被动寻路”与“主动反击”之间找到平衡。
怪物在决定攻击谁之前,首先必须“感知”到目标。感知机制是各种仇恨类型的触发基础。
当怪物进入战斗状态后,它会维护一个 仇恨列表。列表中包含所有对它造成过伤害或产生过仇恨行为的实体。
| 仇恨类型 | 触发方式 | 初始仇恨值 | 衰减模式 | 优先级影响 |
|---|---|---|---|---|
| 基础寻路仇恨 (Base Pathfinding Threat) | 怪物诞生时,自动对“基地核心”生成。 | 极高(无限趋近) | 无衰减 | 只有当其他仇恨源足够高时才转移目标。 |
| 伤害仇恨 (Damage Threat) | 对怪物造成伤害。 | 1 伤害 = 1 Threat | 线性衰减(脱战后清零) | 随伤害量动态变化。 |
| 治疗仇恨 (Healing Threat) | 治疗队友或玩家自身。 | 治疗量 * 0.5 Threat | 线性衰减 | 对所有感知范围内的怪物生效。 |
| 嘲讽仇恨 (Taunt Threat) | 嘲讽技能。 | 目标当前最高仇恨值 + 固定值 (如 100) | 短暂爆发,快速衰减 | 强制改变目标,优先级最高。 |
| 环境仇恨 (Environmental Threat) | 破坏怪物附近的阻挡物(如墙壁、陷阱)。 | 固定值(如 50) | 无衰减 | 优先级低于动态仇恨,高于基础寻路。 |
| 技能仇恨 (Ability Threat) | 释放特定技能(如范围控场)。 | 根据技能强度计算 | 线性衰减 | 特定技能可附带额外仇恨修正。 |
Project Vampirefall 的特殊性在于:玩家不仅要杀怪,还要保护塔。因此,我们设计了多层次的优先级决策。
除了动态的仇恨值,我们引入静态优先级系数来修正怪物的行为倾向。这个系数会乘以最终计算出的仇恨值,形成怪物的“最终威胁值” (Final Threat)。
\[FinalThreat = (RawDynamicThreat + BasePathfindingThreat + EnvironmentalThreat) \times EntityTypePriorityMod \times DistanceFactor\]| 实体类型 (EntityType) | 仇恨计算公式 | Priority Mod | 行为逻辑 |
|---|---|---|---|
| 基地核心 (Nexus) | 仅 BasePathfindingThreat |
10.0x | 所有怪物的默认且最终目标。只有当其他目标提供足够高的 FinalThreat 才会转移。 |
| 嘲讽单位 (Tank Tower) | DynamicThreat (含嘲讽) |
5.0x | 强制吸引火力。高额 PriorityMod 确保其能稳稳地拉住仇恨。 |
| 玩家 (Player) | DynamicThreat |
2.0x | 怪物倾向于攻击高威胁的玩家。玩家的机动性是其优势,但高输出会快速积累仇恨。 |
| 防御塔 (Standard Tower) | DynamicThreat |
1.0x | 正常优先级。怪物会在 Nexus、玩家和防御塔之间平衡选择。 |
| 召唤物 (Minions) | DynamicThreat |
0.5x | 除非挡路或仇恨值极高,否则怪物懒得理会。 |
| 阻挡物 (Wall/Obstacle) | 仅 EnvironmentalThreat |
0.8x | 仅当路径受阻时才成为目标,其仇恨值仅影响破拆速度而非优先追击。 |
为了防止玩家利用高移速将怪物无限拉离防线(导致塔防失效),引入 Leash (牵引绳) 机制。
每种仇恨类型独立计算,然后叠加。
DamageThreat = ActualDamageDealt * DamageThreatModifier
DamageThreatModifier:不同单位对仇恨贡献不同(如玩家的攻击制造更多仇恨)。HealingThreat = ActualHealingDone * HealingThreatModifier
TauntThreat = MaxThreatOnMonster + FixedTauntValue
MaxThreatOnMonster:怪物当前仇恨列表中最高的值。FixedTauntValue:嘲讽技能额外增加的仇恨。为了避免怪物无视脸上的坦克去追远处的弓箭手,距离会影响仇恨判定。
\[DistanceFactor = 1 + \frac{K}{Distance}\]EffectiveThreat = RawThreat * DistanceFactor。为了防止怪物在两个仇恨相近的目标间频繁转头(Ping-Ponging),切换目标需要满足阈值。
FinalThreat > 当前目标 FinalThreat 的 110% 时,才切换。FinalThreat 最高的新目标。不同类型的怪物使用不同的仇恨逻辑模板,通过配置 EntityTypePriorityMod 和其他参数实现。
EntityTypePriorityMod(Wall) = 10.0, EntityTypePriorityMod(Player) = 0.0。它们对 EnvironmentalThreat 的响应权重极高。DistanceFactor,直接锁定 LowestHP 或 CastingTarget,并可能拥有特殊的路径规划能力(如跳跃、闪现)。DamageThreatModifier x 2.0,短时间内只会追击此目标。DamageThreat 但高 AbilityThreat。其技能可能附带强制嘲讽或强制转火效果。建议使用 IAggroTarget 接口和 AggroAgent 组件。
public interface IAggroTarget {
float GetThreatModifier(AggroType aggroType); // 不同仇恨类型可有不同修正
EntityType GetEntityType(); // 返回实体类型 (Player, TankTower, Nexus)
bool IsValid(); // 是否死亡、隐身或不可作为目标
Vector3 Position { get; }
// 可以添加获取当前血量、是否施法等信息的方法
}
public enum EntityType { Player, TankTower, StandardTower, Minion, Nexus, Obstacle }
public enum AggroType { Damage, Healing, Taunt, Environmental, Ability }
public class AggroAgent : MonoBehaviour { // 挂在怪物身上
// 仇恨列表:Key=目标 (IAggroTarget), Value=当前仇恨值
private Dictionary<IAggroTarget, float> threatTable = new Dictionary<IAggroTarget, float>();
public EntityType monsterType; // 配置怪物自身类型,用于特殊行为
// 在怪物受到伤害、感知到治疗、被嘲讽等时调用
public void AddThreat(IAggroTarget source, AggroType type, float rawValue) {
float threat = rawValue;
// 应用来源的 GetThreatModifier
threat *= source.GetThreatModifier(type);
if (!threatTable.ContainsKey(source)) threatTable[source] = 0;
threatTable[source] += threat;
CleanThreatTable(); // 清理无效目标
CheckSwitchTarget(); // 检查是否需要切换目标
}
// ... (Previous code)
private IAggroTarget FindBestTarget() {
IAggroTarget bestTarget = null;
float highestThreat = -1f;
foreach (var entry in threatTable) {
var target = entry.Key;
float rawThreat = entry.Value;
if (!target.IsValid()) continue;
// 1. 计算 EntityType 优先级修正
float priorityMod = GetPriorityModifier(target.GetEntityType());
// 2. 计算距离权重修正
float distance = Vector3.Distance(transform.position, target.Position);
float distanceFactor = 1f + (distanceSensitivityK / Mathf.Max(distance, 0.1f)); // 防止除以0
// 3. 合成最终仇恨
float finalThreat = rawThreat * priorityMod * distanceFactor;
if (finalThreat > highestThreat) {
highestThreat = finalThreat;
bestTarget = target;
}
}
return bestTarget;
}
private void CheckSwitchTarget() {
IAggroTarget potentialTarget = FindBestTarget();
if (potentialTarget == null || potentialTarget == currentTarget) return;
float currentFinalThreat = CalculateFinalThreat(currentTarget);
float newFinalThreat = CalculateFinalThreat(potentialTarget);
// 阈值判定:防止 Ping-Ponging
float thresholdMultiplier = (IsMeleeRange(potentialTarget)) ? 1.1f : 1.3f;
if (newFinalThreat > currentFinalThreat * thresholdMultiplier) {
SetTarget(potentialTarget);
}
}
private float GetPriorityModifier(EntityType type) {
// 这里可以配置化,或者读取 ScriptableObject 配置
switch (type) {
case EntityType.TankTower: return 5.0f;
case EntityType.Player: return 2.0f;
case EntityType.Nexus: return 10.0f; // 特殊处理
default: return 1.0f;
}
}
}
在 Scene View 中绘制线条:
FinalThreat 数值和 EntityType。BasePathfindingThreat 会临时转嫁到阻挡物上。DamageThreat 和 AbilityThreat 清零(或暂时冻结)。怪物转向仇恨列表中的第二目标(通常是塔)。如果列表为空,回满血跑回巡逻路径。隐身状态的 IAggroTarget.IsValid() 返回 false。