You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity有限状态机:如何仅访问子类独有变量?

基于继承的敌人AI有限状态机:访问子类状态变量的解决方案

问题描述

开发游戏时采用基于继承的有限状态系统实现敌人与BOSS AI,遇到以下问题:

  • 基类EnemyState被多个状态子类继承(如AirPatrol)
  • AirPatrol拥有EnemyState没有的speed、patrolPositions等公共变量
  • 状态机类(EnemyController)中的currentState/nextState为EnemyState类型,直接引用currentState.speed会报“未包含定义”错误
  • 希望找到无需将子类特有变量添加到基类的解决方案

相关代码

SecurityDroneAI 类(问题出现位置)

public class SecurityDroneAI : EnemyController
{
    [Header("Movement")]
    public float patrolSpeed = 3.0f;
    public float aggroSpeed = 5.0f;
    [SerializeField] float aggroDistance = 20f;
    public float acceleration = 2f;

    [Header("Pathfinding")]
    public float pathUpdateDelay = 1.0f;
    public List<Vector2> patrolPositions = new List<Vector2>();
    public float nextWaypointDistance = 2;

    Animator anim;
    Rigidbody2D rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        anim = GetComponent<Animator>();

        SetNextState(new AirPatrol());
        nextState.speed = 1; // 此处报错:EnemyState未定义speed
    }

    private void Update()
    {
        anim.SetBool("seeking", currentState.GetType() == typeof(AirPatrol));
        float rot = -rb.velocity.x * 3;
    }
}

EnemyState 基类

public class EnemyState
{
    public Rigidbody2D rb;
    public Animator anim;
    public Transform transform;
    public EnemyController controller;

    public virtual void OnEnter(EnemyController parentController)
    {
        controller = parentController;
        rb = controller.GetComponent<Rigidbody2D>();
        anim = controller.GetComponentInChildren<Animator>();
        transform = controller.transform;
    }

    public virtual void OnUpdate()
    {
    }

    public virtual void OnFixedUpdate()
    {
    }

    public virtual void OnLateUpdate()
    {
    }

    public virtual void OnExit()
    {
    }
}

AirPatrol 子类

public class AirPatrol : EnemyState
{
    public List<Vector2> patrolPositions = new List<Vector2>(); // 巡逻点列表
    public float speed = 3.0f; // 移动速度
    public float acceleration = 2f; // 加速度

    public float pathUpdateDelay = 1.0f; // 路径更新间隔

    public float nextWaypointDistance = 1.5f; // 到达下一个路径点的判定距离

    int positionIndex; // 当前目标巡逻点索引
    int currentWaypoint = 0; // 当前路径点索引

    float timeSinceUpdatedPath; // 距离上次更新路径的时间

    Path path; // 当前路径
    public Seeker seeker; // A*寻路组件

    public override void OnEnter(EnemyController parentController)
    {
        base.OnEnter(parentController);

        positionIndex = 0;
        timeSinceUpdatedPath = 0;
    }

    public override void OnUpdate()
    {
        base.OnUpdate();

        if (timeSinceUpdatedPath <= 0) // 到达路径更新间隔时更新路径
        {
            UpdatePath();
            timeSinceUpdatedPath = pathUpdateDelay;
        }
        timeSinceUpdatedPath += Time.deltaTime;

        if (path == null) { return; }
        if (currentWaypoint >= path.vectorPath.Count) // 到达路径终点
        {
            positionIndex = positionIndex + 1 >= patrolPositions.Count ? 0 : positionIndex + 1; // 切换到下一个巡逻点
            UpdatePath();
            return;
        }

        MoveAlongPath();
    }

    private void MoveAlongPath()
    {
        Vector2 direction = (path.vectorPath[currentWaypoint] - transform.position).normalized;
        rb.velocity = Vector3.Lerp(rb.velocity, direction * speed, acceleration * Time.deltaTime);

        float distance = Vector2.Distance(rb.position, path.vectorPath[currentWaypoint]);
        if (distance < nextWaypointDistance)
        {
            currentWaypoint++;
        }
    }

    private void UpdatePath()
    {
        if (!seeker.IsDone()) { return; }
        seeker.StartPath(transform.position, patrolPositions[positionIndex], SetPath);
    }

    private void SetPath(Path p)
    {
        if (p.error) { return; }

        path = p;
        currentWaypoint = 0;
    }
}

EnemyController 状态机类

public class EnemyController : MonoBehaviour
{
    public EnemyState mainStateType;
    public EnemyState currentState;
    public EnemyState nextState;

    private void Update()
    {
        if (nextState != null)
        {
            SetState(nextState);
        }

        if (currentState != null) { currentState.OnUpdate(); }
    }

    private void LateUpdate()
    {
        if (currentState != null) { currentState.OnLateUpdate(); }
    }

    private void FixedUpdate()
    {
        if (currentState != null) { currentState.OnFixedUpdate(); }
    }

    private void SetState(EnemyState newState)
    {
        nextState = null;
        if (currentState != null)
        {
            currentState.OnExit();
        }
        currentState = newState;
        currentState.OnEnter(this);
    }

    public void SetNextState(EnemyState newState)
    {
        if (newState != null)
        {
            nextState = newState;
        }
    }
}

解决方案

1. 显式类型转换(直接转换或as/is关键字)

这是最直接的方案,在访问子类变量前,将EnemyState类型对象转换为对应子类类型,确保类型安全。

示例代码:

private void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();

    SetNextState(new AirPatrol());
    // 方法1:使用as关键字(安全,转换失败返回null)
    var patrolState = nextState as AirPatrol;
    if (patrolState != null)
    {
        patrolState.speed = 1;
    }

    // 方法2:强制转换(适合确定类型的场景,转换失败会抛出异常)
    // var patrolState = (AirPatrol)nextState;
    // patrolState.speed = 1;
}

在需要访问currentState的子类变量时,同样处理:

private void Update()
{
    var patrolState = currentState as AirPatrol;
    if (patrolState != null)
    {
        // 访问patrolState的speed、patrolPositions等变量
        Debug.Log("当前巡逻速度:" + patrolState.speed);
    }
}

2. 为特定状态定义接口

如果多个状态子类有相似的变量或行为,可以定义接口规范,让子类实现接口,外部通过接口访问变量,提升代码扩展性。

步骤:

  1. 定义巡逻状态接口:
public interface IPatrolState
{
    float speed { get; set; }
    List<Vector2> patrolPositions { get; set; }
}
  1. 让AirPatrol实现该接口:
public class AirPatrol : EnemyState, IPatrolState
{
    // 原有变量和方法保持不变
}
  1. 外部通过接口访问:
private void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();

    SetNextState(new AirPatrol());
    var patrolState = nextState as IPatrolState;
    if (patrolState != null)
    {
        patrolState.speed = 1;
    }
}

3. 状态初始化时传递参数

不在外部直接修改状态变量,而是在创建状态对象时,通过构造函数或初始化方法将控制器中的配置参数传递给状态,避免类型转换。

方法A:构造函数传参

修改AirPatrol添加构造函数:

public class AirPatrol : EnemyState
{
    public AirPatrol(float speed, List<Vector2> patrolPositions)
    {
        this.speed = speed;
        this.patrolPositions = patrolPositions;
    }

    // 原有变量和方法保持不变
}

在SecurityDroneAI中调用:

private void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();

    // 直接传递配置参数
    SetNextState(new AirPatrol(1f, patrolPositions));
}

方法B:初始化方法传参

如果需要无参构造函数,可添加初始化方法:

public class AirPatrol : EnemyState
{
    public void Initialize(float speed, List<Vector2> patrolPositions)
    {
        this.speed = speed;
        this.patrolPositions = patrolPositions;
    }

    // 原有变量和方法保持不变
}

调用时:

private void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();

    var patrolState = new AirPatrol();
    patrolState.Initialize(1f, patrolPositions);
    SetNextState(patrolState);
}

4. 状态从控制器获取配置

将状态需要的配置变量放在控制器(SecurityDroneAI)中,状态在OnEnter方法里自动从控制器获取这些值,无需外部手动设置变量,让状态与控制器解耦。

修改AirPatrol的OnEnter方法:

public override void OnEnter(EnemyController parentController)
{
    base.OnEnter(parentController);
    // 将父控制器转换为SecurityDroneAI类型
    var droneController = parentController as SecurityDroneAI;
    if (droneController != null)
    {
        speed = droneController.patrolSpeed;
        patrolPositions = droneController.patrolPositions;
        acceleration = droneController.acceleration;
        pathUpdateDelay = droneController.pathUpdateDelay;
        nextWaypointDistance = droneController.nextWaypointDistance;
    }

    positionIndex = 0;
    timeSinceUpdatedPath = 0;
}

此时SecurityDroneAI的Start方法只需创建状态:

private void Start()
{
    rb = GetComponent<Rigidbody2D>();
    anim = GetComponent<Animator>();

    SetNextState(new AirPatrol());
}

内容的提问来源于stack exchange,提问作者Glass Shard Games

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 17:39:51