Unity3D用状态机制作角色控制系统

为了让读者对本文知识点有一个比较清晰的了解,我制作了一张结构图,如下图,图中以移动为例子简单的描述了状态机的基本结构,本文不对角色控制系统做全面的讲解,只对状态机的在角色控制系统中是如何运用做出讲解。



1.我们先从Actor讲起。Actor作为角色脚本的基类,承载着角色的基本属性,包括角色id,移动速度,坐标等等,因为我们这里讲的是用状态机来控制角色,所以角色的属性还包括角色的当前状态,所有状态,状态类型等等,还有一些对状态操作的方法,包括初始化状态机,初始化当前状态,改变状态机,更新状态机等等,当然还有一些角色表现的方法,比如移动,改变方向,播放动画等等,这些表现方法是通过状态机来实现,从而实现改变状态来驱动表现,这就是状态机的用法。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum Direction
{
    Front = 0,
    Back,
    Left,
    Right
}

public abstract class Actor : Base
{
    /// <summary>
    /// debug模式,程序测试
    /// </summary>
    public bool _debug;

    /// <summary>
    /// 玩家id
    /// </summary>
    public int _uid;

    /// <summary>
    /// 玩家名字
    /// </summary>
    public string _name;

    /// <summary>
    /// 移动速度
    /// </summary>
    public float _moveSpeed;

    /// <summary>
    /// 是否正在移动
    /// </summary>
    public bool _isMoving;

    /// <summary>
    /// 坐标
    /// </summary>
    public Vector3 _pos;

    /// <summary>
    /// 当前状态
    /// </summary>
    public ActorState _curState { set; get; }

    public ActorStateType _stateType;

    public Direction _direction = Direction.Front;

    /// <summary>
    /// 状态机集合
    /// </summary>
    public Dictionary<ActorStateType, ActorState> _actorStateDic = new Dictionary<ActorStateType, ActorState>();

    /// <summary>
    /// 动画控制器
    /// </summary>
    [HideInInspector]
    public Animator _animator;

    private Transform _transform;

    void Awake()
    {
        _transform = this.transform;
        _animator = GetComponent<Animator>();
        InitState();
        InitCurState();
    }

    /// <summary>
    /// 初始化状态机
    /// </summary>
    protected abstract void InitState();

    /// <summary>
    ///  初始化当前状态
    /// </summary>
    protected abstract void InitCurState();
   

    /// <summary>
    /// 改变状态机
    /// </summary>
    /// <param name="stateType"></param>
    /// <param name="param"></param>
    public void TransState(ActorStateType stateType)
    {
        if (_curState == null)
        {
            return;
        }
        if (_curState.StateType == stateType)
        {
            return;
        }
        else
        {
            ActorState _state;
            if (_actorStateDic.TryGetValue(stateType, out _state))
            {
                _curState.Exit();
                _curState = _state;
                _curState.Enter(this);
                _stateType = _curState.StateType;
            }
        }
    }

    /// <summary>
    /// 更新状态机
    /// </summary>
    public void UpdateState()
    {
        if (_curState != null)
        {
            _curState.Update();
        }
    }

    /// <summary>
    /// 移动 数据(状态)驱动表现
    /// </summary>
    public virtual void Move()
    {
        //TODO 移动相关状态
        _animator.SetInteger("Dir", (int)_direction);
        if (_debug)
        {
            //数据层位置
            _transform.position = _pos;
        }
        else
        {
            //表现层位置
            _transform.position = Vector3.Lerp(_transform.position, _pos, 100 * Time.deltaTime);
        }
    }

    /// <summary>
    /// 改变方向
    /// </summary>
    /// <param name="dir"></param>
    public void ChangeDir(Direction dir)
    {
        _direction = dir;
        if (_direction == Direction.Left)
        {
            _transform.localScale = new Vector3(-1, 1, 1);
        }
        else
        {
            _transform.localScale = new Vector3(1, 1, 1);
        }
    }

    /// <summary>
    /// 播放动画
    /// </summary>
    /// <param name="name"></param>
    /// <param name="dir"></param>
    public void PlayAnim(string name)
    {
        _animator.SetBool("Idle", false);
        _animator.SetBool("Run", false);
        _animator.SetBool(name, true);
        _animator.SetInteger("Dir", (int)_direction);
    }
}

2.我们现在有了Actor这个角色基类,那么我们现在就可以用它来创造很多的不同角色了。我们先来创造一个自己的角色PayerActor,然后继承Actor,因为Actor的状态机初始化是用虚方法的,所以我们必须在子类中去实现它,来达到不同的角色有不同的状态。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerActor : Actor {

    /// <summary>
    /// 摇杆
    /// </summary>
    private ETCJoystick _joystick;

    /// <summary>
    /// 初始化状态机
    /// </summary>
    protected override void InitState()
    {
        _actorStateDic[ActorStateType.Idle] = new IdleState();
        _actorStateDic[ActorStateType.Move] = new MoveState();
    }

    /// <summary>
    /// 初始化当前状态
    /// </summary>
    protected override void InitCurState()
    {
        _curState = _actorStateDic[ActorStateType.Idle];
        _curState.Enter(this);
    }

    void Start()
    {
        _joystick = GameObject.FindObjectOfType<ETCJoystick>();
        if (_joystick != null)
        {
            _joystick.onMoveStart.AddListener(StartMoveCallBack);
            _joystick.onMove.AddListener(MoveCallBack);
            _joystick.onMoveEnd.AddListener(EndMoveCallBack);
        }
    }

    /// <summary>
    /// 开始移动
    /// </summary>
    private void StartMoveCallBack()
    {
        TransState(ActorStateType.Move);
    }


    /// <summary>
    /// 正在移动
    /// </summary>
    /// <param name="arg0"></param>
    private void MoveCallBack(Vector2 vec2)
    {
        float value = 0.02f * _moveSpeed / Mathf.Sqrt(vec2.normalized.x * vec2.normalized.x + vec2.normalized.y * vec2.normalized.y);//勾股定理得出比例,第一个值是摇杆的比例

        _pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);

        int angle = (int)(Mathf.Atan2(vec2.normalized.y, vec2.normalized.x) * 180 / 3.14f);
        //Debug.Log(angle);
        if (angle > 45 && angle < 135)
        {
            ChangeDir(Direction.Back);
            //Debug.Log("上");
        }
        else if (angle <= 45 && angle >= -45)
        {
            ChangeDir(Direction.Right);
            //Debug.Log("右");
        }
        else if (Mathf.Abs(angle) >= 135)
        {
            ChangeDir(Direction.Left);
            //Debug.Log("左");
        }
        else
        {
            ChangeDir(Direction.Front);
            //Debug.Log("下");
        }
    }

    /// <summary>
    /// 移动结束
    /// </summary>
    private void EndMoveCallBack()
    {
        TransState(ActorStateType.Idle);
    }

    void OnDestroy()
    {
        if (_joystick != null)
        {
            _joystick.onMoveStart.RemoveListener(StartMoveCallBack);
            _joystick.onMove.RemoveListener(MoveCallBack);
            _joystick.onMoveEnd.RemoveListener(EndMoveCallBack);
        }
    }
    
}
这里有个可以优化的地方就是动画控制器,由于我这里做的是一个2D的角色,并且他有4个朝向,所以我改成了用混合树来做,通过MoveCallBack方法传进来的二维坐标直接控制混合树的X和Y的参数,进而改变角色的朝向,所以MoveCallBack方法里面的实现,如果你们需要可以进行优化,就是不需要通过角度去算方向了,我就不去修改了。

3.接下来我们来讲讲状态机的基类ActorState,基类包括状态机类型,进入状态,更新状态,退出状态等等。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 角色状态
/// </summary>
public abstract class ActorState
{

    /// <summary>
    /// 状态机类型
    /// </summary>
    public abstract ActorStateType StateType { get; }

    /// <summary>
    /// 进入状态
    /// </summary>
    /// <param name="param"></param>
    public abstract void Enter(params object[] param);

    /// <summary>
    /// 更新状态
    /// </summary>
    public abstract void Update();

    /// <summary>
    /// 退出状态
    /// </summary>
    public abstract void Exit();

}


/// <summary>
/// 角色状态类型
/// </summary>
public enum ActorStateType
{
    Idle,
    Move,
    //...
}

4.既然我们有了状态机的基类,那我们就可以创造出很多的状态了,比如待机,移动,攻击,释放技能等等。同样的,基类ActorState的方法也是虚方法,必须通过子类来实现,所以我们每个不同的状态就可以各自实现自己的操作了。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 待机状态
/// </summary>
public class IdleState : ActorState
{
    private Actor _actor;

    public override ActorStateType StateType
    { 
        get
        {
            return ActorStateType.Idle;
        }
    }

    public override void Enter(params object[] param)
    {
        //Debug.Log("IdleState Enter");
        _actor = param[0] as Actor;
        if (_actor != null)
        {
            _actor.PlayAnim("Idle");

            _actor._isMoving = false;
            //TODO 播放动画相关
        }
    }

    public override void Update()
    {

    }

    public override void Exit()
    {
        _actor = null;
        //Debug.Log("IdleState Exit");

    }
}

/// <summary>
/// 移动状态
/// </summary>
public class MoveState : ActorState
{
    private Actor _actor;

    public override ActorStateType StateType
    {
        get
        {
            return ActorStateType.Move;
        }
    }

    public override void Enter(params object[] param)
    {
        //Debug.Log("MoveState Enter");
        _actor = param[0] as Actor;
        if (_actor != null)
        {
            _actor.PlayAnim("Run");

            _actor._isMoving = true;
            //TODO 播放动画相关
        }
    }

    public override void Update()
    {
        if (_actor != null)
        {
            _actor.Move();
        }
    }

    public override void Exit()
    {
        //Debug.Log("MoveState Exit");
        _actor._isMoving = false;
        _actor = null;
    }
}

那我们是如何实现切换状态的呢?我们回到Actor,看看改变状态机的方法,每次切换的时候都会先把当前状态停掉,然后进入新的状态,再把自己的Actor传进状态机,然后根据状态的需要,实现Actor里的方法。

    /// <summary>
    /// 改变状态机
    /// </summary>
    /// <param name="stateType"></param>
    /// <param name="param"></param>
    public void TransState(ActorStateType stateType)
    {
        if (_curState == null)
        {
            return;
        }
        if (_curState.StateType == stateType)
        {
            return;
        }
        else
        {
            ActorState _state;
            if (_actorStateDic.TryGetValue(stateType, out _state))
            {
                _curState.Exit();
                _curState = _state;
                _curState.Enter(this);
                _stateType = _curState.StateType;
            }
        }
    }

5.最后我们来讲讲简单的角色管理系统ActorManager,包括角色的创建,删除,获取等等。其中最重要的功能就是更新所有角色状态机UpdateActor(),所有角色的持续状态都是通过这个方法实现的。

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-14
// Desc: 角色管理器
// **********************************************************************

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ActorManager : Singleton<ActorManager>
{

    /// <summary>
    /// 所有玩家的角色列表
    /// </summary>
    public Dictionary<int, Actor> _actorDic = new Dictionary<int, Actor>();

    // Update is called once per frame
    void Update()
    {
        UpdateActor();
    }

    /// <summary>
    /// 更新角色状态
    /// </summary>
    private void UpdateActor()
    {
        var enumerator = _actorDic.GetEnumerator();
        while (enumerator.MoveNext())
        {
            enumerator.Current.Value.UpdateState();
        }
        enumerator.Dispose();
    }

    /// <summary>
    /// 创建角色
    /// </summary>
    /// <param name="uid">角色id</param>
    public void CreateActor(int uid)
    {
        Actor actor = null;
        if (!_actorDic.TryGetValue(uid, out actor))
        {
            GameObject go = AppFacade.Instance.GetManager<ResourceManager>(ManagerName.Resource).CreateAsset("Prefabs/Actor/Wizard");
            Camera.main.GetComponentInChildren<Cinemachine.CinemachineVirtualCamera>().Follow = go.transform;
            actor = go.GetComponent<WizardActor>();
            actor._uid = uid;
            actor._name = uid.ToString();
            actor._moveSpeed = 5;
            _actorDic[uid] = actor;
        }
        else
        {
            Debug.Log("玩家" + uid + "已经存在");
        }
    }

    /// <summary>
    /// 删除角色
    /// </summary>
    /// <param name="uid">角色id</param>
    public void RemoveActor(int uid)
    {
        Actor actor = null;
        if (_actorDic.TryGetValue(uid, out actor))
        {
            Destroy(actor.gameObject);
            _actorDic.Remove(uid);
        }
        else
        {
            Debug.Log("玩家" + uid + "不存在");
        }
    }

    /// <summary>
    /// 获取角色
    /// </summary>
    /// <param name="uid">角色id</param>
    /// <returns></returns>
    public Actor GetActor(int uid)
    {
        Actor actor = null;
        _actorDic.TryGetValue(uid, out actor);
        return actor;
    }
}

后面有时间的话,我会基于这篇文章再写一篇简单帧同步的文章。

猜你喜欢

转载自blog.csdn.net/yye4520/article/details/80431380