目录
1.首先要理解为什么要使用状态机进行开发,以及相对于传统开发类型的好处
总结, 本文主要是带领没用过状态机思路开发的小伙伴们入门,建议大家多多尝试各种设计模式,并且能够随机应变才能成为一个好的开发者哦
1.首先要理解为什么要使用状态机进行开发,以及相对于传统开发类型的好处
(这里先提一句 状态机适用的是一个人物或者是其他东西拥有多种较为复杂状态,如果你的人物只有个别几个特别简单的状态的话还是用不到状态机的)
a.状态机可以有效管理游戏状态,避免出现逻辑混乱或者bug.
b.状态机可以提供更简洁的界面,启用可视化调试,方便开发者理解和修改游戏逻辑
c.状态机可以利用Unity的动画系统,实现动画和游戏状态的同步,提高游戏的表现力
d状态机可以遵循状态模式的设计原则,实现状态之间的内部切换,降低状态持有者的复杂度。
2.如果只是这么说肯定是不好理解的
所以举一个具体的例子进行讲解:
开发要求 : 实现一个人物 进行上下左右移动 再给他添加一个技能(位移):点击鼠标左键进行攻击的蓄力,位移距离会随着时间的增加而增加,最后 松开鼠标人物会位移到鼠标方向一定的距离
这种类型的开发 如果使用传统的开发方式的话要注意以下几点:
1. 在进行蓄力的时候,人物是不能动的,要禁用人物移动的函数或者脚本(如果你是分开写的话)
2.是否能完美的契合动画,你需要将之前准备好的动画,反复调试最终找到适合的。(以为实现人物的位移这么几步动画: a.长按蓄力时的准备动画 b.松开鼠标左键以后人物切换成穿刺(也就是位移)的形态 c.保持b. 的状态进行位移
3.在位移的过程种不能再使用按鼠标左键进行蓄力
4 。。。。。。
这些问题如果使用传统开发方式的话会出现以下2种情况:
1. 逻辑十分复杂,即使好不容易调试成功了,这个代码也相当于变成了一块钢板,如果要新增功能的话,改动一下很有可能出现问题(当然这个题目就这一种如果你要增加更多的话就不一样了,比如再加入个记录一下一次位移结束的位置,无论什么时候按下鼠标右键就会回到那个位置 等等等)。
2.不利于调试,混乱的逻辑,各种拆东墙补西墙的操作,会让开发者头痛不已,不便于他人理解。
而与之相对的,使用状态机的话
1.状态机可以将复杂的逻辑分解为有限个状态,以及在这些状态之间的转移和动作,使得代码更加清晰便于维护
2.状态机可以提供更简洁的界面,启用可视化调试,方便开发者理解和修改游戏逻辑
3.状态机可以遵循状态模式的设计原则,实现状态之间的内部切换,降低状态持有者的复杂度
如果能够理解了那么就开始尝试用状态机来写这个功能吧(如果还是不理解建议先用传统方法试一下,再用状态机写一遍应该就能明白了)
3.状态机实现概况:
我这里实现状态机用到了以下几点:
1. 一个叫做 Istate的接口 里面包含 OnEnter , OnUpdate , OnExit 当然你也可以加其他的来细化状态 ,但我觉的这3个一般情况下就够用了
2. 一个FMS脚本 (FSM是有限状态机(finite-state machine)的缩写。它是一种表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型)
它应该要体现 所挂载人物所需要的
参数(比如 生命值, 移动速度,攻击力,等等等) 从而可以方便直观的进行参数修改
所有状态
以及转换状态的函数
3.所有状态各自的脚本。
4.实现状态机的详细步骤:
一. Istate 接口
对于这个接口我们不需要那些命名空间,使用它就是一种多态的理念 就直接这么写就可以了
public interface Istate { void OnEnter(); void OnUpdate(); void OnExit(); }
二.FSM(重点)先放代码有能力的小伙伴自己理解 ,不理解的往下看我讲。
using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using Unity.Mathematics; using Unity.VisualScripting; using UnityEngine; public enum StateType // 所有状态的枚举类型 { Idle,AttackPre,Move,AttackPhase,AttackPhasePre } [Serializable] public class Parameter // 一个类代表玩家的各种参数 { /// <summary> /// 基础参数 /// </summary> public float Health; public float MoveSpeed; public Animator Myanimator; /// <summary> /// 有关闪现的一系列参数 /// </summary> public LayerMask targetLayer; public float attackArea; public Transform AttackPoint; public float MaxAttackArea; public Transform Target; public float FlashTime; public LineRenderer line; } public class FSM : MonoBehaviour // 编写有限状态机的脚本 FSM Finite State Machine { // Start is called before the first frame update public Istate currentState ; // 一个最基本的有限状态机需要声明一个当前的状态 /// <summary> /// 新建一个字典 把State Type 作为检索检字对, Istate 作为检索的内容 /// </summary> private Dictionary<StateType, Istate> states = new Dictionary<StateType, Istate>(); public Parameter parameter; private void Start() { //向字典里加入 状态 states.Add(StateType.Idle, new IdleState(this)); states.Add(StateType.Move, new MoveState(this)); states.Add(StateType.AttackPre, new AttackPre(this)); states.Add(StateType.AttackPhase, new AttackPhase(this)); states.Add(StateType.AttackPhasePre, new AttackPhasePre(this)); TransitionState(StateType.Idle); parameter.Myanimator = GetComponent<Animator>(); } private void Update() { currentState.OnUpdate(); Attack(); File(); if (Input.GetMouseButtonDown(1)) { TransitionState(StateType.AttackPhasePre); if(parameter.Target.position.x - transform.position.x >= 0.0f) { transform.rotation = Quaternion.Euler(0, 0, 0); } else { transform.rotation = Quaternion.Euler(0, 180, 0); } } LineRenderer line = parameter.line; line.positionCount = 2; //设置顶点数量为2 line.SetPosition(0, parameter.Target.position); //设置第一个顶点的位置 line.SetPosition(1, parameter.AttackPoint.position); //设置第二个顶点的位置 } public void TransitionState(StateType type) // 切换状态的函数 { if (currentState != null) // 先退出前一个状态 { currentState.OnExit(); } currentState = states[type]; // 再把现在的状态赋值给它 currentState.OnEnter(); // 然后进入这个状态 } public void File() // 人物反转是一个常驻状态 { Rigidbody2D My = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>(); float Rem = My.velocity.x; if (Rem > 0.0f) { transform.rotation = Quaternion.Euler(0, 0, 0); //翻转 的几种写法 //SpriteRenderer sp = GetComponent<SpriteRenderer>(); //sp.flipX = false; } else if(Rem < 0.0f) { transform.rotation = Quaternion.Euler(0, 180, 0); //翻转 的几种写法 //SpriteRenderer sp = GetComponent<SpriteRenderer>(); //sp.flipX = true; } } public void Attack() { if (Input.GetMouseButtonDown(0)) { TransitionState(StateType.AttackPre); } } private void OnDrawGizmos() { Gizmos.color = Color.blue; Gizmos.DrawWireSphere(parameter.AttackPoint.position, parameter.attackArea); } }
里面比较重要的几点
1.
一个状态的枚举 写出所要用到的状态
2.
[Serializable] public class Parameter // 一个类代表玩家的各种参数 { /// <summary> /// 基础参数 /// </summary> public float Health; public float MoveSpeed; public Animator Myanimator; /// <summary> /// 有关闪现的一系列参数 /// </summary> public LayerMask targetLayer; public float attackArea; public Transform AttackPoint; public float MaxAttackArea; public Transform Target; public float FlashTime; public LineRenderer line; }
一个参数类,方便再其他脚本种调用。
3.我注释都写在里面了应该挺好理解的
三.开始编写各个状态脚本(解释我都写在注释里了,建议自己敲一遍感受)
首先再做一个东西的时候要理清思路建议画一个状态树来让自己更直观的理解。
大致就是这种感觉 :因为这里其实没几个状态所以感觉没什么必要,但你如果做一个Boss 那种复杂状态的话就知道画图的重要了。
因为所要讲的都写在注释里了这里就放代码了,小伙伴们慢慢看(建议写一遍体会一下)
待机状态
using System.Collections; using System.Collections.Generic; using UnityEngine; public class IdleState : Istate // 使用接口实现多态 { // Start is called before the first frame update private FSM Manager; // 获取有限状态机 private Parameter parameter; // 获取参数 public IdleState(FSM manager) { this.Manager = manager; //构造函数方便使用 this.parameter = manager.parameter; } public void OnEnter() { parameter.Myanimator.Play("Idle"); Debug.Log("-><color=red>" + "当前状态是Idle" + "</color>");//便于再控制台直观的现实当前状态 } public void OnUpdate() { float InputX = Input.GetAxis("Horizontal"); float InputY = Input.GetAxis("Vertical"); if ( Mathf.Abs( InputX) > 0.0f ||Mathf.Abs( InputY )> 0.0f) { Manager.TransitionState(StateType.Move); } } public void OnExit() { Debug.ClearDeveloperConsole(); } }
移动状态
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MoveState : Istate { Rigidbody2D My = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>(); // Start is called before the first frame update private FSM Manager; // 获取有限状态机 private Parameter parameter; // 获取参数 public MoveState(FSM manager) { this.Manager = manager; this.parameter = manager.parameter; } public void OnEnter() { parameter.Myanimator.Play("Move"); Debug.Log("-><color=green>" + "当前状态是Move" + "</color>"); } public void OnUpdate() { float InputX = Input.GetAxis("Horizontal"); float InputY = Input.GetAxis("Vertical"); Vector2 PlayerVel = new Vector2(InputX , InputY ).normalized; My.velocity = PlayerVel * parameter.MoveSpeed; if(My.velocity == Vector2.zero) { Manager.TransitionState(StateType.Idle); } } public void OnExit() { My.velocity = Vector2.zero; } }
蓄力
using System.Collections; using System.Collections.Generic; using Unity.VisualScripting; using UnityEngine; public class AttackPre : Istate { // Start is called before the first frame update private FSM manager; private Parameter parameter; private Rigidbody2D rigidbody; public AttackPre (FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public void OnEnter() { parameter.Myanimator.Play("AttackPre"); rigidbody = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>(); parameter.attackArea = 0.0f; } public void OnUpdate() { Debug.Log("-><color=orange>" + "当前状态是攻击蓄力阶段当前半径为"+ parameter.attackArea + "</color>"); Vector2 Mouseposition = Input.mousePosition; Mouseposition = Camera.main.ScreenToWorldPoint(Mouseposition); Vector2 director = (Mouseposition -rigidbody.position).normalized ; parameter.Target.position = rigidbody.position + director * parameter.attackArea; rigidbody = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>(); if (director.x >= 0.0f) { rigidbody.transform.rotation = Quaternion.Euler(0, 0, 0); } else if (director.x < 0.0f) { rigidbody.transform.rotation = Quaternion.Euler(0, 180, 0); } if (parameter.attackArea < parameter.MaxAttackArea) { parameter.attackArea += Time.deltaTime*3; } if (Input.GetMouseButtonUp(0)) { manager.TransitionState(StateType.AttackPhasePre); } } public void OnExit() { } }
蓄力后准备动作
using JetBrains.Annotations; using System.Collections; using System.Collections.Generic; using UnityEngine; public class AttackPhasePre : Istate { // Start is called before the first frame update private Parameter parameter; private FSM manager; public AttackPhasePre (FSM manager) { this.manager = manager; this.parameter = manager.parameter; } public void OnEnter() { parameter.Myanimator.Play("AttackPhase"); Debug.Log("-><color=blue>" + "当前状态是攻击前动作变化,我的攻击范围是" + parameter.attackArea + "</color>"); } public void OnUpdate() { AnimatorStateInfo stateInfo = parameter.Myanimator.GetCurrentAnimatorStateInfo(0); // 得到第0层的动画参数 if(stateInfo.IsName("AttackPhase") && stateInfo.normalizedTime >= 1.0f) { manager.TransitionState(StateType.AttackPhase); } if (Input.GetMouseButtonDown(0)) { manager.TransitionState(StateType.AttackPre); } } public void OnExit() { } }
位移
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AttackPhase : Istate { // Start is called before the first frame update private FSM manager; private Parameter parameter; private Rigidbody2D rigidbody; private Rigidbody2D rigibody; public AttackPhase (FSM manager) { this.manager = manager; this.parameter = manager.parameter; } private float FlashArea; public void OnEnter() { rigibody = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>(); Debug.Log("-><color=yellow>" + "当前状态是攻击中,我的攻击范围是" + parameter.attackArea + "</color>"); Vector3 Mousepos = Input.mousePosition; Mousepos = Camera.main.ScreenToWorldPoint(Mousepos); Vector3 director = Mousepos - parameter.AttackPoint.position; } public void OnUpdate() { if (Vector2.Distance(rigibody.position, parameter.Target.position) > 0.0f) { rigibody.transform.position = Vector2.MoveTowards(rigibody.position, parameter.Target.position, parameter.FlashTime); } else { manager.TransitionState(StateType.Idle); } } public void OnExit() { parameter.attackArea = 0.0f; } }
另外还有一个相机的脚本(这个与状态机无关单纯为了游戏内视觉效果好)
因为是随便写的也就那回事吧
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraMove : MonoBehaviour { // Start is called before the first frame update public Transform target; //玩家角色的Transform组件 public float smoothSpeed = 0.125f; //相机平滑移动的速度系数 public Vector3 offset; //相机相对于玩家角色的偏移量 public float waitTime = 0.0f; public bool fi; void LateUpdate() { //计算相机目标位置,等于玩家位置加上偏移量 Vector3 desiredPosition = target.position + offset; //使用Lerp函数平滑插值计算相机当前位置,避免相机移动过快或过慢 Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed); Vector2 mouspos; if (Input.GetMouseButtonDown(0)) { smoothSpeed *= 3; } if (Input.GetMouseButton(0)) { mouspos = Input.mousePosition; mouspos = Camera.main.ScreenToWorldPoint(mouspos); if (mouspos.y > target.position.y) { offset.y = 8; } else if (mouspos.y < target.position.y) { offset.y =- 8; } if (mouspos.x > target.position.x) { offset.x = 8; } else if (mouspos.x < target.position.x) { offset.x = -8; } } if (Input.GetMouseButtonUp(0)) { smoothSpeed /= 3; offset.y = 0; offset.x = 0; } if (Input.GetMouseButtonUp(1)&&fi == false) { fi = true; smoothSpeed *= 3; } if (fi && waitTime<3) { waitTime += Time.deltaTime; } else if (waitTime >= 3) { waitTime = 0; smoothSpeed /=3 ; fi = false; } //将相机位置设置为平滑位置 transform.position = smoothedPosition; } }
5. 演示视频
我录屏控制台录不进去就分开录了