Unity学习笔记(七)使用状态机重构角色攻击
前言
本文为Udemy课程The Ultimate Guide to Creating an RPG Game in Unity学习笔记
攻击状态重构
首先我们重构攻击状态的动画
之前的动画,我们是使用状态(isAttacking)+攻击次数(comboCounter)完成动画的过渡,这样虽然能完成功能,但是如果状态多了之后非常难维护。现在我们用子状态来处理攻击的动画
首先创建子状态机,将 playerAttack1、playerAttack2、playerAttack3 拷贝到子状态机里面,之后就跟普通的动画连线过程没什么区别了
最终完成的状态、Attack-攻击状态,ComboCounter -连招次数
需要注意的是要记得取消过渡时间
创建攻击状态脚本(PlayerPrimaryAttackState)
PlayerPrimaryAttackState
public class PlayerPrimaryAttackState : PlayerState
{
private int comboCounter;
private float lastTimeAttacked;
private float comboWindow = 2;
public PlayerPrimaryAttackState(Player3 _player, PlayerStateMachine _stateMachine, string _animBoolName) : base(_player, _stateMachine, _animBoolName)
{
}
public override void Enter()
{
base.Enter();
xInput = 0; // 修复移动后按攻击时,攻击方向相反的问题
if (comboCounter > 2 || Time.time >= lastTimeAttacked + comboWindow)
comboCounter = 0;
player.anim.SetInteger("ComboCounter", comboCounter);
float attackDir = player.facingDir;
if (xInput != 0)
{
attackDir = xInput;
}
player.SetVelocity(player.attackMovement[comboCounter].x * attackDir, player.attackMovement[comboCounter].y);
stateTimer = .1f;
}
public override void Exit()
{
base.Exit();
lastTimeAttacked = Time.time;
comboCounter++;
}
public override void Update()
{
base.Update();
if (stateTimer < 0)
{
player.SetZeroVelocity();
}
if (triggerCalled)
{
stateMachine.ChangeState(player.idleState);
}
}
}
PlayerAnimationTriggers 动画结束脚本
public class PlayerAnimationTriggers : MonoBehaviour
{
private Player3 player => GetComponentInParent<Player3>();
private void AnimationTrigger()
{
player.AnimationTrigger();
}
}
Player 脚本,这里只展示变化
public class Player3 : MonoBehaviour{
[Header("Attack details")]
// 这个是来设置攻击时移动的 x,y 轴,通过这个来调整动画的展示效果
public Vector2[] attackMovement;
public float counterAttackDuration = 0.2f;
public PlayerPrimaryAttackState attackState { get; private set; }
private void Awake()
{
...
attackState = new PlayerPrimaryAttackState(this, stateMachine, "Attack");
}
public void AnimationTrigger() => stateMachine.currentState.AnimatorFinishTrigger();
}
最终效果
当前版本的问题
攻击时突然转向
bug 效果
我们可以观察上面状态的变化,player在攻击的间隔会转为 Idle 状态,这个时候突然相反方向,那么就会转换方向
修复问题
使用一个变量表示玩家正在进行某个动作(isBusy),玩家这个状态时,不可以进行方向变化
只展示改动代码
Player3
public class Player3 : MonoBehaviour{
public bool isBusy { get; private set; }
// 这个方法是修复玩家攻击时突然转向的问题
public IEnumerator BusyFor(float _seconds)
{
isBusy = true;
yield return new WaitForSeconds(_seconds);
isBusy = false;
}
}
PlayerIdleState
public class PlayerIdleState : PlayerGroundedState
{
public override void Update()
{
base.Update();
if(xInput != 0 && !player.isBusy)
{
stateMachine.ChangeState(player.moveState);
}
}
}
PlayerPrimaryAttackState
public override void Exit()
{
...
player.StartCoroutine("BusyFor", .15f);
}
最终效果
拓展
Blend Tree & Sub-State Machine 区别
在 Unity 的动画系统中,混合树(Blend Tree) 和 子状态(Sub-State Machine) 是两种不同的功能,分别用于实现动画的不同效果和组织动画的逻辑结构。以下是它们的区别和用途:
- 混合树(Blend Tree)
- 概念: 混合树是一种特殊的动画状态,用于在多段动画之间实现平滑过渡。通过混合参数的调整,Unity 会根据权重动态地混合多个动画剪辑或子混合树,从而生成实时动画。
- 主要功能:
- 实现动画的动态混合,例如:
- 行走、奔跑、冲刺等动作的平滑过渡。
- 不同方向(前、后、左、右)动作的流畅衔接。
- 通过输入参数(如速度、方向)控制混合比例,从而生成适应当前状态的动画。
- 实现动画的动态混合,例如:
- 使用场景:
- 角色的运动状态(如行走和跑步)。
- 需要在多个动画之间平滑转换的情况。
- 优点:
- 动态控制动画混合,非常适合多方向或多状态动画。
- 减少状态机的复杂度,避免过多的状态和过渡线。
- 结构:
- 混合树中的每个动画剪辑(或子混合树)都有对应的权重,权重通过参数(如
Speed
或Direction
)实时调整。 - 可以嵌套子混合树,进一步分层混合。
- 混合树中的每个动画剪辑(或子混合树)都有对应的权重,权重通过参数(如
- 子状态机(Sub-State Machine)
- 概念: 子状态机是一种逻辑分组机制,用于将动画状态机中的多个动画状态和它们的过渡逻辑组织到一个独立的子状态机中。
- 主要功能:
- 提高动画状态机的组织性和可读性。
- 将复杂的状态机分解为更小、更易于管理的模块。
- 使用场景:
- 拥有复杂动画逻辑的角色(如战斗角色)的状态机。
- 将状态机按照功能或逻辑分组,例如:
- 一个子状态机管理移动相关的动画(行走、跑步、跳跃)。
- 另一个子状态机管理攻击相关的动画(轻击、重击、连击)。
- 优点:
- 提高状态机的可维护性,减少主状态机的混乱。
- 子状态机可以嵌套,支持递归组织逻辑。
- 结构:
- 每个子状态机可以包含若干状态(动画剪辑)和它们的过渡线。
- 子状态机之间可以通过入口和出口连接到主状态机或其他状态。
主要区别
特性 | 混合树(Blend Tree) | 子状态机(Sub-State Machine) |
---|---|---|
作用 | 动态混合多个动画,生成实时动画 | 组织和分组动画状态,简化状态机逻辑 |
适用场景 | 需要平滑过渡的多动画混合(如行走和跑步) | 复杂动画逻辑分组(如战斗、移动等模块化状态) |
控制方式 | 通过参数实时调整混合权重 | 通过状态之间的过渡和触发条件切换状态 |
复杂度管理 | 简化动画混合逻辑,但本身是一个状态 | 简化状态机结构,适合管理复杂的动画状态 |
嵌套性 | 支持嵌套子混合树 | 支持嵌套多个子状态机 |
总结
- 混合树: 专注于动画的实时混合(如平滑的方向切换、行走到跑步的动态过渡)。
- 子状态机: 专注于逻辑分组和组织(将状态机模块化,便于管理复杂动画逻辑)。
在实际项目中,可以将两者结合使用:
-
混合树用于处理连续性较强的动画(如运动状态的混合)。
-
子状态机用于对动画状态机的逻辑分组(如区分移动逻辑和攻击逻辑)。
-
-
C# 协程(Coroutine)
在 Unity 中,
StartCoroutine
是一个用于执行 协程(Coroutine) 的方法。协程是 Unity 提供的一种方式,用来在多帧中断执行代码,而不是一次性运行整个方法。通过协程,你可以实现延迟、等待或分阶段的逻辑操作,而无需阻塞主线程。- 协程的特点
- 异步行为:协程允许代码在一定条件下暂停执行,并在后续某个时间点继续运行。
- 基于帧更新:协程通过
yield
语句返回控制权,可以在多帧之间执行操作。 - 与主线程同步:协程不是多线程,它与 Unity 主线程(游戏主循环)一起运行。
2.
StartCoroutine
的用法基本语法
StartCoroutine(IEnumerator coroutineMethod);
- 参数:
IEnumerator
是一个迭代器,用来定义协程的行为。 - 返回值:协程的引用(可以用来停止协程,详见
StopCoroutine
)。
示例 1:等待几秒后执行操作
using UnityEngine; public class CoroutineExample : MonoBehaviour { void Start() { // 启动一个协程 StartCoroutine(WaitAndPrint()); } // 定义协程方法 IEnumerator WaitAndPrint() { Debug.Log("开始等待..."); // 等待 3 秒(暂停) yield return new WaitForSeconds(3f); Debug.Log("3 秒后继续执行!"); } }
示例 2:连续多步操作
IEnumerator MultiStepProcess() { Debug.Log("Step 1"); yield return new WaitForSeconds(2f); // 等待 2 秒 Debug.Log("Step 2"); yield return new WaitForSeconds(1f); // 等待 1 秒 Debug.Log("Step 3"); }
示例 3:无限循环协程
协程可以实现循环逻辑,例如每帧或固定时间间隔执行某些操作。
IEnumerator ContinuousAction() { while (true) { Debug.Log("每 2 秒执行一次!"); yield return new WaitForSeconds(2f); // 等待 2 秒 } } // 在某些时候启动,例如 Start() StartCoroutine(ContinuousAction());
3.
yield
的作用在协程中,
yield
用于暂停协程的执行,并指定暂停条件。以下是一些常见的yield
语句:
常用
yield
表达式语句 说明 yield return null;
暂停到下一帧再继续执行。 yield return new WaitForSeconds(float time);
等待指定时间(以秒为单位),然后继续执行。 yield return new WaitUntil(() => condition);
等待直到条件满足(返回 true
),然后继续执行。yield return new WaitWhile(() => condition);
等待直到条件不再满足(返回 false
),然后继续执行。yield break;
提前退出协程,后续代码不会执行。 停止协程
可以通过
StopCoroutine
或StopAllCoroutines
停止协程。停止指定协程
Coroutine myCoroutine; void Start() { // 保存协程引用 myCoroutine = StartCoroutine(MyCoroutineMethod()); } void StopMyCoroutine() { // 停止协程 StopCoroutine(myCoroutine); }
停止所有协程
void StopAll() { StopAllCoroutines(); // 停止当前脚本中所有运行的协程 }
- 协程的局限性
- 协程不是线程:
- 协程运行在主线程上,不适用于多线程并发处理。
- 如果主线程阻塞,协程也会暂停。
- 耗时操作问题:
- 不适合处理非常长时间的操作(如加载大文件),因为它会阻塞 Unity 的主线程。
- 需要注意生命周期:
- 如果一个对象销毁了,它附加的协程也会自动停止。
- 常见用例
- 延迟操作:在游戏中实现延迟(如攻击冷却、技能施放)。
- 动画控制:在动画或粒子系统中等待一定时间后触发事件。
- 网络请求:等待网络响应完成后处理结果。
- 阶段处理:分帧处理复杂计算或加载操作,避免主线程卡顿。
- 总结
- StartCoroutine 是 Unity 中一种优雅的异步机制,用于执行多帧操作。
- 它的本质是基于
IEnumerator
和yield
来实现的逻辑暂停。 - 虽然简单易用,但需要注意它并不是真正的多线程,不适合所有异步场景。
如果你需要更高性能的异步处理,可以结合 Unity 的 异步任务(Async/Await) 来使用,例如配合
Task
和 UnityWebRequest 等功能。 -