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) 是两种不同的功能,分别用于实现动画的不同效果和组织动画的逻辑结构。以下是它们的区别和用途:

  1. 混合树(Blend Tree)
  • 概念: 混合树是一种特殊的动画状态,用于在多段动画之间实现平滑过渡。通过混合参数的调整,Unity 会根据权重动态地混合多个动画剪辑或子混合树,从而生成实时动画。
  • 主要功能:
    • 实现动画的动态混合,例如:
      • 行走、奔跑、冲刺等动作的平滑过渡。
      • 不同方向(前、后、左、右)动作的流畅衔接。
    • 通过输入参数(如速度、方向)控制混合比例,从而生成适应当前状态的动画。
  • 使用场景:
    • 角色的运动状态(如行走和跑步)。
    • 需要在多个动画之间平滑转换的情况。
  • 优点:
    • 动态控制动画混合,非常适合多方向或多状态动画。
    • 减少状态机的复杂度,避免过多的状态和过渡线。
  • 结构:
    • 混合树中的每个动画剪辑(或子混合树)都有对应的权重,权重通过参数(如 SpeedDirection)实时调整。
    • 可以嵌套子混合树,进一步分层混合。
  1. 子状态机(Sub-State Machine)
  • 概念: 子状态机是一种逻辑分组机制,用于将动画状态机中的多个动画状态和它们的过渡逻辑组织到一个独立的子状态机中。
  • 主要功能:
    • 提高动画状态机的组织性和可读性。
    • 将复杂的状态机分解为更小、更易于管理的模块。
  • 使用场景:
    • 拥有复杂动画逻辑的角色(如战斗角色)的状态机。
    • 将状态机按照功能或逻辑分组,例如:
      • 一个子状态机管理移动相关的动画(行走、跑步、跳跃)。
      • 另一个子状态机管理攻击相关的动画(轻击、重击、连击)。
  • 优点:
    • 提高状态机的可维护性,减少主状态机的混乱。
    • 子状态机可以嵌套,支持递归组织逻辑。
  • 结构:
    • 每个子状态机可以包含若干状态(动画剪辑)和它们的过渡线。
    • 子状态机之间可以通过入口和出口连接到主状态机或其他状态。

主要区别

特性混合树(Blend Tree)子状态机(Sub-State Machine)
作用动态混合多个动画,生成实时动画组织和分组动画状态,简化状态机逻辑
适用场景需要平滑过渡的多动画混合(如行走和跑步)复杂动画逻辑分组(如战斗、移动等模块化状态)
控制方式通过参数实时调整混合权重通过状态之间的过渡和触发条件切换状态
复杂度管理简化动画混合逻辑,但本身是一个状态简化状态机结构,适合管理复杂的动画状态
嵌套性支持嵌套子混合树支持嵌套多个子状态机

总结

  • 混合树: 专注于动画的实时混合(如平滑的方向切换、行走到跑步的动态过渡)。
  • 子状态机: 专注于逻辑分组和组织(将状态机模块化,便于管理复杂动画逻辑)。

在实际项目中,可以将两者结合使用:

  • 混合树用于处理连续性较强的动画(如运动状态的混合)。

  • 子状态机用于对动画状态机的逻辑分组(如区分移动逻辑和攻击逻辑)。

    • C# 协程(Coroutine)

      在 Unity 中,StartCoroutine 是一个用于执行 协程(Coroutine) 的方法。协程是 Unity 提供的一种方式,用来在多帧中断执行代码,而不是一次性运行整个方法。通过协程,你可以实现延迟、等待或分阶段的逻辑操作,而无需阻塞主线程。

      1. 协程的特点
      • 异步行为:协程允许代码在一定条件下暂停执行,并在后续某个时间点继续运行。
      • 基于帧更新:协程通过 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;提前退出协程,后续代码不会执行。

    停止协程

    可以通过 StopCoroutineStopAllCoroutines 停止协程。

    停止指定协程

    Coroutine myCoroutine;
    void Start()
    {
        // 保存协程引用
        myCoroutine = StartCoroutine(MyCoroutineMethod());
    }
    void StopMyCoroutine()
    {
        // 停止协程
        StopCoroutine(myCoroutine);
    }
    

    停止所有协程

    void StopAll()
    {
        StopAllCoroutines(); // 停止当前脚本中所有运行的协程
    }
    
    1. 协程的局限性
    • 协程不是线程
      • 协程运行在主线程上,不适用于多线程并发处理。
      • 如果主线程阻塞,协程也会暂停。
    • 耗时操作问题
      • 不适合处理非常长时间的操作(如加载大文件),因为它会阻塞 Unity 的主线程。
    • 需要注意生命周期
      • 如果一个对象销毁了,它附加的协程也会自动停止。
    1. 常见用例
    • 延迟操作:在游戏中实现延迟(如攻击冷却、技能施放)。
    • 动画控制:在动画或粒子系统中等待一定时间后触发事件。
    • 网络请求:等待网络响应完成后处理结果。
    • 阶段处理:分帧处理复杂计算或加载操作,避免主线程卡顿。
    1. 总结
    • StartCoroutine 是 Unity 中一种优雅的异步机制,用于执行多帧操作。
    • 它的本质是基于 IEnumeratoryield 来实现的逻辑暂停。
    • 虽然简单易用,但需要注意它并不是真正的多线程,不适合所有异步场景。

    如果你需要更高性能的异步处理,可以结合 Unity 的 异步任务(Async/Await 来使用,例如配合 Task 和 UnityWebRequest 等功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/949569.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu20.04中安装ns-3.36及遇到的问题

一、安装虚拟机&#xff1a;VMware 17.5 参考教程&#xff1a;VMware17Pro虚拟机安装教程(超详细)-CSDN博客 博主&#xff1a;七维大脑 遇到的问题&#xff1a; Q1&#xff1a;安装ubuntu系统时&#xff0c;页面看不到”继续“选项&#xff0c;无法进行下一步 A&#xff…

iOS 逆向学习 - iOS Architecture Cocoa Touch Layer

iOS 逆向学习 - iOS Architecture Cocoa Touch Layer 一、Cocoa Touch Layer 简介二、Cocoa Touch Layer 的核心功能1. UIKit2. Event Handling&#xff08;事件处理&#xff09;3. Multitasking&#xff08;多任务处理&#xff09;4. Push Notifications&#xff08;推送通知&…

人大金仓实现主键自增.

使用数据库中自带的参数类型 serial 类型(相当于创建一个INT列), 或者bigserial(相当于创建一个BIGINT列. 示例sql: CREATE TABLE ord(id SERIAL,ord_no INT NOT NULL,ord_name VARCHAR(32),CONSTRAINT "ord_PKEY" PRIMARY KEY ("id"));插入时指定自增值…

React Router 向路由组件传state参数浏览器回退历史页面显示效果问题

昨天在看尚硅谷张天禹老师讲的 React教程p90&#xff0c;老师讲到 React路由的 replace模式和push模式&#xff0c;老师的演示效果与自己本地操作不太一样。 老师的效果&#xff1a;点击查看消息1&#xff0c;消息2&#xff0c;消息3 再点回退&#xff0c;可以依次查看到 消息…

selenium无法定位元素的几种解决方案

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、frame/iframe表单嵌套 WebDriver只能在一个页面上对元素识别与定位&#xff0c;对于frame/iframe表单内嵌的页面元素无法直接定位。 解决方法&#xff1a; d…

SSM-Spring-IOC/DI注解开发

目录 IOC/DI注解开发 1 注解开发定义bean 2 纯注解开发模式 步骤 Bean的作用范围 Bean生命周期 3 注解开发依赖注入 Autowired 注解实现按照名称注入 简单数据类型注入 注解读取properties配置文件 4 IOC/DI 注解开发管理第三方bean 4.1 步骤&#xff08;以管理第三…

深入探讨 Android 中的 AlarmManager:定时任务调度及优化实践

引言 在 Android 开发中&#xff0c;AlarmManager 是一个非常重要的系统服务&#xff0c;用于设置定时任务或者周期性任务。无论是设置一个闹钟&#xff0c;还是定时进行数据同步&#xff0c;AlarmManager 都是不可或缺的工具之一。然而&#xff0c;随着 Android 系统的不断演…

接口开发完后,个人对于接下来接口优化的一些思考

优化点 入参的合法性和长度范围&#xff0c;必填项的检查验证 因为没有入参&#xff0c;所以不需要考虑。 批量思想解决N1问题 // 假设要查询100个订单及其对应的用户信息 List<Order> orders orderMapper.selectList(new QueryWrapper<>().last("limit …

Redis内存碎片

什么是内存碎片? 你可以将内存碎片简单地理解为那些不可用的空闲内存。 举个例子&#xff1a;操作系统为你分配了 32 字节的连续内存空间&#xff0c;而你存储数据实际只需要使用 24 字节内存空间&#xff0c;那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数…

小程序租赁系统开发的优势与应用前景分析

内容概要 小程序租赁系统是一种新兴的数字化解决方案&#xff0c;旨在为用户提供更加便捷与高效的租赁服务。它通常包括一系列功能&#xff0c;如在线浏览、即时预定、支付功能以及用户反馈机制。这些系统在使用上极为友好&#xff0c;让用户能够轻松选择所需的商品或服务&…

25年1月更新。Windows 上搭建 Python 开发环境:PyCharm 安装全攻略(文中有安装包不用官网下载)

python环境没有安装的可以点击这里先安装好python环境&#xff0c;python环境安装教程 安装 PyCharm IDE 获取 PyCharm PyCharm 提供两种主要版本——社区版&#xff08;免费&#xff09;和专业版&#xff08;付费&#xff09;。对于初学者和个人开发者而言&#xff0c;社区…

RedisTemplate执行lua脚本及Lua 脚本语言详解

使用RedisTemplate执行lua脚本 在开发中&#xff0c;我们经常需要与Redis数据库进行交互&#xff0c;而Redis是一个基于内存的高性能键值存储数据库&#xff0c;它支持多种数据结构&#xff0c;并提供了丰富的命令接口。在某些情况下&#xff0c;我们可能需要执行一些复杂的逻…

基于Python 的宠物管理系统(源码+部署)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

stm32第一次烧录或者上电运行卡死问题分析

问题描述 单片机烧录代码&#xff08;刚上电&#xff09;无法立即运行&#xff0c;必须要复位一次或多次才能运行&#xff1b;跟踪调试会进入HardFault_Handler中断。 问题分析 烧录配置如下图&#xff0c;首先排除配置问题那么该问题就比较让人头大了&#xff0c;理论上&am…

ESP32-C3 AT WiFi AP 启 TCP Server 被动接收模式 + BLE 共存

TCP 被动接收模式&#xff0c;每次发的数据会先存到缓冲区&#xff0c;参见&#xff1a;ATCIPRECVTYPE 指令说明。 即每包数据不会实时报告 IPD 接收情况&#xff0c;如果需要查询缓冲区的数据&#xff0c;先用 ATCIPRECVLEN? 指令查询被动接收模式下套接字数据的长度 。获取…

【LeetCode Hot100 二分查找】搜索插入位置、搜索二维矩阵、搜索旋转排序数组、寻找两个正序数组的中位数

二分查找 搜索插入位置搜索二维矩阵在排序数组中查找元素的第一个和最后一个位置寻找旋转排序数组中的最小值搜索旋转排序数组寻找两个正序数组的中位数&#xff08;hard&#xff09; 搜索插入位置 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并…

ChatGPT 主流模型GPT-4/GPT-4o mini的参数规模是多大?

微软论文又把 OpenAI 的机密泄露了&#xff1f;&#xff1f;在论文中明晃晃写着&#xff1a; o1-preview 约 300B&#xff1b;o1-mini 约 100BGPT-4o 约 200B&#xff1b;GPT-4o-mini 约 8BClaude 3.5 Sonnet 2024-10-22 版本约 175B微软自己的 Phi-3-7B&#xff0c;这个不用约…

Docker 安装Elasticsearch搜索引擎 搜索优化 词库挂载 拼音分词 插件安装

介绍 允许用户快速索引和搜索大量的文本数据。通过使用倒排索引&#xff0c;它能够在海量数据中高效检索相关信息。提供灵活的查询语言&#xff0c;可以做全文搜索、模糊搜索、数据统计等&#xff0c;用来代替MYSQL的模糊搜索&#xff0c;MYSQL的模糊搜索不支持使用索引从而导…

Scala_【5】函数式编程

第五章 函数式编程函数和方法的区别函数声明函数参数可变参数参数默认值 函数至简原则匿名函数高阶函数函数作为值传递函数作为参数传递函数作为返回值 函数闭包&柯里化函数递归控制抽象惰性函数友情链接 函数式编程 面向对象编程 解决问题时&#xff0c;分解对象&#xff…

jenkins入门7 --发送邮件1

jenkins发送邮件配置&#xff08;全局配置&#xff09;_jenkins 怎么发送邮件-CSDN博客 本文通过163发送邮件 1、首先163设置选择pop3/smtp/imap,开启服务&#xff0c;获取授权码 2、jenkins下载邮件插件 登录Jenkins管理界面&#xff0c;点击“Manage Jenkins”。 选择“Man…