[Unity] 摆脱Mecanim Animation里的“毛线团”:使用Enum来管理人物状态和动画系统

使用Enum来管理人物状态的好处有:

  1. 完美替代 Unity Mecanim 的状态转换问题。避免Animator看起来像一团纠结在一起的毛线团
  2. 利用 [Flags] 的 Attribute可以方便的解决同时有两个或以上状态的问题
  3. 利用当前的任务状态Property来改变动画,实现动画渲染和实际控制代码的解耦
  4. TODO: 当有多层Layer来表示动画的时候,可以创建多种状态Enum;许多常见的机制还没有写进去,如二段跳、coyote jump等

图1: MegaMan示例和Animator的简单结构,例子来源:这里


这是一个不难解决却困扰了我很久的问题。直到最近看《游戏编程模式》这本书里阐述了类似的情景。附上自己写的还算比较满意的代码,输入上也实现了更新状态和输入分离。Bitwise基本操作的参考说明见Alan Zucconi

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

public class PlayerContrllerEnum : MonoBehaviour
{
    [SerializeField] private float horizontalSpeed = 10f;
    public float HorizontalSpeed { get { return horizontalSpeed; } private set { horizontalSpeed = value; } }
    [SerializeField] [Range(0, 10f)] private float jumpForce = 8f;
    public float JumpForce { get { return jumpForce; } private set { jumpForce = value; } }
    [SerializeField] private float attackGap = 0.2f;
    public float AttackGap { get { return attackGap; } private set { attackGap = value; } }

    [SerializeField] private float horizontalInput = 0f;
    public float HorizontalInput { get { return horizontalInput; } private set { horizontalInput = value; } }
    [SerializeField] private bool isJumpPressed = false;
    public bool IsJumpPressed { get { return isJumpPressed; } private set { isJumpPressed = value; } }
    [SerializeField] bool isOnGround = true;
    public bool IsOnGround { get { return isOnGround; } private set { isOnGround = value; } }
    [SerializeField] private bool isShootPressed = false;
    public bool IsShootPressed { get { return isShootPressed; } private set { isShootPressed = value; } }
    [SerializeField] private bool isAbleToShoot = true;
    [SerializeField] private float shootGapCountDown;
    public int GroundMask { get; set; } = 0;

    [SerializeField] private Rigidbody2D myRigidBody;

    [SerializeField] Animator animator;
    private PlayerInput _playerInput { get; set; }
    private string MovementActionsName { get; } = "Movement";
    private string JumpActionName { get; } = "Jump";
    private string ShootActionName { get; } = "Shoot";

    #region Animation Names
    private string PlayerIdlingAnimString { get; } = "Player_idle";
    private string PlayerMovingAnimString { get; } = "Player_run";
    private string PlayerJumpingAnimString { get; } = "Player_jump";
    private string PlayerAttackAnimString { get; } = "Player_attack";
    private string PlayerAirAttackAnimString { get; } = "Player_air_attack";
    #endregion

    [Flags]
    public enum PlayerStatus 
    {
        None = 0,
        Idling = 1<<0,
        Moving = 1<<1,
        Jumping = 1<<2,
        Attacking = 1<<3,
    }

    private PlayerStatus currentStatus = PlayerStatus.Idling;
    public PlayerStatus CurrentStatus { get { return currentStatus; }
        set 
        {
            if(value != currentStatus && value!=PlayerStatus.None)
            {
                if(!isAbleToShoot && !value.HasFlag(PlayerStatus.Attacking)) return; // keep shooting anim when it's still in cool down
                if(value == PlayerStatus.Idling)
                {
                    ChangeAnimationStateTo(PlayerIdlingAnimString);
                }
                else if(value == PlayerStatus.Moving)
                {
                    ChangeAnimationStateTo(PlayerMovingAnimString);
                }
                else if(value== PlayerStatus.Attacking)
                {
                    ChangeAnimationStateTo(PlayerAttackAnimString);
                }
                else if(value == (PlayerStatus.Attacking | PlayerStatus.Jumping))
                {
                    ChangeAnimationStateTo(PlayerAirAttackAnimString);
                }
                else if(value== PlayerStatus.Jumping)
                {
                    ChangeAnimationStateTo(PlayerJumpingAnimString);
                }
                currentStatus = value;
            }
        }
    }

    void ChangeAnimationStateTo(string newAnimation)
    {
        animator.Play(newAnimation);
    }

    // Start is called before the first frame update
    void Start()
    {
        _playerInput = GetComponent<PlayerInput>();
        animator = GetComponent<Animator>();
        currentStatus = PlayerStatus.Idling;
        GroundMask = LayerMask.GetMask("Ground");
        myRigidBody = GetComponent<Rigidbody2D>();

    }

    // Update is called once per frame
    void Update()
    {
        HorizontalInput = _playerInput.actions[MovementActionsName].ReadValue<Vector2>().x;
        IsJumpPressed = _playerInput.actions[JumpActionName].triggered;
        IsShootPressed = _playerInput.actions[ShootActionName].triggered;

    }

    private void FixedUpdate()
    {
        CheckIfIsOnGround();
        FixedUpdateAttackGap();

        PlayerStatus statusInferred = PlayerStatus.Idling;
        if(Mathf.Abs(HorizontalInput) > 0.01)
        {
            Move(HorizontalInput*HorizontalSpeed);
            statusInferred = PlayerStatus.Moving;
        }
        if(!IsOnGround)
        {
            statusInferred = PlayerStatus.Jumping;
        }
        if(IsJumpPressed && IsOnGround)
        {
            IsJumpPressed = false;
            Jump();
            statusInferred = PlayerStatus.Jumping;
        }

        if(IsShootPressed && isAbleToShoot)
        {
            IsShootPressed = false;
            ShootABullet();
            statusInferred &= ~PlayerStatus.Idling;
            statusInferred &= ~PlayerStatus.Moving; 
            statusInferred |= PlayerStatus.Attacking;
        }
        CurrentStatus = statusInferred;
    }

    public void Move(float inputValue)
    {
        // method: set velocity directly

        Vector2 currentVelocity = myRigidBody.velocity;
        //flip if necessary
        Vector3 localScale = transform.localScale;
        if(Mathf.Sign(localScale.x) != Mathf.Sign(inputValue))
        {
            localScale.x = -localScale.x;
            transform.localScale = localScale;
        }
        currentVelocity.x = inputValue;
        myRigidBody.velocity = currentVelocity;
    }

    public void Jump()
    {
        myRigidBody.AddForce(new Vector2(0, JumpForce), ForceMode2D.Impulse);
    }

    public void ShootABullet()
    {
        isAbleToShoot = false;
        shootGapCountDown = AttackGap;
        //TODO
        Debug.Log("A bullet is shot");
    }

    private void FixedUpdateAttackGap()
    {
        if(!isAbleToShoot)
        {
            shootGapCountDown -= Time.fixedDeltaTime;
            if(shootGapCountDown <= 0)
                isAbleToShoot = true;
        }
    }

    private void CheckIfIsOnGround()
    {
        RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.down, 0.1f, GroundMask);
        if(hit.collider != null)
        {
            IsOnGround = true;
        }
        else
            IsOnGround = false;
    }
}
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注