AI 决策层(E001CaoZhiAi + PerceptionStateMachine)彻底移除所有 actuation: 不再有 Mover.Stop/FacePlayer/UseChaseSpeed/WalkRandom、也不再 SetPhase。 每个 AI 状态只声明触发哪个能力 id,行为(移动/朝向/停/速度/动画/伤害/ 阶段声明)全部下沉到对应能力里。 - 新增 IdleAbility/PatrolAbility/AlertAbility:待机静止、巡逻游走、 警觉朝向玩家,各自 SetAiPhase 驱动阶段动画。 - ContactChaseAbility 追击进入时自设 RunSpeed + Chase 阶段(原在 AI)。 - PerceptionStateMachine 改为 AddAbilityState:进入/每帧 EnsureAbility (被打断/受击后自动重触发)、离开 InterruptAbilities;转换规则不变。 - IEnemyActor/EnemyBrainContext 移除已无用的 SetPhase,保留 HasAlertState。 - 新增 SO:ABL_E001_Idle(e001_idle)、ABL_E001_Patrol(e001_patrol)。 - 脚手架 PlaceE001 与 ENM_CaoZhi 预制体同步为四能力结构;TestRoomA 实例替换为规范预制体实例。 PlayMode 验证:Chase→ContactChaseAbility、脱离感知→Move_Patrol→ PatrolAbility,阶段由能力设置,无报错。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.2 KiB
C#
115 lines
4.2 KiB
C#
using UnityEngine;
|
|
using BaseGames.AI;
|
|
|
|
namespace BaseGames.Enemies
|
|
{
|
|
/// <summary>
|
|
/// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。
|
|
/// 状态逻辑只依赖这些接口,不直接引用具体子系统。
|
|
/// </summary>
|
|
public sealed class EnemyBrainContext : IAiContext, ISensor, IMover, ICombatant, IActorVitals, IEnemyActor
|
|
{
|
|
readonly EnemyBase _enemy;
|
|
readonly Blackboard _blackboard = new Blackboard();
|
|
Vector2 _lastKnown;
|
|
float _lostTimer;
|
|
|
|
public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; }
|
|
|
|
// ---- IAiContext ----
|
|
public ISensor Sensor => this;
|
|
public IMover Mover => this;
|
|
public ICombatant Combat => this;
|
|
public IActorVitals Vitals => this;
|
|
public Blackboard Blackboard => _blackboard;
|
|
|
|
/// <summary>每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时(供需要的敌人用)。</summary>
|
|
public void Refresh(float dt)
|
|
{
|
|
if (InVisionZone() && _enemy.PlayerTransform != null)
|
|
{
|
|
_lastKnown = _enemy.PlayerTransform.position;
|
|
_lostTimer = 0f;
|
|
}
|
|
else
|
|
{
|
|
_lostTimer += dt;
|
|
}
|
|
}
|
|
|
|
/// <summary>对象池复用时清空临时态。</summary>
|
|
public void ResetScratch()
|
|
{
|
|
_blackboard.Clear();
|
|
_lastKnown = _enemy.transform.position;
|
|
_lostTimer = 0f;
|
|
}
|
|
|
|
// ---- ISensor ----(纯感知查询,转换由 AI 状态机决定)
|
|
public bool InChaseZone() => _enemy.InChaseZone();
|
|
public bool InVisionZone() => _enemy.InVisionZone();
|
|
public bool SeesPlayer() => _enemy.IsPlayerVisible();
|
|
public bool InRange(float range) => _enemy.IsPlayerInRange(range);
|
|
public bool LostFor(float seconds) => !InVisionZone() && _lostTimer >= seconds;
|
|
public Vector2 LastKnown => _lastKnown;
|
|
|
|
// ---- IMover ----
|
|
public void MoveTo(Vector2 target) => _enemy.MoveTo(target);
|
|
public void FacePlayer() => _enemy.FacePlayer();
|
|
public void Stop() => _enemy.StopMovement();
|
|
public void WalkRandom()
|
|
{
|
|
var nav = _enemy.Nav;
|
|
if (nav != null && !nav.IsMoving) nav.WalkToRandom();
|
|
}
|
|
public void LookAround() => _enemy.BeginLookAround();
|
|
public void UseChaseSpeed() => _enemy.Nav?.SetSpeed(_enemy.Stats != null ? _enemy.Stats.RunSpeed : 0f);
|
|
public void UsePatrolSpeed() => _enemy.Nav?.SetSpeed(_enemy.Stats != null ? _enemy.Stats.WalkSpeed : 0f);
|
|
|
|
// ---- ICombatant ----
|
|
public bool UseAbility(string abilityId)
|
|
{
|
|
var a = _enemy.Abilities?.Get(abilityId);
|
|
return a != null && a.Execute();
|
|
}
|
|
public bool IsAbilityRunning(string abilityId = null)
|
|
{
|
|
var reg = _enemy.Abilities;
|
|
if (reg == null) return false;
|
|
if (abilityId != null)
|
|
{
|
|
var a = reg.Get(abilityId);
|
|
return a != null && a.IsRunning;
|
|
}
|
|
var all = reg.All;
|
|
for (int i = 0; i < all.Count; i++)
|
|
if (all[i].IsRunning) return true;
|
|
return false;
|
|
}
|
|
public bool NoAbilityRunning => !IsAbilityRunning();
|
|
public bool CanUseAbility(string abilityId)
|
|
{
|
|
var a = _enemy.Abilities?.Get(abilityId);
|
|
return a != null && a.CanUse;
|
|
}
|
|
public void InterruptAbilities()
|
|
=> _enemy.Abilities?.InterruptAll(Abilities.InterruptReason.ExternalRequest);
|
|
|
|
// ---- IActorVitals ----
|
|
public bool IsAlive => _enemy.IsAlive;
|
|
public bool IsControllable => _enemy.CurrentState == EnemyStateType.Controlled;
|
|
public float HpPercent
|
|
{
|
|
get
|
|
{
|
|
var stats = _enemy.Stats;
|
|
return stats != null && stats.MaxHP > 0 ? (float)stats.CurrentHP / stats.MaxHP : 0f;
|
|
}
|
|
}
|
|
public bool HpBelow(float ratio) => HpPercent < ratio;
|
|
|
|
// ---- IEnemyActor ----
|
|
public bool HasAlertState => _enemy.StatsSO != null && _enemy.StatsSO.HasAlertState;
|
|
}
|
|
}
|