refactor(ai): E001 贯彻"AI只决策"——每状态=一能力(Model A)
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>
This commit is contained in:
@@ -4,14 +4,13 @@ namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// E001(草蛭)AI —— 纯决策层,复用 PerceptionStateMachine 的"待机/警觉/追击/巡逻"统一规则。
|
||||
/// 只声明差异:待机=伪装静止、警觉=张口(e001_alert)、追击=委托 e001_chase 能力、R=巡逻。
|
||||
/// 追击移动/接触伤害由能力实现;退出追击由 AI 中断能力。依据 Docs/Game/Design/小怪设计-程序开发文档-01.md。
|
||||
/// 只声明差异:每个状态触发哪个能力(待机=伪装静止、巡逻=游走、警觉=张口、追击=接触追击)。
|
||||
/// AI 只决策(进哪个状态/转换条件/触发哪个能力);移动/朝向/停/速度/动画/伤害/阶段声明全部在能力里实现。
|
||||
/// 依据 Docs/Game/Design/小怪设计-程序开发文档-01.md。
|
||||
/// </summary>
|
||||
[AiDefinition("E001")]
|
||||
public sealed class E001CaoZhiAi : AiScript
|
||||
{
|
||||
static IEnemyActor Actor(IAiContext c) => (IEnemyActor)c;
|
||||
|
||||
protected override void Build(BrainBuilder b)
|
||||
{
|
||||
PerceptionStateMachine.Add(b, new PerceptionStateMachine.Config
|
||||
@@ -23,18 +22,12 @@ namespace BaseGames.Enemies
|
||||
Death = "Death",
|
||||
Entry = "Idle_Disguise",
|
||||
Rest = "Move_Patrol",
|
||||
ChaseAbilityId = "e001_chase",
|
||||
|
||||
OnIdleEnter = c => { c.Mover.Stop(); Actor(c).SetPhase(AiPhase.Idle); },
|
||||
OnPatrolEnter = c => { c.Mover.UsePatrolSpeed(); Actor(c).SetPhase(AiPhase.Patrol); },
|
||||
OnPatrolTick = c => c.Mover.WalkRandom(),
|
||||
// 警觉:停下、朝向玩家、切 Alert 阶段(播警觉动画+feedback)、张口占位
|
||||
OnAlertEnter = c => { c.Mover.Stop(); c.Mover.FacePlayer(); Actor(c).SetPhase(AiPhase.Alert); c.Combat.UseAbility("e001_alert"); },
|
||||
// 追击进入:切追击速度、朝向、Chase 阶段(追击能力由 PerceptionStateMachine 触发)
|
||||
OnChaseEnter = c => { c.Mover.UseChaseSpeed(); c.Mover.FacePlayer(); Actor(c).SetPhase(AiPhase.Chase); },
|
||||
// 追击退出:中断追击能力(停移动、关接触伤害)
|
||||
OnChaseExit = c => c.Combat.InterruptAbilities(),
|
||||
OnDeathEnter = c => c.Mover.Stop(),
|
||||
IdleAbilityId = "e001_idle",
|
||||
PatrolAbilityId = "e001_patrol",
|
||||
AlertAbilityId = "e001_alert",
|
||||
ChaseAbilityId = "e001_chase",
|
||||
// DeathAbilityId 留空:死亡演出走物理状态机(EnemyBase.PerformDeath)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ namespace BaseGames.Enemies
|
||||
public bool HpBelow(float ratio) => HpPercent < ratio;
|
||||
|
||||
// ---- IEnemyActor ----
|
||||
public void SetPhase(AiPhase phase) => _enemy.SetAiPhase(phase);
|
||||
public bool HasAlertState => _enemy.StatsSO != null && _enemy.StatsSO.HasAlertState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人侧的决策输出接口:AI 决策层用它"声明"当前行为阶段,由敌人模块(EnemyBase)负责
|
||||
/// 实现(播放阶段动画、供警戒/群体逻辑消费)。AiPhase 属 Enemies 命名空间,
|
||||
/// 故不放进通用 BaseGames.AI 接口;由 EnemyBrainContext 实现,Enemies 侧的 AiScript 使用。
|
||||
/// 敌人侧的决策输入接口:AI 决策层用它读取敌人的决策参数(是否有警觉态等)。
|
||||
/// 行为阶段(AiPhase)的声明已下沉到能力里(每状态一能力),AI 不再直接声明阶段。
|
||||
/// 由 EnemyBrainContext 实现,Enemies 侧的 AiScript 使用。
|
||||
/// </summary>
|
||||
public interface IEnemyActor
|
||||
{
|
||||
/// <summary>声明当前 AI 行为阶段(EnemyBase 据此自动播放对应动画)。</summary>
|
||||
void SetPhase(AiPhase phase);
|
||||
|
||||
/// <summary>该敌人是否有警觉状态(勾选项,来自 EnemyStatsSO)。false 时未发现态直接追击、不经警觉。</summary>
|
||||
bool HasAlertState { get; }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using BaseGames.AI;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 可复用的"待机/警觉/追击/巡逻"感知状态机(统一规则,见 spec《敌人感知↔状态》)。
|
||||
/// 职责边界:**状态转换逻辑在 AI 层(本类)**;感知系统只回答 InChaseZone/InVisionZone。
|
||||
/// 纯决策:AI 只决定"进哪个状态 / 转换条件 / 每状态触发哪个能力";
|
||||
/// 每个状态的行为(移动/朝向/动画/伤害/阶段声明)全部封装在对应能力里(AI 不做任何 actuation)。
|
||||
/// 规则:
|
||||
/// 待机/巡逻: 在追逐区→追击(优先); 否则 有警觉且在视野→警觉; 否则保持。
|
||||
/// 警觉: 在追逐区→追击; 否则 脱离视野→R; 否则保持警觉。
|
||||
/// 追击: 委托能力; 脱离视野→R(永不回警觉)。
|
||||
/// 缺省自动降级:无警觉(HasAlert=false)→警觉分支不触发;无视野槽→维持退化用追逐区。
|
||||
/// 追击: 脱离全部感知(追逐区且视野区都不在)→R(永不回警觉)。
|
||||
/// 缺省自动降级:无警觉(HasAlert=false)→警觉分支不触发;某状态无能力 id→该状态不触发能力。
|
||||
/// </summary>
|
||||
public static class PerceptionStateMachine
|
||||
{
|
||||
@@ -23,14 +23,12 @@ namespace BaseGames.Enemies
|
||||
public string Death = "Death";
|
||||
public string Entry = "Idle"; // 初始态
|
||||
public string Rest = "Patrol"; // R:降级(退出追击/警觉丢失)回到的静止态
|
||||
public string ChaseAbilityId; // 追击委托的能力 id(进入追击即触发;退出由 OnChaseExit 中断)
|
||||
public Action<IAiContext> OnIdleEnter;
|
||||
public Action<IAiContext> OnPatrolEnter;
|
||||
public Action<IAiContext> OnPatrolTick;
|
||||
public Action<IAiContext> OnAlertEnter;
|
||||
public Action<IAiContext> OnChaseEnter;
|
||||
public Action<IAiContext> OnChaseExit;
|
||||
public Action<IAiContext> OnDeathEnter;
|
||||
// 每状态对应的行为能力 id(AI 进入状态即触发、离开即中断)
|
||||
public string IdleAbilityId;
|
||||
public string PatrolAbilityId;
|
||||
public string AlertAbilityId;
|
||||
public string ChaseAbilityId;
|
||||
public string DeathAbilityId;
|
||||
}
|
||||
|
||||
public static void Add(BrainBuilder b, Config c)
|
||||
@@ -38,39 +36,43 @@ namespace BaseGames.Enemies
|
||||
b.Entry(c.Entry);
|
||||
b.Global().To(c.Death).OnEvent(AiSignal.Died); // 受击致死 → 死亡(全局最高优先)
|
||||
|
||||
// 待机 / 巡逻:未发现态,同样的升级转换(追逐优先、其次警觉)
|
||||
AddUndiscovered(b.State(c.Idle).OnEnter(x => c.OnIdleEnter?.Invoke(x)), c);
|
||||
AddUndiscovered(
|
||||
b.State(c.Patrol)
|
||||
.OnEnter(x => c.OnPatrolEnter?.Invoke(x))
|
||||
.Tick(x => c.OnPatrolTick?.Invoke(x)),
|
||||
c);
|
||||
// 待机 / 巡逻:未发现态,升级转换相同(追逐优先、其次警觉)
|
||||
AddAbilityState(b, c.Idle, c.IdleAbilityId)
|
||||
.To(c.Chase).When(x => x.Sensor.InChaseZone(), "InChaseZone")
|
||||
.To(c.Alert).When(x => HasAlert(x) && x.Sensor.InVisionZone(), "InVision+HasAlert");
|
||||
AddAbilityState(b, c.Patrol, c.PatrolAbilityId)
|
||||
.To(c.Chase).When(x => x.Sensor.InChaseZone(), "InChaseZone")
|
||||
.To(c.Alert).When(x => HasAlert(x) && x.Sensor.InVisionZone(), "InVision+HasAlert");
|
||||
|
||||
// 警觉(仅升级路径出现):进追逐区→追击;脱离视野→R;在视野内则保持(无转出)
|
||||
b.State(c.Alert)
|
||||
.OnEnter(x => c.OnAlertEnter?.Invoke(x))
|
||||
// 警觉(仅升级路径出现):进追逐区→追击;脱离视野→R;在视野内保持
|
||||
AddAbilityState(b, c.Alert, c.AlertAbilityId)
|
||||
.To(c.Chase).When(x => x.Sensor.InChaseZone(), "InChaseZone")
|
||||
.To(c.Rest).When(x => !x.Sensor.InVisionZone(), "leftVision");
|
||||
|
||||
// 追击:委托能力实现移动/伤害;脱离**全部感知**(追逐区 且 视野区都不在) → R(永不回警觉);退出时中断追击能力。
|
||||
// 维持用"追逐区 OR 视野区"任一:视野(los)可能被墙挡而漏检,仅靠视野会导致玩家在近身追逐区内却反复进出追击(抖动)。
|
||||
b.State(c.Chase)
|
||||
.OnEnter(x =>
|
||||
{
|
||||
c.OnChaseEnter?.Invoke(x);
|
||||
if (!string.IsNullOrEmpty(c.ChaseAbilityId)) x.Combat.UseAbility(c.ChaseAbilityId);
|
||||
})
|
||||
.OnExit(x => c.OnChaseExit?.Invoke(x))
|
||||
// 追击:脱离全部感知(追逐区 且 视野区都不在)→R(永不回警觉)。
|
||||
// 维持用"追逐区 OR 视野区"任一:视野(los)可能被墙挡漏检,仅靠视野会在近身追逐区内反复进出追击(抖动)。
|
||||
AddAbilityState(b, c.Chase, c.ChaseAbilityId)
|
||||
.To(c.Rest).When(x => !x.Sensor.InChaseZone() && !x.Sensor.InVisionZone(), "leftAllZones");
|
||||
|
||||
b.State(c.Death).OnEnter(x => c.OnDeathEnter?.Invoke(x));
|
||||
// 死亡:终态(无转出);死亡演出走物理状态机
|
||||
AddAbilityState(b, c.Death, c.DeathAbilityId);
|
||||
}
|
||||
|
||||
// 未发现态(Idle/Patrol)升级:在追逐区→追击(优先);否则 有警觉且在视野→警觉
|
||||
static void AddUndiscovered(BrainBuilder.StateBuilder sb, Config c)
|
||||
/// <summary>
|
||||
/// 一个"由能力驱动行为"的状态:进入即确保能力运行;每帧确保能力运行(被中断/受击后自动重触发);离开即中断。
|
||||
/// </summary>
|
||||
static BrainBuilder.StateBuilder AddAbilityState(BrainBuilder b, string state, string abilityId)
|
||||
{
|
||||
sb.To(c.Chase).When(x => x.Sensor.InChaseZone(), "InChaseZone")
|
||||
.To(c.Alert).When(x => HasAlert(x) && x.Sensor.InVisionZone(), "InVision+HasAlert");
|
||||
return b.State(state)
|
||||
.OnEnter(x => EnsureAbility(x, abilityId))
|
||||
.Tick(x => EnsureAbility(x, abilityId))
|
||||
.OnExit(x => x.Combat.InterruptAbilities());
|
||||
}
|
||||
|
||||
static void EnsureAbility(IAiContext x, string abilityId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(abilityId) && !x.Combat.IsAbilityRunning(abilityId))
|
||||
x.Combat.UseAbility(abilityId);
|
||||
}
|
||||
|
||||
static bool HasAlert(IAiContext x) => x is IEnemyActor a && a.HasAlertState;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 警觉行为能力:停下 + 朝向玩家 + Alert 阶段(播警觉动画/feedback)。持续朝向玩家直到被打断。
|
||||
/// "警觉态朝向角色"等行为归本能力,AI 只在进入警觉状态时触发。
|
||||
/// </summary>
|
||||
public class AlertAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("警觉动画(可选,留空则用 AnimConfig.Alert 阶段动画)")]
|
||||
[SerializeField] private ClipTransition _alertClip;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
_enemy.SetAiPhase(AiPhase.Alert); // 阶段动画 + 广播(供群体警戒等)
|
||||
if (_alertClip.Clip != null)
|
||||
_animancer.Play(_alertClip);
|
||||
|
||||
while (true)
|
||||
{
|
||||
_enemy.StopMovement(); // 保持静止
|
||||
if (_enemy.PlayerTransform != null)
|
||||
_enemy.FaceTarget(_enemy.PlayerTransform.position); // 朝向玩家
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c51a5a3300c5d2549b3f8826d8c54fa8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -23,6 +23,11 @@ namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
// 追击行为的速度/阶段声明归本能力(AI 只触发能力,不做这些执行)
|
||||
if (_enemy.Nav != null && _enemy.Stats != null)
|
||||
_enemy.Nav.SetSpeed(_enemy.Stats.RunSpeed);
|
||||
_enemy.SetAiPhase(AiPhase.Chase);
|
||||
|
||||
if (_loopClip.Clip != null)
|
||||
_animancer.Play(_loopClip);
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 待机行为能力:停下 + 声明 Idle 阶段(播 Idle/伪装动画)。保持到被 AI 切换状态时打断。
|
||||
/// 由 AI 决策层在进入待机状态时触发(AI 只决策,行为在此实现)。
|
||||
/// </summary>
|
||||
public class IdleAbility : EnemyAbilityBase
|
||||
{
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
_enemy.StopMovement();
|
||||
_enemy.SetAiPhase(AiPhase.Idle);
|
||||
while (true) // 保持待机(占用运行态)直到被打断
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599faa708ea4a0f4b96ca9a3691523d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 巡逻行为能力:巡逻速度 + Patrol 阶段(播 Walk 动画) + 经导航随机游走。保持到被打断。
|
||||
/// 移动执行归本能力,AI 只在进入巡逻状态时触发。
|
||||
/// </summary>
|
||||
public class PatrolAbility : EnemyAbilityBase
|
||||
{
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
_enemy.SetAiPhase(AiPhase.Patrol);
|
||||
var nav = _enemy.Nav;
|
||||
if (nav != null && _enemy.Stats != null)
|
||||
nav.SetSpeed(_enemy.Stats.WalkSpeed);
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (nav != null && !nav.IsMoving)
|
||||
nav.WalkToRandom(); // 到达后再选一个随机可达点,持续游走
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
_enemy.Nav?.StopNavigation(); // 退出巡逻时清除残留游走目标
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36ef5fe655e471c448e7c1300355888b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user