Violet_Wdream

Personal Website

生如不定流水, 夕拾难觅朝花


Unity 2D chatper 1-- State Machine

目录

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili

1.状态机StateMachine

alt text

  • Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public PlayerStateMachine stateMachine { get; private set; }

    public PlayerIdleState idleState { get; private set; }
    public PlayerMoveState moveState { get; private set; }

    private void Awake()
    {
        //初始化状态机,创建状态
        stateMachine = new PlayerStateMachine();
        idleState = new PlayerIdleState(this, stateMachine,"Idle");
        moveState = new PlayerMoveState(this, stateMachine, "Move");

    }
    private void Start()
    {
        //初始化状态机,设置初始状态
        stateMachine.Initialize(idleState);
    }
    private void Update()
    {
        //更新状态机
        stateMachine.Update();
    }
}

  • PlayerStateMachine.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStateMachine 
{
    public PlayerState currentState { get; private set; } 
    //public get , private set ==> read only property
    public void Initialize(PlayerState _startState)
    {
        currentState = _startState;
        currentState.Enter();
    }
    public void ChangeState(PlayerState _newState)
    {
        currentState?.Exit();  // 如果 currentState 为 null,不会调用 Exit()
        currentState = _newState;
        currentState.Enter();
    }
    public void Update()
    {
        //通过当前状态对应的 Update() 方法来实现状态的切换   
        currentState?.Update();
    }
}

  • PlayerState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerState   //不继承MonoBehaviour,通过Update()方法来实现状态机
{
    protected Player player;
    protected PlayerStateMachine stateMachine;
    private string animBoolName;

    //构造函数
    public PlayerState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName)
    {
        this.player = _player;
        this.stateMachine = _stateMachine;
        this.animBoolName = _animBoolName;
    }
    public virtual void Enter()
    {   
        Debug.Log("Enter " + animBoolName);

    }
    public virtual void Update() 
    {
       Debug.Log("Update " + animBoolName);

    }
    public virtual void Exit()
    {
        Debug.Log("Exit " + animBoolName);
    }       
}
  • PlayerIdleState PlayerMoveState 继承PlayerState,采用快速操作生成构造以及重写 alt text

alt text

alt text

  • PlayerIdleState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerIdleState : PlayerState
{
    public PlayerIdleState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        if (Input.GetKeyDown(KeyCode.N))
        {
            stateMachine.ChangeState(player.moveState);
        }
    }
}

  • PlayerMoveState.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMoveState : PlayerState
{
    public PlayerMoveState(Player _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
    {//注意参数顺序,_player在前,_stateMachine在后,_animBoolName在最后

    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Exit()
    {
        base.Exit();
    }

    public override void Update()
    {
        base.Update();
        if(Input.GetKeyDown(KeyCode.N))
        {
            stateMachine.ChangeState(player.idleState);
        }
    }
}

在 C# 中,如果子类的构造函数没有显式地执行任何操作,它依然可以调用基类的构造函数。你在 PlayerMoveState 中的构造函数通过 : base(.) 语法调用了基类的构造函数,并传递了所需的参数。基类的构造函数负责初始化这个对象,因此你在子类构造函数中可以选择不写任何额外的代码。

在 C# 中,子类的实例可以被视为父类的实例,这就是所谓的 “向上转型”(Upcasting)。

当子类的实例作为父类传递并调用方法时,实际执行的是子类的实现,只要父类的方法是虚方法(virtual)并且在子类中被重写(override)。这体现了面向对象编程中的多态性。没被重写就用父类的。

2.动画Animation

  • 创建动画:拖动对应动画素材到Animation加工处

  • 设置Transition参数:参数名需要与初始化时设置的名称相同

    如:

     idleState = new PlayerIdleState(this, stateMachine,"Idle");
    //设置参数Idle
     moveState = new PlayerMoveState(this, stateMachine, "Move");
    //设置参数Move
    
  • 设置Transition参数对应Bool值,比如true进入,false退出,在Animator -> make transition中也设置。

     //PlayerState.cs
    public virtual void Enter()
     {   
         player.anim.SetBool(animBoolName, true);
     }
    public virtual void Exit()
    {
        player.anim.SetBool(animBoolName, false);
      
    }
    
  • Player.cs中设置动画组件,Start中获取组件

    #region Component
    public Animator anim { get; private set; }
      
    #endregion
        private void Start()
    {
        anim = GetComponentInChildren<Animator>();
     	...
    }
    
  • PlayerState.cs中设置xInput检测键盘输入

    protected float xInput;
     public virtual void Update() 
     {
        //Debug.Log("Update " + animBoolName);
        xInput = Input.GetAxisRaw("Horizontal");
      
     }
    
  • 在对应状态中设置update检测

    //PlayerIdleState.cs
    public override void Update()
    {
        base.Update();
        //检测移动
        if(xInput!= 0)
        {
            stateMachine.ChangeState(player.moveState);
        }
          
    }
    //PlayerMoveState.cs
    public override void Update()
    {
        base.Update();
        //检测移动
        if(xInput== 0)
        {
            stateMachine.ChangeState(player.idleState);
        }
    }
    

3.实现移动

  • 添加组件

    //Player.cs
    #region Component
    public Animator anim { get; private set; }
    public Rigidbody2D rb { get; private set; }
    #endregion
    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
     	...
    }
    
  • 添加公共方法,修改速度

    //Player.cs
    [Header("Move info")]
    [SerializeField] public float moveSpeed ;
    public void SetVelocity(float x, float y)
    {
        rb.velocity = new Vector2(x, y);
    }
    
  • 在PlayerState中添加rb,anim简化代码量:

    //PlayerState.cs
    protected Rigidbody2D rb;
    protected Animator anim;
    public virtual void Enter()
    {   
        rb = player.rb;
        anim = player.anim;
        ...
    }
    
  • 在移动状态中对接

    //PlayMoveState.cs
    public override void Update()
    {
        base.Update();
        if (xInput == 0)
        {
            stateMachine.ChangeState(player.idleState);
        }
        player.SetVelocity(xInput * moveSpeed, rb.velocity.y);
    }
    

4.跳跃/下降

  • 创建一个PlayerGroundedtate继承PlayerState, 让Player Move/Idle State继承PlayerGroundedState

    public class PlayerGroundedState : PlayerState
        //生成构造和重写
    public class PlayerIdleState : PlayerGroundedState
    public class PlayerMoveState : PlayerGroundedState
    
  • 创建动画

  • Animator中设置一棵Blend Tree - JumpFall

  • 将混合数自动生成的参数Blend改为yVelocity , 设置参数Jump , make transtion

  • 双击JumpFall 进入, 去掉勾选Automate Thresholds, 添加Motion Field : playerJump, playerFall

alt text

  • 创建PlayerAirState (fall) PlayerJumpState(jump)继承PlayerState, ,自动构造 重写

  • 在Player中声明

    //Player.cs
    public PlayerJumpState jumpState { get; private set; }
    public PlayerAirState airState { get; private set; }
    private void Awake()
    {
        jumpState = new PlayerJumpState(this, stateMachine, "Jump");
        airState  = new PlayerAirState(this, stateMachine, "Jump");//jump
      
    	...
    }
    
  • 通过检测y速度转换状态

    //PlayerState.cs
    public virtual void Update() 
    {
        anim.SetFloat("yVelocity", rb.velocity.y);
        ...
    }
    
  • 实现跳跃

    //PlayerJumpState.cs
    public override void Enter()
     {
         base.Enter();
         rb.velocity = new Vector2(rb.velocity.x, player.jumpForce);
     }
     public override void Update()
     {
         base.Update();
         //检测移动
         if(rb.velocity.y < 0)
         {
             stateMachine.ChangeState(player.airState);
         }
     }
      
    //PlayerAirState.cs
     public override void Update()
     {
         base.Update();
         if ( player.isGrounded)
         {
             stateMachine.ChangeState(player.idleState);
         }
     }
    
  • 地面检测,将平台/地面的标签色Tag 设置为Ground,并添加碰撞箱,刚体,composite,组件

    //Player.cs
    public bool isGrounded { get; set; } = false;
      
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }
    }
    private void OnCollisionExit2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = false;
        }
    }
    

5.翻转Flip

//Player.cs
public int facingDirection { get; set; } = 1; //默认右边1 ,左边-1
public void SetVelocity(float x, float y)
{
    rb.velocity = new Vector2(x, y);
    FlipController(x); //设置速度的时候就需要判断是否改变朝向。
}
public void Flip()
{
    transform.Rotate(0, 180, 0);
    facingDirection *= -1;

}
public void FlipController(float xInput)
{
    if (facingDirection * xInput < 0)//如果按右边,角色此时也朝右边,方向就不变;如果此时角色朝左,就需要翻转。就是说按键方向和人物朝向相反时调用Flip
    {
        Flip();
    }
}

//PlayerState.cs 
public virtual void Update() 
 {
     player.SetVelocity(xInput * player.moveSpeed, rb.velocity.y);
	...
 }

Comments