学习目标:
之前我们已经写过非常多的脚本控制玩家类,动画器复杂的连线和众多的判断条件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属性