用状态机做人物(AI)的状态切换以及动画效果——人物动画以及切换

github: https://github.com/lucaschen1993/PRG_DEMO

前言

在之前写状态机文章的时候介绍过,基于事件与委托实现的状态机。这篇文章将介绍状态机的使用,以及人物动画的实现,以及状态的切换。

角色 Animator Controller

我自己找了一个资源包是带动画的,但是要自己添加一个Animator Contoller,然后自己给这个动画控制器添加动画,这里我已经做好了,,废话不多说,直接上图。
在这里插入图片描述
这里的Attack状态我使用了子状态机,因为攻击动画不止一个,我又不想整个Base Layer看起来太凌乱,就使用了子状态机(Sub_stateMachine)。
在这里插入图片描述
另外的Move我使用了混合树,为了通过计算EasyTouch的摇杆的轴值来控制动画播放走还是跑。
在这里插入图片描述

这里有5个参数,MoveFactor是控制就是上面说的,控制角色播放跑还是走的动画,因为使用混合树,可以混合好walk与run的动画。Death是控制死亡动画播放的Trigger,AttackFactor是控制播放哪个攻击动画,ToIdle是回到Idle动画。Jump暂时没有做,但是方法也是一样的,先把Move Death,Attack这些比较重要的先做出来吧。

角色的状态控制以及动画控制

这里开始介绍控制角色的代码,先贴一些Player.cs的参数

	public delegate void PlayerStateChange(AllCharacter.StateType stateType);
    public event PlayerStateChange StateChange;
    public int PlayerHp;
    public float AtkCoolDown;
    private float attackCoolDownTime;
    private AllCharacter.StateType playerType;
    public FiniteStateMachine FsMachine;
    public ETCJoystick MoveJoystick;
    private Animator _anim;
    private NavMeshAgent _nav;

delegate与event是角色状态切换的事件与委托;AtkCoolDown与attackCoolDownTime用于制作攻击间隔;MoveJoystick是为了获取EasyTouch的摇杆,为了得到摇杆轴值。
在初始化Start的时候设置好角色的初始值,

    void Start()
    {
        //设置角色的初始状态
        PlayerHp = 100;
        _anim = GetComponentInChildren<Animator>();
        _nav = GetComponent<NavMeshAgent>();
        EnterStand();
        playerType = FsMachine.CurBaseState.GetStateType();
        attackCoolDownTime = 0;
        AtkCoolDown = 2.5f;
    }

接着在Update里面是状态控制,因为有些参数是要在某些状态内才计算的,所以我在这里使用了switch做状态判断。但是不控制状态的切换。例如攻击冷却间隔只在攻击状态下才会进行计算。由于角色挂了,就啥都不能做了,所以我就把角色的死亡放在switch外。(不过突然觉得这个不是很好,得找个时间修改一下)

    void Update ()
    {
        FsMachine.OnUpdate();
        if (IsPlayerDead())
        {
            StateChange(StateType.STATE_DEAD);
        }
        switch (playerType)
        {
            case StateType.STATE_ATTACK:
                attackCoolDownTime += Time.deltaTime;
                break;
            case StateType.STATE_STAND:
                break;
            case StateType.STATE_DEAD:
                break;
            case StateType.STATE_MOVE:
                break;
        }        
	}

这里使用的EasyTouch按钮通过ETCHolder.cs控制,我就不详细讲了,直接看代码就行。
ETCHolder.cs

using System.Collections;
using System.Collections.Generic;
using HedgehogTeam.EasyTouch;
using UnityEngine;

public class ETCHolder : MonoBehaviour
{
    private Player _player;
	// Use this for initialization
	void Start ()
	{
	    _player = GameObject.Find("Player").GetComponent<Player>();
	}
	
	// Update is called once per frame
	void Update () {
		
	}

    public void OnMoveStart()
    {
        _player.StickMove();
    }

    public void OnTouchUp()
    {
    	//松开JoyStick,回到Stand状态,Idle
        _player.EnterStand();
    }

    public void OnAttackDown()
    {
       _player.DownAttack();
    }
}

在这里是直接调用Player里面的方法进行状态切换的,而状态的切换都是通过事件StateChange进行切换的

    public void StickMove()
    {
        //进入移动状态
        StateChange(AllCharacter.StateType.STATE_MOVE);
    }

    public void DownAttack()
    {
        //进入攻击状态
        StateChange(AllCharacter.StateType.STATE_ATTACK);
    }

    public void EnterStand()
    {
    	//进入Stand状态,Idle
        StateChange(AllCharacter.StateType.STATE_STAND);
    }

接下讲一下获取Joystick的轴值以及用该轴值控制动画的播放。先贴一部分代码

    //获取ETCJoystick轴值
    public Vector3 GetJoystickAxis()
    {
        float h = MoveJoystick.axisX.axisValue;
        float v = MoveJoystick.axisY.axisValue;

        return new Vector3(h,0,v);
    }

    public void SetMoveFactor(float factor)
    {
        _anim.SetFloat("MoveFactor",factor);
    }

这里直接通过Joystick的axisX.axisValue、axisY.axisValue就可以获取轴值了,返回的是一个Vector3的向量,接着通过这个返回值来计算其长度再赋值给moveFactor。
MoveState.cs

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

public class MoveState : BaseState
{
    private readonly Player _player;
    public MoveState(Player player)
    {
        this._player = player;
    }
    public override AllCharacter.StateType GetStateType()
    {
        return AllCharacter.StateType.STATE_MOVE;
    }
    public override void EnterState(FiniteStateMachine fsMachine, BaseState preState)
    {
        if (preState != null)
        {
            //Debug.Log("Enter the MoveState the preState is:" + preState.GetStateType());
        }
        else
        {
            //Debug.Log("Enter the MoveState");
        }
        _player.SetType(AllCharacter.StateType.STATE_MOVE);
        //Debug.Log(_player.CurBaseState);
    }

    public override void UpdateState()
    {
        Vector3 v3 = _player.GetJoystickAxis();
        float moveFactor = Vector3.Magnitude(v3);
        _player.SetMoveFactor(moveFactor);
        //Debug.Log("I'm Moving.");
    }

    public override void ExitState(BaseState preState)
    {
        _player.SetMoveFactor(0f);
        //Debug.Log("Exit Move State. " + preState.GetStateType());
    }

    public override void InputHandle()
    {

    }
}

这样就可以通过轴值来控制动画的播放了。当摇杆偏移量小的时候是播放walk动画,当偏移量拉满的时候是播放run动画,当该偏移量在这之间,则是播放run与walk的混合动画。
接着是Attack的状态,以及攻击间隔的制作。
我们在Player的update中,已经计算了attackCoolDownTime,每进行一次攻击,就要重置一次攻击冷却时间;在计算攻击冷却时间的时候需要判断,攻击间隔是否大于默认的攻击冷却时间AtkCoolDown,接着能攻击了,就调用Attack。

    //随机攻击动画
    #region RandomAttack_animation

    public void ResetAttackCoolDown()
    {
        attackCoolDownTime = 0;
    }

    public bool CanAttack()
    {
        if (attackCoolDownTime > AtkCoolDown)
        {
            ResetAttackCoolDown();
            return true;
        }
        return false;

    }

    public void Attack()
    {
        StartCoroutine(AttarCoroutine());
    }
    private IEnumerator AttarCoroutine()
    {
        int atkFactor = Random.Range(1, 3);
        _anim.SetInteger("AttackFactor", atkFactor);
        yield return null;
        _anim.SetInteger("AttackFactor", 0);
    }

    public void StopAttack()
    {
        _anim.SetInteger("AttackFactor", 0);
    }

AttackState.cs

using System.Collections;
using System.Collections.Generic;
using BehaviorDesigner.Runtime.Tasks.Basic.Math;
using UnityEngine;

public class AttackState : BaseState
{
    private readonly Player _player;
    public AttackState(Player player)
    {
        //向状态机发送事件,请求转换状态
        this._player = player;
    }
    public override AllCharacter.StateType GetStateType()
    {
        return AllCharacter.StateType.STATE_ATTACK;
    }
    public override void EnterState(FiniteStateMachine fsMachine, BaseState preState)
    {
        if (preState != null)
        {
            Debug.Log("Enter the AttackState the preState is:" + preState.GetStateType());
        }
        else
        {
            Debug.Log("Enter the AttackState");
        }
        _player.SetType(AllCharacter.StateType.STATE_ATTACK);
        _player.ResetAttackCoolDown();
        _player.Attack();
        //Debug.Log(_player.CurBaseState);
    }

    public override void UpdateState()
    {
        //战斗状态不间断平A
        //Debug.Log("I'm in Battle Mode .");
        bool canAttack = _player.CanAttack();
        //攻击间隔
        if (canAttack)
        {
            _player.Attack();
        }
    }

    public override void ExitState(BaseState preState)
    {
        _player.StopAttack();
        _player.ResetAttackCoolDown();
        Debug.Log("Exit Attack State. " + preState.GetStateType());
    }

    public override void InputHandle()
    {
        
    }
}

在进入攻击状态时要先执行一次攻击,不然就要等默认攻击冷却过后才能进行一次攻击,每次退出状态的时候攻击冷却时间清零。防止下一次攻击的时候该时间混乱。
这样,人物的状态切换动画制作就已经做完啦,给个成品gif。
在这里插入图片描述

总结

其实这里人物的动画以及状态切换是比较简单的,因为逻辑并不复杂。看上面的代码其实很清晰就清楚,哪个状态在干啥,通过哪个按键切换到哪个状态。下一篇将介绍一下用状态机制作的游戏AI。

ps:文章没办法面面俱到,所以有啥问题尽管私信我,我会一一回答。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值