[Unity]使用状态机模式创建平台控制游戏(以Unity酱为例)

学习目标:

之前我们已经写过非常多的脚本控制玩家类,动画器复杂的连线和众多的判断条件bool类型之类 的让我们应接不暇,所以我们不妨使用unity常用的状态机脚本模式来编写代码,今天就从b站看到了一部状态机脚本教程,就试着跟着老师一起写一下。

[Unity] 平台游戏控制器 教程 Ep.00 教程简介_哔哩哔哩_bilibili

今天就把学到的状态机知识全部吐出来吧。 

【项目资源包下载】(在老师的视频简介课看到)

- 百度网盘 - https://pan.baidu.com/s/1Xk6VwiKh2Jzun8HVCt97mg?pwd=j9aa 提取码:j9aa

- Google Drive - https://drive.google.com/file/d/1VAwWqFeKIiu-gpwJdfUsDnijJ-fFu_HF/view?usp=sharing

  


学习内容:

  首先我们先创建一个3D项目,把刚刚下载好的素材拖进来

  我们需要把没用的插件给删除掉,然后再下载几个插件,这些都是UNity自带的,例如cinemaChine,Poss-Poccessing,Input systems,最后在in Project中就这些

点开老师创建好的场景,会发现有地图,人物,和一些粒子系统

 

还有创建好的后处理系统

 

然而这些都不是我们这篇文章要讲的重点,最主要是我也不太会这些专业名字。

然后我们就开始编写脚本吧。 


代码部分:

  按这样管理好脚本的文件夹

然后我们先创建状态机系统吧,先在Base文件夹下创建一个IState接口来给玩家状态机继承。

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

public interface IState 
{
    void Enter();
    void Exit();
    void LogicUpdate();
    void PhysicsUpdate();
}

 我们还需要一个PlayerController控制玩家移动的

using UnityEngine;

public class PlayerController : MonoBehaviour
{

    Rigidbody rigibody;


    private void Awake()
    {

        rigibody = GetComponent<Rigidbody>();
    }

    private void Start()
    {
    }

    

    public void SetVelocity(Vector3 velocityY)
    {
        rigibody.velocity = velocityY;
    }

    public void SetVelocityX(float velocityX)
    {
        rigibody.velocity = new Vector3(velocityX, rigibody.velocity.y);
    }

    public void SetVelocityY(float velocityY)
    {
        rigibody.velocity = new Vector3(rigibody.velocity.x, velocityY);
    }
}

 然后我们创建一个玩家状态机让它继承接口并且实现接口的方法。

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

public class PlayerStates : ScriptableObject, IState
{
    protected PlayerController player;

    
    public void Initialize(PlayerController player)
    {
        this.player = player;
        
    }
    public virtual void Enter()
    {
        
    }

    public virtual void Exit()
    {
        
    }

    public virtual void LogicUpdate()
    {
        
    }

    public virtual void PhysicsUpdate()
    {
        
    }
}

 初始工作做完后,我们可以使用Input Systems继续做一个动作系统

 之前的视频已经做过类似的了,所以我这里直接就将动作表贴出来了。

然后点击Generate C#脚本 

他就生成了那个PlayerInputActions的脚本,然后我们再创建一个PlayerInput脚本,把我们上面创建的脚本放在Player上。

 

using UnityEngine;

public class PlayerInput : MonoBehaviour
{
    PlayerInputActions playerInputActions;

    Vector2 Axes => playerInputActions.GamePlay.Axes.ReadValue<Vector2>();
    public bool Jump => playerInputActions.GamePlay.Jump.WasPerformedThisFrame();
    public bool StopJump => playerInputActions.GamePlay.Jump.WasReleasedThisFrame();

    public bool Move => AxisX != 0;
    public float AxisX => Axes.x;
    private void Awake()
    {
        playerInputActions = new PlayerInputActions();
    }

    public void EnableGamePlayInput()
    {
        playerInputActions.GamePlay.Enable();
        Cursor.lockState = CursorLockMode.Locked;
    }
}

 我们还需要一个StateMachine状态机脚本,它负责存储保存好的状态通过字典,然后切换退出开始状态,在它的Update函数中逐帧执行Istate的逻辑函数,然后再FixedUpdate上执行和物理运动相关的函数。

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

public class StateMachine : MonoBehaviour
{
    IState currentState;

    protected Dictionary<System.Type, IState> stateTable;

    private void Update()
    {
        currentState.LogicUpdate();
    }

    private void FixedUpdate()
    {
        currentState.PhysicsUpdate();
    }

    protected void SwitchOn(IState newState)
    {
        currentState = newState;
        currentState.Enter();
    }

    public void SwitchState(IState newState)
    {
        currentState.Exit();
        SwitchOn(newState);
    }

    public void SwitchState(System.Type newStateType)
    {
        SwitchState(stateTable[newStateType]);
    }
}

创建PlayerStateMachine让它继承这个类

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

public class PlayerStateMachine : StateMachine
{

    [SerializeField] PlayerStates[] states;

    Animator animator;

    PlayerInput input;
    PlayerController player;

    private void Awake()
    {
        animator = GetComponentInChildren<Animator>();
        player = GetComponent<PlayerController>();
        input = GetComponent<PlayerInput>();

        stateTable = new Dictionary<System.Type, IState>(states.Length);

        foreach (PlayerStates state in states)
        {
            state.Initialize(animator,player, input ,this);
            stateTable.Add(state.GetType(),state);
        }
    }

    private void Start()
    {
        SwitchOn(stateTable[typeof(PlayerState_Idle)]);
    }
}

 包括:在Awake函数利用循环结构初始化我们的状态以及为字典添加状态。

最后在完善PlayerStates脚本

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

public class PlayerStates : ScriptableObject, IState
{
    [SerializeField,Range(0f,1f)] float transitionDuration = 0.1f;
    [SerializeField] string stateName;
    int stateHash;
    float stateStartTime;

    protected float currentSpeed;

    protected Animator animator;

    protected PlayerController player;

    protected PlayerStateMachine playerStateMachine;

    protected PlayerInput input;

    protected float StateDuration => Time.time - stateStartTime;
    protected bool IsAnimationFinished => StateDuration >= animator.GetCurrentAnimatorStateInfo(0).length;
    private void OnEnable()
    {
        stateHash = Animator.StringToHash(stateName);
    }
    public void Initialize(Animator animator,PlayerController player,PlayerInput input,PlayerStateMachine playerStateMachine)
    {
        this.player = player;
        this.animator = animator;
        this.playerStateMachine = playerStateMachine;
        this.input = input;
    }
    public virtual void Enter()
    {
        //通过哈希值在内存中找到指定的对象
        animator.CrossFade(stateHash, transitionDuration);
        stateStartTime = Time.time;
    }

    public virtual void Exit()
    {
        
    }

    public virtual void LogicUpdate()
    {
        
    }

    public virtual void PhysicsUpdate()
    {
        
    }
}

并且再完善一下PlayerController脚本

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    PlayerGroundDetector groundDetector;
    PlayerInput input;
    Rigidbody rigibody;

    public float MoveSpeed =>Mathf.Abs( rigibody.velocity.x);
    public bool isGrounded => groundDetector.isOnGrounded;
    public bool isFalling => rigibody.velocity.y < 0f && !isGrounded;

    private void Awake()
    {
        groundDetector = GetComponentInChildren<PlayerGroundDetector>();
        input = GetComponent<PlayerInput>();
        rigibody = GetComponent<Rigidbody>();
    }

    private void Start()
    {
        input.EnableGamePlayInput();
    }

    public void Move(float speed)
    {
        if (input.Move)
        {
            transform.localScale = new Vector3(input.AxisX, 1, 1);
        }

        SetVelocityX(speed * input.AxisX);
    }

    public void SetVelocity(Vector3 velocityY)
    {
        rigibody.velocity = velocityY;
    }

    public void SetVelocityX(float velocityX)
    {
        rigibody.velocity = new Vector3(velocityX, rigibody.velocity.y);
    }

    public void SetVelocityY(float velocityY)
    {
        rigibody.velocity = new Vector3(rigibody.velocity.x, velocityY);
    }
}

这里的isGround设计到另一个脚本PlayerGroundDetector脚本,可以看到作者已经为我们添加好一个Gruonded Detector的空对象,我们就直接拖进来就完事了

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

public class PlayerGroundDetector : MonoBehaviour
{
    [SerializeField] float detectionRadius = 0.1f;

    Collider[] colliders = new Collider[1];

    [SerializeField] LayerMask groundLayer;
    public bool isOnGrounded
    {
        get
        {
            //不会产生内存分配,也就不会有垃圾回收机制
            return Physics.OverlapSphereNonAlloc(transform.position, detectionRadius, colliders, groundLayer) != 0;
        }
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, detectionRadius);
    }
}

 最后我们依次为Player的每个动作添加一个状态脚本(如Idle,Run,Jump)这些我们通过特性CreateAssetMenu来为它们每个创建一个图标方便调用。

 大伙先像我这样创建好各个脚本,然后我们梳理一下各个脚本的逻辑就开始编写吧。

using UnityEngine;

[CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Idle",fileName = "PlayerState_Idle")]
public class PlayerState_Idle : PlayerStates
{
    [SerializeField] float deceleration = 5f;
    public override void Enter()
    {
        base.Enter();

        currentSpeed = player.MoveSpeed;
    }

    public override void LogicUpdate()
    {
        if(input.Move)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Run));
        }
        if (input.Jump)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
        }
        if (!player.isGrounded)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Fall));
        }

        currentSpeed = Mathf.MoveTowards(currentSpeed, 0f, deceleration * Time.deltaTime);
    }

    public override void PhysicsUpdate()
    {
        player.SetVelocityX(currentSpeed * player.transform.localScale.x);
    }
}

using UnityEngine;
[CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]
public class PlayerState_Run : PlayerStates
{
    [SerializeField] float runSpeed = 5f;
    [SerializeField] float accleration = 5f;
    public override void Enter()
    {
        base.Enter();

        currentSpeed = player.MoveSpeed;
    }
    public override void LogicUpdate()
    {
        if (!input.Move)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Idle));
        }
        if (input.Jump)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
        }
        if (!player.isGrounded)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Fall));
        }

        currentSpeed = Mathf.MoveTowards(currentSpeed, runSpeed, accleration * Time.deltaTime);
    }

    public override void PhysicsUpdate()
    {
        player.Move(currentSpeed);
    }

}
using UnityEngine;

[CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Fall", fileName = "PlayerState_Fall")]
public class PlayerState_Fall : PlayerStates
{
    [SerializeField] AnimationCurve speedCurve;
    [SerializeField] float moveSpeed = 5f;
    public override void LogicUpdate()
    {
        if (player.isGrounded)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Land));
        }
    }

    public override void PhysicsUpdate()
    {
        player.Move(moveSpeed);
        player.SetVelocityY( speedCurve.Evaluate(StateDuration));
    }


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

[CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/JumpUp", fileName = "PlayerState_JumpUp")]
public class PlayerState_JumpUp : PlayerStates
{
    [SerializeField] float jumpForce = 7f;
    [SerializeField] float moveSpeed = 5f;
    public override void Enter()
    {
        base.Enter();

        player.SetVelocityY(jumpForce);
    }

    public override void LogicUpdate()
    {
        if (input.StopJump || player.isFalling)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Fall));
        }
    }
    public override void PhysicsUpdate()
    {
        player.Move(moveSpeed);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Land", fileName = "PlayerState_Land")]
public class PlayerState_Land : PlayerStates
{
    [SerializeField] float stiffTime = 0.2f;
    public override void Enter()
    {
        base.Enter();

        player.SetVelocity(Vector3.zero); 
    }

    public override void LogicUpdate()
    {
        if (input.Jump)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
        }
        if (StateDuration < stiffTime)
            return;

        if (input.Move)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Run));

        }
        if (IsAnimationFinished)
        {
            playerStateMachine.SwitchState(typeof(PlayerState_Idle));
        }
    }
}

别忘了给墙壁添加Ground 的Layer 

  给我们的Unity_Chan添加好动画组件后并且摆放好,我们终于终结了复杂的动画连线了。 最后再给Main Camera一个CinemaMachine.


学习产出:

  最后我们设置好它们的State属性

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/dangoxiba/article/details/124890351
今日推荐