让 AI 写敌人状态机,并用脚本化场景验证状态转换正确:funplay-unity-mcp 实战

上一篇验证的是关卡静态上能不能通 ------纯几何,跑都不用跑。这篇走到运行时,问一个不一样的问题:敌人的行为对不对

需求一句话:

做一个敌人:平时在两点之间巡逻,发现玩家就追,玩家跑远了就放弃、返回巡逻。并且要能验证这套状态切换是对的。

"做一个会追人的敌人"不难,难的是后半句------怎么证明状态机没写错 。看它在编辑器里跑起来"感觉对"是不够的:状态机的 bug 往往藏在边界(玩家刚好在检测圈边缘反复横跳)、藏在某条少见的转换路径上。这篇延续前两篇的一贯做法:把"行为正确"变成机器能断言的东西

状态机本体

三个状态:Patrol(巡逻)、Chase(追击)、Return(返回)。转换规则:

  • Patrol:在两个航点间来回;玩家进入检测半径 → Chase
  • Chase:朝玩家移动;玩家超出丢失半径 → Return
  • Return:回最近的航点;到了 → Patrol;半路又撞见玩家 → Chase

一个关键细节:检测半径(5)< 丢失半径(8),故意不相等。这叫滞回(hysteresis)。如果只有一个半径,玩家站在圈边缘抖一下,敌人就会 Chase↔Patrol 疯狂横跳。两个半径拉开一段,进入和退出用不同阈值,状态就稳了。这是状态机的经典坑,值得专门处理。

csharp 复制代码
public class EnemyFSM : MonoBehaviour
{
    public enum State { Patrol, Chase, Return }
    public Transform player;
    public Transform[] waypoints;
    public float moveSpeed = 3f;
    public float detectRadius = 5f;   // 进圈 → 追
    public float loseRadius = 8f;     // 出圈 → 放弃(滞回,比 detect 大)

    public State Current { get; private set; } = State.Patrol;
    public List<string> Transitions = new();   // 记录每次切换,给验证用
    int _wp;

    void SetState(State s)
    {
        if (s == Current) return;
        Transitions.Add($"{Current}->{s}");    // 关键:把切换记下来
        Current = s;
    }

    void Update()
    {
        float dp = PlanarDist(transform.position, player.position);
        switch (Current)
        {
            case State.Patrol:
                MoveTo(waypoints[_wp].position);
                if (Arrived(waypoints[_wp])) _wp = (_wp + 1) % waypoints.Length;
                if (dp <= detectRadius) SetState(State.Chase);
                break;
            case State.Chase:
                MoveTo(player.position);
                if (dp >= loseRadius) SetState(State.Return);
                break;
            case State.Return:
                _wp = NearestWaypoint();
                MoveTo(waypoints[_wp].position);
                if (Arrived(waypoints[_wp])) SetState(State.Patrol);
                if (dp <= detectRadius) SetState(State.Chase);
                break;
        }
    }
}

注意 SetState 里每次切换都往 Transitions 里记一笔------这就是后面验证的抓手。

怎么验证它对:脚本化场景 + 序列比对

这是这篇的核心。验证不靠"看",靠一个脚本化的剧本:让玩家从远处靠近 → 停留 → 跑远 ,全程录下敌人实际发生的状态切换,最后和预期序列逐一比对。

预期很明确:

复制代码
Patrol -> Chase    (玩家靠近,进检测圈)
Chase  -> Return   (玩家跑远,出丢失圈)
Return -> Patrol   (敌人回到航点)

测试器脚本化这个剧本:

csharp 复制代码
public class EnemyFSMTest : MonoBehaviour
{
    public EnemyFSM enemy;
    public Transform player;
    Vector3 farPos = new(20, 0.5f, 0), nearPos = new(0, 0.5f, 4);
    float approachAt = 1.0f, retreatAt = 3.5f, endAt = 8.0f;
    public string verdict = "not run";

    static readonly string[] Expected =
        { "Patrol->Chase", "Chase->Return", "Return->Patrol" };

    public void RunTest()
    {
        enemy.Transitions.Clear();
        player.position = farPos;      // 开局玩家在远处
        _t0 = Time.time; _running = true;
    }

    void Update()
    {
        if (!_running) return;
        float t = Time.time - _t0;
        if (!_approached && t >= approachAt) { player.position = nearPos; _approached = true; }
        if (!_retreated && t >= retreatAt)   { player.position = farPos;  _retreated = true; }
        if (t >= endAt) { _running = false; verdict = Compare(enemy.Transitions, Expected); }
    }

    static string Compare(List<string> got, string[] exp)
    {
        if (got.Count != exp.Length)
            return $"FAIL --- 期望{exp.Length}次, 实际{got.Count}次: [{string.Join(", ", got)}]";
        for (int i = 0; i < exp.Length; i++)
            if (got[i] != exp[i]) return $"FAIL --- 第{i}次 期望{exp[i]} 实际{got[i]}";
        return $"PASS --- 状态序列正确: [{string.Join(", ", got)}]";
    }
}

剧本只是"在固定时刻把玩家瞬移到远/近",敌人的状态切换是它自己根据规则产生的------测试只负责制造情境核对结果

跑起来

PlayMode 里调 RunTest(),跑完读 verdict:

复制代码
verdict = PASS --- 状态序列正确: [Patrol->Chase, Chase->Return, Return->Patrol]

实际录到的切换序列和预期一字不差。

为了让状态肉眼可见,敌人按状态变色(巡逻=青、追击=红、返回=橙),地面画出检测圈(黄,半径 5)和丢失圈(红,半径 8)。

巡逻态------敌人青色,在两个航点间来回,玩家在圈外(画面外):

追击态------玩家(橙)进了黄色检测圈,敌人立刻转红、朝玩家移动:

整个过程:AI 写了状态机、写了带滞回的转换规则、写了一个脚本化场景把行为正确性变成可断言的序列比对,然后自己跑出 PASS。和前两篇是同一套方法论------别让"感觉对"蒙混过关,用客观信号断言

一脉相承的方法

回头看这个系列的验证思路,三篇是同一个模子:

  • 调跳跃手感:采样轨迹 → 断言峰值 / 上升下降比
  • 程序化关卡:用玩家物理 → 断言每个 gap 可达
  • 敌人状态机:脚本化情境 → 断言状态转换序列

共同点是:把"对不对"翻译成一个机器能跑、能比对的信号。AI 擅长这个------它能同时写出"被测的东西"和"验证它的东西",还能保证两者用同一套规则。手感、可达、行为,被测对象变了,方法没变。

诚实地讲:验证了什么,没验证什么

这套验证有明确边界:

  • 只测了一个理想剧本。玩家"直线靠近→停留→直线跑远"是我喂给它的情境。真实玩家的走位千奇百怪,要更有信心得喂更多剧本(贴边横跳、绕圈、瞬间反复进出------尤其要测滞回到底防没防住抖动)。
  • 没有视线遮挡(LOS)。现在是纯距离检测,隔着墙也能"看见"玩家。真实敌人 AI 要加 Raycast 视线判定,那又是一组新的转换和新的验证。
  • 移动是直线 MoveTowards,不绕障碍。真要能用得接导航网格(NavMesh)寻路。
  • 行为"对" ≠ AI "聪明 / 好玩"。状态序列正确只是不出 bug 的底线。敌人追得笨不笨、有没有威胁感、节奏好不好------那是设计和调校,验证器管不了。
  • 没测多敌人 / 性能。一个敌人的状态机是玩具规模;几十个敌人同时跑、还要寻路,是另一个量级的问题。

复杂的敌人 AI 最终会走向行为树(Behavior Tree)、GOAP、效用 AI 这些更强的结构。但有限状态机 + 可断言的转换测试,是把"敌人行为"从"跑起来看着像那么回事"变成"有客观验证"的第一步,也是 AI 最容易帮你快速立起来的那一层。

完整产物

下一篇换个方向:当一个 execute_code 的套路反复用,怎么把它沉淀成自己的 ToolProvider 正式工具 + 美术资源批处理------从"一次性脚本"走到"工程化工具"。

相关推荐
weixin_468466851 小时前
ResNet 残差网络新手入门与实战指南
人工智能·深度学习·ai·残差网络·resnet·机器视觉
jiayong231 小时前
harness 与 hermes-agent 扩展性、安全与运维
运维·人工智能·安全·ai·架构·智能体·harness
tealcwu1 小时前
【Unity实战】Unity IAP 5.3 中实现 Windows Custom Store 实战教程
windows·unity·游戏引擎
风_沙1 小时前
AI + SAP ADT实战案例(一):用 Codex 只读排查领料接口里的物料错位
人工智能·ai·sap·abap·erp
code_pgf1 小时前
DPO和PPO详解及区别
人工智能·机器学习
埃菲尔铁塔_CV算法1 小时前
基于扩张卷积与双分支参数调控的低光照图像增强算法完整研究与工程解析
人工智能·神经网络·算法·机器学习·计算机视觉
hai3152475431 小时前
有规则的AI编制操作系统演进过程展示
人工智能·程序人生·算法·逻辑回归·创业创新
老鱼说AI1 小时前
统计学习方法第七章:支持向量机精讲(超硬核长文深入预警!)
人工智能·深度学习·神经网络·算法·机器学习·支持向量机·学习方法