当前位置:   article > 正文

[Unity]使用状态机模式创建平台控制游戏(以Unity酱为例)_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接口来给玩家状态机继承。

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public interface IState
  5. {
  6. void Enter();
  7. void Exit();
  8. void LogicUpdate();
  9. void PhysicsUpdate();
  10. }

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

  1. using UnityEngine;
  2. public class PlayerController : MonoBehaviour
  3. {
  4. Rigidbody rigibody;
  5. private void Awake()
  6. {
  7. rigibody = GetComponent<Rigidbody>();
  8. }
  9. private void Start()
  10. {
  11. }
  12. public void SetVelocity(Vector3 velocityY)
  13. {
  14. rigibody.velocity = velocityY;
  15. }
  16. public void SetVelocityX(float velocityX)
  17. {
  18. rigibody.velocity = new Vector3(velocityX, rigibody.velocity.y);
  19. }
  20. public void SetVelocityY(float velocityY)
  21. {
  22. rigibody.velocity = new Vector3(rigibody.velocity.x, velocityY);
  23. }
  24. }

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

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerStates : ScriptableObject, IState
  5. {
  6. protected PlayerController player;
  7. public void Initialize(PlayerController player)
  8. {
  9. this.player = player;
  10. }
  11. public virtual void Enter()
  12. {
  13. }
  14. public virtual void Exit()
  15. {
  16. }
  17. public virtual void LogicUpdate()
  18. {
  19. }
  20. public virtual void PhysicsUpdate()
  21. {
  22. }
  23. }

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

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

然后点击Generate C#脚本 

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

 

  1. using UnityEngine;
  2. public class PlayerInput : MonoBehaviour
  3. {
  4. PlayerInputActions playerInputActions;
  5. Vector2 Axes => playerInputActions.GamePlay.Axes.ReadValue<Vector2>();
  6. public bool Jump => playerInputActions.GamePlay.Jump.WasPerformedThisFrame();
  7. public bool StopJump => playerInputActions.GamePlay.Jump.WasReleasedThisFrame();
  8. public bool Move => AxisX != 0;
  9. public float AxisX => Axes.x;
  10. private void Awake()
  11. {
  12. playerInputActions = new PlayerInputActions();
  13. }
  14. public void EnableGamePlayInput()
  15. {
  16. playerInputActions.GamePlay.Enable();
  17. Cursor.lockState = CursorLockMode.Locked;
  18. }
  19. }

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

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class StateMachine : MonoBehaviour
  5. {
  6. IState currentState;
  7. protected Dictionary<System.Type, IState> stateTable;
  8. private void Update()
  9. {
  10. currentState.LogicUpdate();
  11. }
  12. private void FixedUpdate()
  13. {
  14. currentState.PhysicsUpdate();
  15. }
  16. protected void SwitchOn(IState newState)
  17. {
  18. currentState = newState;
  19. currentState.Enter();
  20. }
  21. public void SwitchState(IState newState)
  22. {
  23. currentState.Exit();
  24. SwitchOn(newState);
  25. }
  26. public void SwitchState(System.Type newStateType)
  27. {
  28. SwitchState(stateTable[newStateType]);
  29. }
  30. }

创建PlayerStateMachine让它继承这个类

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerStateMachine : StateMachine
  5. {
  6. [SerializeField] PlayerStates[] states;
  7. Animator animator;
  8. PlayerInput input;
  9. PlayerController player;
  10. private void Awake()
  11. {
  12. animator = GetComponentInChildren<Animator>();
  13. player = GetComponent<PlayerController>();
  14. input = GetComponent<PlayerInput>();
  15. stateTable = new Dictionary<System.Type, IState>(states.Length);
  16. foreach (PlayerStates state in states)
  17. {
  18. state.Initialize(animator,player, input ,this);
  19. stateTable.Add(state.GetType(),state);
  20. }
  21. }
  22. private void Start()
  23. {
  24. SwitchOn(stateTable[typeof(PlayerState_Idle)]);
  25. }
  26. }

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

最后在完善PlayerStates脚本

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerStates : ScriptableObject, IState
  5. {
  6. [SerializeField,Range(0f,1f)] float transitionDuration = 0.1f;
  7. [SerializeField] string stateName;
  8. int stateHash;
  9. float stateStartTime;
  10. protected float currentSpeed;
  11. protected Animator animator;
  12. protected PlayerController player;
  13. protected PlayerStateMachine playerStateMachine;
  14. protected PlayerInput input;
  15. protected float StateDuration => Time.time - stateStartTime;
  16. protected bool IsAnimationFinished => StateDuration >= animator.GetCurrentAnimatorStateInfo(0).length;
  17. private void OnEnable()
  18. {
  19. stateHash = Animator.StringToHash(stateName);
  20. }
  21. public void Initialize(Animator animator,PlayerController player,PlayerInput input,PlayerStateMachine playerStateMachine)
  22. {
  23. this.player = player;
  24. this.animator = animator;
  25. this.playerStateMachine = playerStateMachine;
  26. this.input = input;
  27. }
  28. public virtual void Enter()
  29. {
  30. //通过哈希值在内存中找到指定的对象
  31. animator.CrossFade(stateHash, transitionDuration);
  32. stateStartTime = Time.time;
  33. }
  34. public virtual void Exit()
  35. {
  36. }
  37. public virtual void LogicUpdate()
  38. {
  39. }
  40. public virtual void PhysicsUpdate()
  41. {
  42. }
  43. }

并且再完善一下PlayerController脚本

  1. using UnityEngine;
  2. public class PlayerController : MonoBehaviour
  3. {
  4. PlayerGroundDetector groundDetector;
  5. PlayerInput input;
  6. Rigidbody rigibody;
  7. public float MoveSpeed =>Mathf.Abs( rigibody.velocity.x);
  8. public bool isGrounded => groundDetector.isOnGrounded;
  9. public bool isFalling => rigibody.velocity.y < 0f && !isGrounded;
  10. private void Awake()
  11. {
  12. groundDetector = GetComponentInChildren<PlayerGroundDetector>();
  13. input = GetComponent<PlayerInput>();
  14. rigibody = GetComponent<Rigidbody>();
  15. }
  16. private void Start()
  17. {
  18. input.EnableGamePlayInput();
  19. }
  20. public void Move(float speed)
  21. {
  22. if (input.Move)
  23. {
  24. transform.localScale = new Vector3(input.AxisX, 1, 1);
  25. }
  26. SetVelocityX(speed * input.AxisX);
  27. }
  28. public void SetVelocity(Vector3 velocityY)
  29. {
  30. rigibody.velocity = velocityY;
  31. }
  32. public void SetVelocityX(float velocityX)
  33. {
  34. rigibody.velocity = new Vector3(velocityX, rigibody.velocity.y);
  35. }
  36. public void SetVelocityY(float velocityY)
  37. {
  38. rigibody.velocity = new Vector3(rigibody.velocity.x, velocityY);
  39. }
  40. }

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

 

 

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public class PlayerGroundDetector : MonoBehaviour
  5. {
  6. [SerializeField] float detectionRadius = 0.1f;
  7. Collider[] colliders = new Collider[1];
  8. [SerializeField] LayerMask groundLayer;
  9. public bool isOnGrounded
  10. {
  11. get
  12. {
  13. //不会产生内存分配,也就不会有垃圾回收机制
  14. return Physics.OverlapSphereNonAlloc(transform.position, detectionRadius, colliders, groundLayer) != 0;
  15. }
  16. }
  17. private void OnDrawGizmos()
  18. {
  19. Gizmos.color = Color.red;
  20. Gizmos.DrawWireSphere(transform.position, detectionRadius);
  21. }
  22. }

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

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

  1. using UnityEngine;
  2. [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Idle",fileName = "PlayerState_Idle")]
  3. public class PlayerState_Idle : PlayerStates
  4. {
  5. [SerializeField] float deceleration = 5f;
  6. public override void Enter()
  7. {
  8. base.Enter();
  9. currentSpeed = player.MoveSpeed;
  10. }
  11. public override void LogicUpdate()
  12. {
  13. if(input.Move)
  14. {
  15. playerStateMachine.SwitchState(typeof(PlayerState_Run));
  16. }
  17. if (input.Jump)
  18. {
  19. playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
  20. }
  21. if (!player.isGrounded)
  22. {
  23. playerStateMachine.SwitchState(typeof(PlayerState_Fall));
  24. }
  25. currentSpeed = Mathf.MoveTowards(currentSpeed, 0f, deceleration * Time.deltaTime);
  26. }
  27. public override void PhysicsUpdate()
  28. {
  29. player.SetVelocityX(currentSpeed * player.transform.localScale.x);
  30. }
  31. }

  1. using UnityEngine;
  2. [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Run", fileName = "PlayerState_Run")]
  3. public class PlayerState_Run : PlayerStates
  4. {
  5. [SerializeField] float runSpeed = 5f;
  6. [SerializeField] float accleration = 5f;
  7. public override void Enter()
  8. {
  9. base.Enter();
  10. currentSpeed = player.MoveSpeed;
  11. }
  12. public override void LogicUpdate()
  13. {
  14. if (!input.Move)
  15. {
  16. playerStateMachine.SwitchState(typeof(PlayerState_Idle));
  17. }
  18. if (input.Jump)
  19. {
  20. playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
  21. }
  22. if (!player.isGrounded)
  23. {
  24. playerStateMachine.SwitchState(typeof(PlayerState_Fall));
  25. }
  26. currentSpeed = Mathf.MoveTowards(currentSpeed, runSpeed, accleration * Time.deltaTime);
  27. }
  28. public override void PhysicsUpdate()
  29. {
  30. player.Move(currentSpeed);
  31. }
  32. }

 

  1. using UnityEngine;
  2. [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Fall", fileName = "PlayerState_Fall")]
  3. public class PlayerState_Fall : PlayerStates
  4. {
  5. [SerializeField] AnimationCurve speedCurve;
  6. [SerializeField] float moveSpeed = 5f;
  7. public override void LogicUpdate()
  8. {
  9. if (player.isGrounded)
  10. {
  11. playerStateMachine.SwitchState(typeof(PlayerState_Land));
  12. }
  13. }
  14. public override void PhysicsUpdate()
  15. {
  16. player.Move(moveSpeed);
  17. player.SetVelocityY( speedCurve.Evaluate(StateDuration));
  18. }
  19. }
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/JumpUp", fileName = "PlayerState_JumpUp")]
  5. public class PlayerState_JumpUp : PlayerStates
  6. {
  7. [SerializeField] float jumpForce = 7f;
  8. [SerializeField] float moveSpeed = 5f;
  9. public override void Enter()
  10. {
  11. base.Enter();
  12. player.SetVelocityY(jumpForce);
  13. }
  14. public override void LogicUpdate()
  15. {
  16. if (input.StopJump || player.isFalling)
  17. {
  18. playerStateMachine.SwitchState(typeof(PlayerState_Fall));
  19. }
  20. }
  21. public override void PhysicsUpdate()
  22. {
  23. player.Move(moveSpeed);
  24. }
  25. }
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. [CreateAssetMenu(menuName = "Data/StateMachine/PlayerState/Land", fileName = "PlayerState_Land")]
  5. public class PlayerState_Land : PlayerStates
  6. {
  7. [SerializeField] float stiffTime = 0.2f;
  8. public override void Enter()
  9. {
  10. base.Enter();
  11. player.SetVelocity(Vector3.zero);
  12. }
  13. public override void LogicUpdate()
  14. {
  15. if (input.Jump)
  16. {
  17. playerStateMachine.SwitchState(typeof(PlayerState_JumpUp));
  18. }
  19. if (StateDuration < stiffTime)
  20. return;
  21. if (input.Move)
  22. {
  23. playerStateMachine.SwitchState(typeof(PlayerState_Run));
  24. }
  25. if (IsAnimationFinished)
  26. {
  27. playerStateMachine.SwitchState(typeof(PlayerState_Idle));
  28. }
  29. }
  30. }

别忘了给墙壁添加Ground 的Layer 

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


 

 

 

 

 

学习产出:

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

 

 

 

 

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/125077
推荐阅读
相关标签
  

闽ICP备14008679号