【Unity】2D平台游戏初中级教程-笔记补充

文章目录

    • 观前提醒
      • 链接地址
        • 百度网盘(第3、4、5章的`Asset`内容)
      • 为什么要弄这篇博客?
      • 章节内容提示
      • 本人制作的环境
    • 第1章:玩家控制器
      • Part1:设置瓦片地图与分类层的顺序
        • 【1】导入素材
        • 【2】制作瓦片地图
        • 【3】调色盘与瓦片存放操作
        • 【4】在 Hierarchy の [Tilemap](https://docs.unity.cn/cn/2020.3/Manual/class-Tilemap.html)
        • 【5】快捷键与最终成品
      • Part2:移动和跳跃
        • 【1】给游戏游戏对象添加碰撞体
        • 【2】角色的移动和跳跃功能(基本原理)
        • 【3】`Cinemachine`插件:实现跟随角色镜头
        • 【4】拓展
      • Part3:动画,地面检测与跳跃次数
        • 【1】Player的Idle状态的动画
        • 【2】动画状态切换与条件判断
        • 【3】C# 核心代码,地面检测与修复无限跳跃
        • 【4】unity操作:
        • 【5】拓展
      • Part4+part5:土狼时间,滑墙和跳墙
        • 【1】C#脚本
        • 【2】滑墙动画
        • 【3】unity操作
        • 【4】拓展
      • Part6:改进跳墙逻辑判断
        • 【1】C#脚本
        • 【2】unity操作
      • Part7:平台攀爬和解决遗留Bug
        • 【1】原理
        • 【2】C#脚本
        • 【3】攀爬动画与添加事件(重复操作)
        • 【4】解决遗留Bug
        • 【5】拓展
      • Part8:冲刺与残影
        • 【1】原理
        • 【2】C#脚本
        • 【3】unity操作
        • 【4】拓展
    • 第2章:基本战斗
      • part9:战斗连击
        • 【1】C#脚本
        • 【2】连击动画
      • part10:怎么实现稻草人的生命周期动画
        • 【1】C#脚本
        • 【2】unity操作
        • 【3】稻草人
        • 【4】预制体
      • part11+part12:死亡特效
    • 第3章~第5章:改进有限状态机与简化动画状态机
        • 【1】理清思路
        • VS实现父子继承操作的快捷键
        • FSM怎么方便操作的

观前提醒

由于代码真的是太多,又被作者重构多次,各位可以根据下面的链接和文章有需自取。

链接地址

油管主(作者):Barden
原视频连接
https://www.youtube.com/watch?v=Pux1GlFwKPs&list=PLy78FINcVmjA0zDBhLuLNL1Jo6xNMMq-W&index=1
原视频简介:
Discord Server:
https://discord.gg/uHQrf7K
Assets:
https://drive.google.com/drive/mobile/folders/1X_BGNUa75INjJRm0G0sEFd6o8E4Z8N8U?usp=sharing

国人制作的笔记与机翻视频(B站UP主勿杉
教程素材、问题、笔记、源码
https://wuushan-public-content.notion.site/2D-f5b04930fa9c40468f360587b14871d0
https://www.bilibili.com/video/BV1ot4y1478z

百度网盘(第3、4、5章的Asset内容)

链接:https://pan.baidu.com/s/1peF7GM6MKpGWrfI16qI0BA
提取码:o8mp

为什么要弄这篇博客?

勿杉UP写的笔记图片不能显示,不方便查看操作,所以我经过他的同意,在CSDN进行一次重述和内容补充。

章节内容提示

  • 第1到2章没必要拘泥于代码,要学的是它实现功能的思路和熟悉unity各个功能模块,避免入坑和浪费时间。
  • 第3到5章则在看视频的基础上结合我给的脚本链接,视频中的多数内容都是在讲如何重构有限状态机。

本人制作的环境

  • unity版本:2022.3.5
  • Microsoft Visual Studio Community 2022 (64 位)
  • .NET Framwork 4.8

第1章:玩家控制器

Part1:设置瓦片地图与分类层的顺序

这里我们不了解Unity 的界面 其实问题也不大,后续都会有图文解释,但是我们先需要做一个约定。

Tab、选项卡、标签页都是一个东西
Player、角色、玩家都指同一个游戏对象

【1】导入素材

我们先要下载好素材先,点击notion笔记里面的素材地址,然后进行下载(建议提前了解别人的环境再导入资源
请添加图片描述
接着导入素材操作
请添加图片描述

导入以下的文件夹内容:

  • 图片素材/Player/Old下的Idel,Jump,Wall,Wall Slide这四张png图片
  • 图片素材/Maps下的所有内容

后续我们要编辑精灵图都基本是下图的操作。
请添加图片描述

【2】制作瓦片地图

单击这些素材文件,然后到Inspector栏下,点击Sprite Editor按钮,编辑2D精灵图,它们设置的值如下:
Tile Set.png
请添加图片描述
Idel,Jump,Wall,Wall Slide.png也是同样操作,值要设置成X:32,Y:32,就是要确定切割的瓦片的长宽,后续都是自行判断。

Decor Items.png图片的值,要按如下这样设置,是为了避免版本不一致,导致后续游戏对象在Scene窗口显示的图片大小的问题,同时要注意Pivot的选择,精灵的中心点在原始纹理的 Rect 中的位置
请添加图片描述

【3】调色盘与瓦片存放操作

打开(如果没有,就到package中搜索,再install,一般都是默认安装好的
请添加图片描述
然后创建调色盘,自行找文件夹位置
请添加图片描述

Decor Items.pngTile Set.png按图操作,然后保存到Assets/Tile Map/Assets中,这个文件夹里面就只会保存切割好的精灵图了。
请添加图片描述

【4】在 Hierarchy の Tilemap

创建游戏对象GameObject
请添加图片描述
游戏对象包含在下图,后续要铺设瓦片,都需要注意先点击对应的游戏对象,具体布局规则可看part1的【5】:
请添加图片描述
按照游戏对象的名称对应修改Soring Layer,而我们后面的操作都离不开这Tags and Layers 的关键操作,要保证再sprite render组件进行下图的一个操作。
请添加图片描述
所有的排序图层如下:
请添加图片描述
【游戏设计】为什么要让草地的图层排在玩家对象前面?
因为要突出草地,同时加上在摄像机的观察下,游戏显示效果更好,几乎所有的2D平台类的游戏都会这样操作。
请添加图片描述

【5】快捷键与最终成品

最终Part1部分,点击Tile Platte标签页,对瓦片按照下图进行关卡设计,其中的快捷键操作提升如下:

  • Shift+[对瓦片地图进行左右选择,即Y轴对称
  • Shift+]对瓦片地图进行上下选择,即X轴对称
  • []键则是顺时针与逆时针旋转
    自己看需要设置地图的转角部分。
    请添加图片描述
    后续瓦片地图操作完成就可以关闭Tile Platte的标签页了。

Part2:移动和跳跃

【1】给游戏游戏对象添加碰撞体

正常地给我们玩家对象添加碰撞体和刚体,模拟物理效果。
请添加图片描述
由于我们要控制Player游戏对象进行一系列涉及到物理模拟的操作,所以为了不让其在移动的途中出现翻转,要冻结旋转,参考文档Rigidbody-freezeRotation - Unity 脚本 API,具体操作如下图:

请添加图片描述
然后就是给我们瓦片地图添加刚体和碰撞体,注意需要加的组件有Tilemap Collider2DComposite Collider2D,其中的刚体组件,unity会自行判断并添加上。另外下图操作的作用:

瓦片地图 2D 碰撞体 (Tilemap Collider 2D) - Unity 手册

2D 复合碰撞体 (Composite Collider 2D) - Unity 手册

  • Composite Collider2D组件可以将多个瓦片变成一个整体,这也是为什么在Scene窗口中,显示瓦片与瓦片之间的网格消失掉了,但是相对的瓦片内部也是可以任由游戏对象进行任何空间上的操作,因此该组件只是作用于瓦片地图的暴露再外的边缘。

请添加图片描述
为什么要设置瓦片地图的刚体为静态,而玩家的刚体组件为动态

TIP:自行深入了解这BodyType选项的区别。

文档内容:

Static 2D 刚体设计为在模拟条件下完全不动;如果任何对象与 Static 2D 刚体碰撞,此类型刚体的行为类似于不可移动的对象(就像具有无限质量)。此刚体类型也是使用资源最少的刚体类型。Static 刚体只能与 Dynamic 2D 刚体碰撞。不支持两个 Static 2D 刚体进行碰撞,因为这种刚体不是为了移动而设计的。

A Static Rigidbody 2D is designed to not move under simulation at all; if anything collides with it, a Static Rigidbody 2D behaves like an immovable object (as though it has infinite mass). It is also the least resource-intensive body type to use. A Static body only collides with Dynamic Rigidbody 2Ds. Having two Static Rigidbody 2Ds collide is not supported, since they are not designed to move.

效果如下:
请添加图片描述

【2】角色的移动和跳跃功能(基本原理)

实现移动:

  1. 在每帧更新中,获取水平输入值,通常使用Input.GetAxisRaw("Horizontal")
  2. 根据输入值和移动速度,计算出水平移动的速度。这是你想要将角色移动的方向和速度。
  3. 使用 Rigidbody2D 组件,将计算出的水平速度分配给角色的刚体,这将使角色沿水平方向移动。

示例代码片段:

float movementInputDirection = Input.GetAxisRaw("Horizontal");
Vector2 movement = new Vector2(movementInputDirection * movementSpeed, rigidbody2D.velocity.y);
rigidbody2D.velocity = movement;

实现跳跃:

  1. 检查玩家是否按下跳跃键(例如,空格键)。
  2. 如果玩家按下了跳跃键,为角色的垂直速度(通常是 Y 轴速度)添加一个跳跃力(jumpForce)。
  3. 这将使角色向上跳跃。

示例代码片段:

if (Input.GetButtonDown("Jump"))
{rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, jumpForce);
}

涉及到的问题:为什么打包游戏,会出现“The type or namespace name UnityEditor could not be found. Areyoumissing a using directiveor an assembly reference?”错误?

如果当各位打开自己脚本代码时,存在一个Tilemaps的引用,而根据using UnityEditor引用注意事项_差点忘记打铁了的博客-CSDN博客这篇文章,可知

因为Unity在发布游戏的时候不会使用UnityEditor命名空间的程序集UnityEditor.dll,自然就不能识别UnityEditor命名空间了。

所以各位需要注意对代码中的UnityEditor.Tilemaps进行注释,反正实际的运行也不会启用。

数值自行调整,控制Player游戏对象的跳跃受力,测试查看具体的跳跃效果。请添加图片描述

【3】Cinemachine插件:实现跟随角色镜头

如果是观看视频的朋友,参考这篇CSDN的文章内容解决Unity导入Cinemachine插件后不显示在菜单栏而是显示在Gameobject栏的问题_cinemachine不在菜单栏_master_yi_的博客-CSDN博客,如果你的Unity版本跟我一样,那么下载插件,untiy2022平台对应2.9.7版本:
请添加图片描述
新建这个游戏对象,命名为Player Camera
请添加图片描述

值就如下图设置,然后空对象Cameras下绑定多个相关有关镜头的子对象,Main Camera可以在cinemachine插件的帮助下,实现我们摄像头跟随Player游戏对象的目标。

问题:Cinemachine实现跟随角色镜头时,角色移动产生画面抖动现象?

解决方案:

在part2的【2】中涉及到的脚本,由于我们已经实现了角色的移动是在FixedUpdate()这个生命周期内进行,所以就只用在Main Camera中的Cinemachine BrainUpdate Method更改为Fixed Update可以解决Player移动发生的抖动。

实际上我没看到在unity2022有这个问题,就算是默认的Smart Update也不可能发生抖动了,而这类问题根据网上的信息来看,更多发生在2021版本(亲自试过还真是这样!)。

问题:Cinemachine实现跟随角色镜头时,出现白线和黑线问题?

【4】拓展

①Unity在VS中没有代码提示的问题(代码自动补全问题)

Unity在VS中没有代码提示的问题_vs2019没有代码提示了_菊头蝙蝠的博客-CSDN博客

②我走过的坑:不同版本的unity打开同一个项目

我之前分不清抖动与地图抗锯齿线的问题,纠结了半天,于是就手贱换到unity 2021平台进行测试,结果就报了An error occurred while resolving packages的错误,之所以产生这样的原因是因为unity是向下兼容部分版本的,即只能让unity2022平台打开用unity2021完成的项目,而不能反过来,其他版本暂时未试过。

解决方案:看这篇Unity项目报错_an error occurred while resolving packages: one or_夏炎黄的博客-CSDN博客,但是我不建议这样干!因为要重新下载对应unity版本的插件,如果是在公司用,应该明天就要滚了吧,个人就无所谓,毕竟可以重新下嘛。

Part3:动画,地面检测与跳跃次数

【1】Player的Idle状态的动画

在part2的视频中,我们要实现PlayerIdle动画,先打开并创建动画与动画控制器的标签页
请添加图片描述

接着我们在文件夹中,新建对应的动画控制器
请添加图片描述

点击我们的游戏对象,然后到Animation标签页,制作动画的方式如下,跟着下图创建动画状态比在unity的project标签页中右键创建会方便操作的多。
请添加图片描述

Player这个游戏对象 添加对应属性,选择它应有的动画控制器,后续的操作都不会啰嗦提示。

请添加图片描述

打开Animation Controller标签页,我们设置动画状态机图应该是这样的
请添加图片描述

效果如下:
请添加图片描述

问题①:在unity哪里找到Smaples选项,从而可以调整人物的动画时间间隔?

Animation标签页,注意要先点击动画状态才会自动显示。
请添加图片描述

问题②:手贱搞动画时,遇到错误: Animation AnimationEvent has no function name specified!

解决方案:Unity3d Animation AnimationEvent has no function name specified!_一笑傲王侯的博客-CSDN博客

接上操作,同样的步骤可以制作Walk动画状态,经过个人的测试,发现Idle状态的Sample Rate12,而Walk状态为30,动画效果会更好,当然做到下图的效果还需要后续的努力。

请添加图片描述

我们继续对Player游戏对象进行操作,完成跳跃的动画状态,方便后续的混合树。
请添加图片描述

【2】动画状态切换与条件判断

首先我们在Animator标签页中创建三个参数用于后续的条件判断,其中当Player跳跃时yVelocity为正,下落为负
请添加图片描述

在进行动画状态切换,拉箭头(make transition)之前,我们先要了解官方文档动画过渡 - Unity 手册 (unity3d.com)的内容,知道后续的一个基本操作。
请添加图片描述

参考文档混合树 - Unity 手册 (unity3d.com)的内容,__混合树__允许通过不同程度合并多个动画来使动画平滑混合,而它就很适合因为yVelocity的一个变化,展示跳跃动画状态从起跳到落地,所以我们需要在Animator标签页创建一个混合树,然后如下图设置即可。
请添加图片描述
最终的一个动画状态机如下图所示:
请添加图片描述

【3】C# 核心代码,地面检测与修复无限跳跃

PlayerController.cs

代码中//...表示承接上文的代码,如果出现了函数则表示替换/添加。

// 玩家控制器类,继承自MonoBehaviour
public class PlayerController : MonoBehaviour
{private void Start(){//...animator = GetComponent<Animator>(); // 获取动画控制器组件amountOfJumpsLeft = amountOfJumps; // 初始化剩余可跳跃次数}private void Update(){//...UpdateAnimations(); // 更新动画状态CheckIfCanJump(); // 检查是否可以跳跃}private void FixedUpdate(){//...CheckSurroundings(); // 检查周围环境}private void Jump(){// 跳跃方法if (canJump){rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, jumpForce); // 设置刚体的Y轴速度实现跳跃amountOfJumpsLeft--; // 跳跃次数减少}}private void CheckMovementDirection(){//...if(rigidbody2D.velocity.x != 0){isWalking = true; // 如果刚体X轴速度不为0,表示在行走}else{isWalking = false; // 否则不在行走}}// 检查周围环境是否在地面上private void CheckSurroundings(){isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, whatIsGround);}// 更新动画状态private void UpdateAnimations(){animator.SetBool("isWalking", isWalking); // 更新动画参数isWalkinganimator.SetBool("isGrounded", isGrounded); // 更新动画参数isGroundedanimator.SetFloat("yVelocity", rigidbody2D.velocity.y); // 更新动画参数yVelocity}// 在编辑器中绘制地面检测点的圆形Gizmosprivate void OnDrawGizmosSelected(){Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);}// 检查是否可以跳跃private void CheckIfCanJump(){if(isGrounded && rigidbody2D.velocity.y <= 0){amountOfJumpsLeft = amountOfJumps; // 在地面且Y轴速度小于等于0时,重置剩余可跳跃次数}if(amountOfJumpsLeft <= 0){canJump = false; // 如果剩余可跳跃次数小于等于0,则无法再跳跃}else{canJump = true; // 否则可以跳跃}}
}

奇怪的BUG:自由落体后,rigidbody.velocity.x在Update生命周期内出现一个极大负值?
具体画面表现,启动游戏的几分秒内,Player游戏对象就会先出现Walk动画状态,而这种情况只当游戏对象自由落体后就会产生。
经过我跟ChatGPT的探讨,它并不觉得代码存在问题,因为rigidbody.velocity.xmovementSpeedmovementInputDirection决定,而我DEBUG后发现方向值是固定为0右为1,左为-1)的,因此如果你也出现了这样的BUG,那么就跟我一样,修改CheckMovementDirection()函数中isWalking的条件判断即可。

if (movementInputDirection != 0) isWalking = true;
else isWalking = false;

而我目前也暂时没有从官方文档2D 刚体 - Unity 手册 (unity3d.com)中得到任何启示。

或者修改成如下的写法:

if (Mathf.Abs(rigidbody2D.velocity.x)>=0.01f){isWalking = true;}else{isWalking = false;}

【4】unity操作:

根据上述代码OnDrawGizmosSelected()函数,首先对我们的平台进行图层标记,以让Player游戏对象可以地面检测。
请添加图片描述

文档Gizmos-DrawWireSphere - Unity 脚本 API教我们如何绘制圆形,然后就是让Player作为父对象,新建一个用于地面检测子空对象,它将作为transform得到一个检测圆形的半径,也即是物理射线检测,后面诸多的检测墙面和边角,判断敌人都离不开它。
请添加图片描述

【5】拓展

①额外了解
Animator - Unity 脚本 API
②Inspector面板的右上角三点打开,选择Debug可以显示私有变量、组件实例化编号等详细信息。
请添加图片描述

③遗留问题:1次跳跃情况下的良性bug?

问题原因:因为没有给刚体组件添加2D物理材质

请添加图片描述

给我们Player游戏对象的刚体组件添加物理材质,我看了下文档2D 物理材质 - Unity 手册 (unity3d.com),摩擦力的用处是在于基于给予一个物体力的矢量操作时,才能发挥作用,而目前都是修改Vector2空间位置,加了摩擦力反而会造成物体不能移动的问题,所以记得设为0。
请添加图片描述

Part4+part5:土狼时间,滑墙和跳墙

【1】C#脚本

PlayerController.cs

public class PlayerController : MonoBehaviour
{//...// 定义变量和属性public Transform wallCheck; // 用于检测是否接触墙壁的射线点private bool isTouchingWall; // 是否接触墙壁public float wallCheckDistance; // 检测墙壁的距离private bool isWallSliding; // 是否正在墙壁滑动public float wallSlideSpeed; // 墙壁滑动速度public float movementForceInAir = 50f; // 空中移动的力大小public float airDragMultiplier = 0.9f; // 空中阻力乘数public float variableJumpHeightMultiplier = 0.5f; // 可变跳跃高度的乘数public Vector2 wallHopDirection; // 蹬墙壁方向public Vector2 wallJumpDirection; // 墙壁跳跃方向public float wallHopForce; // 蹬墙壁力大小public float wallJumpForce; // 墙壁跳跃的力大小private int facingDirection = 1; // 角色朝向// 在脚本启动时执行private void Start(){// 初始化墙壁跳跃方向wallHopDirection.Normalize();wallJumpDirection.Normalize();}// 在每一帧更新时执行private void Update(){// 检测是否正在墙壁滑动CheckIfWallSliding();}// 检测玩家输入private void CheckInput(){//...// 检测跳跃松开输入(土狼时间),还要考虑空气阻力、重力等情况,具体看ApplyMovementif (Input.GetButtonUp("Jump")){// 根据可变跳跃高度乘数调整垂直速度rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, rigidbody2D.velocity.y * variableJumpHeightMultiplier);}}// 翻转角色朝向private void Flip(){if (!isWallSliding){// 改变角色朝向facingDirection *= -1;//...}}// 更新动画状态private void UpdateAnimations(){//...animator.SetBool("isWallSliding", isWallSliding);}// 检测周围环境private void CheckSurroundings(){//...// 发射射线检测是否接触墙壁isTouchingWall = Physics2D.Raycast(wallCheck.position, transform.right, wallCheckDistance, whatIsGround);}// 在编辑模式下绘制场景Gizmosprivate void OnDrawGizmosSelected(){//...// 绘制墙壁检测射线Gizmos.DrawLine(wallCheck.position, new Vector3(wallCheck.position.x + wallCheckDistance, wallCheck.position.y, wallCheck.position.z));}// 检测是否正在墙壁滑动private void CheckIfWallSliding(){// 如果接触到墙壁,不在地面上,并且垂直速度小于0,则正在墙壁滑动if (isTouchingWall && !isGrounded && rigidbody2D.velocity.y < 0){isWallSliding = true;}else{isWallSliding = false;}}// 应用角色移动private void ApplyMovement(){// 如果不在地面上、不在墙壁滑动状态且没有水平输入,则应用空中阻力if (!isGrounded && !isWallSliding && movementInputDirection == 0){rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x * airDragMultiplier, rigidbody2D.velocity.y);}else{// 在地面上或墙壁滑动状态下,应用水平移动速度rigidbody2D.velocity = new Vector2(movementSpeed * movementInputDirection, rigidbody2D.velocity.y);}// 如果正在墙壁滑动,限制垂直速度if (isWallSliding){if (rigidbody2D.velocity.y < -wallSlideSpeed){rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, -wallSlideSpeed);}}}// 角色跳跃private void Jump(){if (canJump && !isWallSliding){// 应用跳跃力,并减少可跳跃次数rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, jumpForce);amountOfJumpsLeft--;}else if (isWallSliding && movementInputDirection == 0 && canJump){// 在墙壁滑动时,进行墙壁跳跃isWallSliding = false;amountOfJumpsLeft--;Vector2 forceToAdd = new Vector2(wallHopForce * wallHopDirection.x * -facingDirection, wallHopForce * wallHopDirection.y);rigidbody2D.AddForce(forceToAdd, ForceMode2D.Impulse);}else if ((isWallSliding || isTouchingWall) && movementInputDirection != 0 && canJump){// 在墙壁滑动或接触墙壁状态下,进行墙壁跳跃isWallSliding = false;amountOfJumpsLeft--;Vector2 forceToAdd = new Vector2(wallJumpForce * wallJumpDirection.x * movementInputDirection, wallJumpForce * wallJumpDirection.y);rigidbody2D.AddForce(forceToAdd, ForceMode2D.Impulse);}}// 检测是否能够跳跃private void CheckIfCanJump(){if ((isGrounded && rigidbody2D.velocity.y <= 0) || isWallSliding){// 如果在地面上并且垂直速度小于等于0,或者正在墙壁滑动,则重置可跳跃次数amountOfJumpsLeft = amountOfJumps;}//...}
}

问题:下面为视频Part4的脚本,它与上面本章的part4Applymovement()中在对移动判断的区别:

函数本章视频
结构与可读性代码简洁,易读条件语句较多,可读性稍差
性能较少使用物理引擎操作,性能可能稍好使用了AddForce,可能对性能有微小影响
维护性简单逻辑,易于维护多条件语句,需要更多注意维护
扩展性功能较简单,扩展性有限使用AddForce,具备更大的功能扩展性

选择哪个函数更好取决于你的需求和项目规模。如果项目相对简单,强调代码的可读性和维护性,本章可能更适合。如果你希望拥有更多的物理特性和灵活性,视频的可能更合适。

private void ApplyMovement()
{// 应用角色的移动逻辑// 如果角色在地面上if (isGrounded){// 设置角色的水平速度,保持垂直速度不变rigidbody2D.velocity = new Vector2(movementSpeed * movementInputDirection, rigidbody2D.velocity.y);}// 如果角色不在地面上,没有贴墙滑动,并且有水平输入else if (!isGrounded && !isWallSliding && movementInputDirection != 0){// 计算要添加的空中水平力Vector2 forceToAdd = new Vector2(movementForceInAir * movementInputDirection, 0);rigidbody2D.AddForce(forceToAdd);// 如果水平速度超过最大移动速度,限制水平速度if (Mathf.Abs(rigidbody2D.velocity.x) > movementSpeed){rigidbody2D.velocity = new Vector2(movementSpeed * movementInputDirection, rigidbody2D.velocity.y);}}// 如果角色不在地面上,没有贴墙滑动,并且没有水平输入else if (!isGrounded && !isWallSliding && movementInputDirection == 0){// 应用空气阻力来减缓水平速度rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x * airDragMultiplier, rigidbody2D.velocity.y);}// 如果角色正在贴墙滑动if (isWallSliding){// 如果垂直速度过快,限制垂直速度if (rigidbody2D.velocity.y < -wallSlideSpeed){rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, -wallSlideSpeed);}}
}

【2】滑墙动画

Animation标签页中,将图片素材/Player/Old/Wall Slide.png图片按之前的步骤切割处理成sprite图,之后再新建动画状态,拖拽图片到下图标签页,就变成动画。

请添加图片描述
点击Player游戏对象先,再到在Animator标签页,先自行设置布尔参数isWallSliding,它的动画状态机如下图所示(本章后续的步骤不少都是重复的,我不会太多图文内容,会将更多的精力放在新知识上):

请添加图片描述
bug问题:没有设置正确动画的状态切换条件

操作:跳上墙,然后按住右键,在接近地面后就按住左键

请添加图片描述

【3】unity操作

首先跟之前的地面检测一样的操作,新建用于墙面检测的transform
请添加图片描述
然后按住下图的指示操作,另外一定要注意调整后续涉及到的检测线的位置,如果位置不对,那么会出现很严重的问题
请添加图片描述
设置如下的数值,就可以进行测试了。
请添加图片描述

【4】拓展

①土狼时间是什么?为什么我们要在平台动作类游戏中这样设计。

https://game.academy.163.com/course/careerArticle?course=517

这里我顺便推荐一个有关独立游戏制作的优质平台给各位,它会用C#实现比本章更加详细的土狼时间。

https://indienova.com/indie-game-development/input-buffering-and-coyote-time/

OnDrawGizmosSelected()OnDrawGizmos()的区别

在unity编辑器点击Gizmos操作后,一个显示操作麻烦了点(如果存在父子关系则选择父类游戏对象),另一个则是不选择对应的游戏对象也能自行显示。

Part6:改进跳墙逻辑判断

【1】C#脚本

PlayerController.cs它的主要操作就是拆分跳跃的逻辑,增加了判断和检测条件

布尔变量canJump变为到底是平台跳跃NormalJump和墙面跳跃WallJump

Jump()函数变为NormalJump()WallJump(),考虑计时器对跳跃的影响(实际的跳墙操作我并不觉得好

public class PlayerController : MonoBehaviour
{//...private float jumpTimer; // 跳跃计时器,用于控制跳跃时机public float jumpTimerSet = 0.15f; // 跳跃计时器的初始值private bool isAttemptingToJump; // 是否正在尝试跳跃private bool canNormalJump; // 是否能够普通跳跃private bool canWallJump; // 是否能够墙壁跳跃private bool checkJumpMultiplier; // 是否需要检查跳跃倍数private bool canMove; // 是否能够移动private bool canFlip; // 是否能够翻转private float turnTimer; // 翻转计时器,用于控制翻转延迟public float turnTimerSet = 0.1f; // 翻转计时器的初始值private float wallJumpTimer; // 墙壁跳跃计时器,用于控制墙壁跳跃后的无敌时间public float wallJumpTimerSet = 0.5f; // 墙壁跳跃计时器的初始值private bool hasWallJumped; // 是否已经执行了墙壁跳跃private int lastWallJumpDirection; // 上一次墙壁跳跃的方向private void Update(){//...CheckJump();// 在每帧更新时检查跳跃}private void CheckInput(){// 检查输入,包括移动和跳跃movementInputDirection = Input.GetAxisRaw("Horizontal"); // 获取水平移动输入if (Input.GetButtonDown("Jump")) // 检测是否按下跳跃按钮{if (isGrounded || (amountOfJumpsLeft > 0 && isTouchingWall)) // 当在地面上或者还有剩余跳跃次数且贴着墙壁时{NormalJump(); // 执行普通跳跃}else{jumpTimer = jumpTimerSet; // 否则,开始进行长按跳跃计时isAttemptingToJump = true; // 标记为正在尝试跳跃}}if (Input.GetButtonDown("Horizontal") && isTouchingWall) // 当按下水平方向按钮且贴着墙壁时{if (!isGrounded && movementInputDirection != facingDirection) // 当不在地面上且输入方向与面朝方向不一致时{canMove = false; // 禁止移动canFlip = false; // 禁止翻转turnTimer = turnTimerSet; // 启动翻转延迟计时}}if (!canMove) // 如果不能移动{turnTimer -= Time.deltaTime; // 计时器递减if (turnTimer <= 0) // 当计时器归零{canMove = true; // 允许移动canFlip = true; // 允许翻转}}if (checkJumpMultiplier && !Input.GetButton("Jump")) // 如果需要检查跳跃倍数且未按下跳跃按钮{checkJumpMultiplier = false; // 关闭检查跳跃倍数的标志rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, rigidbody2D.velocity.y * variableJumpHeightMultiplier); // 对垂直速度进行跳跃倍数调整}}private void CheckJump(){if (jumpTimer > 0) // 如果跳跃计时器大于0{if (!isGrounded && isTouchingWall && movementInputDirection != 0 && movementInputDirection != facingDirection) // 如果不在地面上,贴着墙壁,并且有水平输入{WallJump(); // 执行墙壁跳跃}else if (isGrounded) // 否则,如果在地面上{NormalJump(); // 执行普通跳跃}}if (isAttemptingToJump) // 如果正在尝试跳跃{jumpTimer -= Time.deltaTime; // 跳跃计时器递减}if (wallJumpTimer > 0) // 如果墙壁跳跃计时器大于0{if (hasWallJumped && movementInputDirection == -lastWallJumpDirection) // 如果已经执行了墙壁跳跃且水平输入与上次墙壁跳跃方向相反{rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, 0.0f); // 停止垂直速度hasWallJumped = false; // 重置墙壁跳跃标志}else if (wallJumpTimer <= 0) // 否则,如果墙壁跳跃计时器归零{hasWallJumped = false; // 重置墙壁跳跃标志}else{wallJumpTimer -= Time.deltaTime; // 墙壁跳跃计时器递减}}}private void NormalJump(){if (canNormalJump) // 如果可以普通跳跃{rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, jumpForce); // 对垂直速度进行普通跳跃力的设置amountOfJumpsLeft--; // 跳跃次数减少jumpTimer = 0; // 跳跃计时器归零isAttemptingToJump = false; // 重置跳跃尝试标志checkJumpMultiplier = true; // 启动检查跳跃倍数}}private void WallJump(){if (canWallJump) // 如果可以墙壁跳跃{rigidbody2D.velocity = new Vector2(rigidbody2D.velocity.x, 0.0f); // 停止垂直速度isWallSliding = false; // 关闭墙壁滑行标志amountOfJumpsLeft = amountOfJumps; // 重置剩余跳跃次数amountOfJumpsLeft--; // 跳跃次数减少Vector2 forceToAdd = new Vector2(wallJumpForce * wallJumpDirection.x * movementInputDirection, wallJumpForce * wallJumpDirection.y); // 计算墙壁跳跃的力rigidbody2D.AddForce(forceToAdd, ForceMode2D.Impulse); // 施加力进行墙壁跳跃jumpTimer = 0; // 跳跃计时器归零isAttemptingToJump = false; // 重置跳跃尝试标志checkJumpMultiplier = true; // 启动检查跳跃倍数turnTimer = 0; // 翻转计时器归零canMove = true; // 允许移动canFlip = true; // 允许翻转hasWallJumped = true; // 设置墙壁跳跃标志wallJumpTimer = wallJumpTimerSet; // 设置墙壁跳跃计时器lastWallJumpDirection = -facingDirection; // 更新上次墙壁跳跃方向}}private void Flip(){if (!isWallSliding && canFlip) // 如果不在墙壁滑行状态且可以翻转{// ...}}private void CheckIfCanJump(){if (isGrounded && rigidbody2D.velocity.y <= 0.01f) // 如果在地面上且垂直速度接近零{amountOfJumpsLeft = amountOfJumps; // 重置剩余跳跃次数}if (isTouchingWall) // 如果贴着墙壁{canWallJump = true; // 允许墙壁跳跃}if (amountOfJumpsLeft <= 0) // 如果剩余跳跃次数小于等于零{canNormalJump = false; // 禁止普通跳跃}else{canNormalJump = true; // 允许普通跳跃}}private void CheckIfWallSliding(){if (isTouchingWall && movementInputDirection == facingDirection && rigidbody2D.velocity.y < 0) // 如果贴着墙壁,输入方向与面朝方向相同且垂直速度小于零{isWallSliding = true; // 设置墙壁滑行标志}else{isWallSliding = false; // 关闭墙壁滑行标志}}private void ApplyMovement(){if (!isGrounded && !isWallSliding && movementInputDirection == 0){//...}else if(canMove){//...} //...}
}

【2】unity操作

Player游戏对象的设值,可以自行调整跳跃的力量、滑墙速度、重力等影响,查看具体效果。
请添加图片描述

Part7:平台攀爬和解决遗留Bug

【1】原理

平台攀爬的基本原理如下

  1. 检测台阶探测点: 通过一个射线起点 ledgeCheck 来探测角色是否接触到台阶。当角色碰到墙壁,但没有碰到台阶时,记录下台阶底部的位置 ledgePosBot
  2. 检测能否攀爬: 通过之前的墙面检测射线,当角色贴着墙壁,就会显示为true。如果之前没有检测到台阶并且此时发现了台阶(!ledgeDetected),则将 ledgeDetected 标志设置为 true,并记录台阶底部的位置。
  3. 开始攀爬: 一旦检测到台阶且没有进行过攀爬操作(!canClimbLedge),则将 canClimbLedge 设置为 true,表示角色可以执行攀爬动作。根据角色的朝向,计算出两个可能的台阶顶部位置 ledgePos1ledgePos2
  4. 禁止移动和翻转: 在进行攀爬时,设置 canMovecanFlipfalse,以防止角色在攀爬过程中移动或翻转。
  5. 更新攀爬的动画: 设置角色的动画状态,表示角色正在进行攀爬操作。
  6. 移动到台阶顶部位置: 将角色的位置移动到台阶顶部位置 ledgePos1,以便角色完成攀爬。
  7. 攀爬完成: 当攀爬结束时,例如通过动画事件触发,将 canClimbLedge 设置为 false,允许角色恢复移动和翻转,然后将角色的位置移动到另一个台阶顶部位置 ledgePos2,以确保角色不再粘在台阶上。

请添加图片描述

【2】C#脚本

PlayerController.cs

public class PlayerController : MonoBehaviour
{//...public Transform ledgeCheck; // 检测台阶的射线起点private bool isTouchingLedge; // 是否触碰到台阶private bool canClimbLedge = false; // 是否可以攀爬台阶private bool ledgeDetected; // 是否探测到台阶private Vector2 ledgePosBot; // 台阶底部位置private Vector2 ledgePos1; // 位置1private Vector2 ledgePos2; // 位置2// 攀爬台阶的偏移量public float ledgeClimbXOffset1 = 0f;public float ledgeClimbYOffset1 = 0f;public float ledgeClimbXOffset2 = 0f;public float ledgeClimbYOffset2 = 0f;private void Update(){//...CheckLedgeClimb(); // 检查攀爬台阶}// 检查玩家输入的函数private void CheckInput(){//...if (turnTimer >= 0) // 如果计时器大于等于0,进行一次性延迟操作{turnTimer -= Time.deltaTime; // 减少计时器值if (turnTimer <= 0) // 当计时器归零时{canMove = true; // 允许移动canFlip = true; // 允许翻转}}//...}// 检查周围环境的函数private void CheckSurroundings(){//...// 使用射线探测是否碰到台阶isTouchingLedge = Physics2D.Raycast(ledgeCheck.position, transform.right, wallCheckDistance, whatIsGround);if (isTouchingWall && !isTouchingLedge && !ledgeDetected){ledgeDetected = true;ledgePosBot = wallCheck.position; // 记录台阶底部位置}}// 检查是否正在墙壁滑动的函数private void CheckIfWallSliding(){// 如果贴着墙壁、水平输入方向与朝向一致、垂直速度小于0且不能攀爬台阶if (isTouchingWall && movementInputDirection == facingDirection && rigidbody2D.velocity.y<0 && !canClimbLedge)//...}// 检查攀爬台阶的函数private void CheckLedgeClimb(){// 如果探测到台阶且不能攀爬台阶if (ledgeDetected && !canClimbLedge){canClimbLedge = true; // 允许攀爬台阶// 根据角色朝向确定台阶顶部位置1和位置2if (isFacingRight){ledgePos1 = new Vector2(Mathf.Floor(ledgePosBot.x + wallCheckDistance) - ledgeClimbXOffset1, Mathf.Floor(ledgePosBot.y) + ledgeClimbYOffset1);ledgePos2 = new Vector2(Mathf.Floor(ledgePosBot.x + wallCheckDistance) + ledgeClimbXOffset2, Mathf.Floor(ledgePosBot.y) + ledgeClimbYOffset2);}else{ledgePos1 = new Vector2(Mathf.Ceil(ledgePosBot.x - wallCheckDistance) + ledgeClimbXOffset1, Mathf.Floor(ledgePosBot.y) + ledgeClimbYOffset1);ledgePos2 = new Vector2(Mathf.Ceil(ledgePosBot.x - wallCheckDistance) - ledgeClimbXOffset2, Mathf.Floor(ledgePosBot.y) + ledgeClimbYOffset2);}canMove = false; // 禁止移动canFlip = false; // 禁止翻转animator.SetBool("canClimbLedge", canClimbLedge); // 设置角色动画状态}if (canClimbLedge){transform.position = ledgePos1; // 攀爬时固定的位置1}}// 攀爬台阶完成后的回调函数public void FinishLedgeClimb(){canClimbLedge = false; // 禁止攀爬台阶transform.position = ledgePos2; // 移动到台阶顶部位置2canMove = true; // 允许移动canFlip = true; // 允许翻转ledgeDetected = false; // 重置台阶探测标志animator.SetBool("canClimbLedge", canClimbLedge); // 设置角色动画状态}
}

Mathf.FloorMathf.Ceil是数值取整

https://blog.csdn.net/weixin_38211198/article/details/90489629。

【3】攀爬动画与添加事件(重复操作)

按照老步骤,自己把图拖拽到Assets中,变成unity的内部资源,然后设置sprite图的属性。
请添加图片描述

对我们的爬墙动画这样切割。
请添加图片描述

创建爬墙的动画状态,需要注意的是我们要自行每帧情况下,编辑碰撞体的变化

请添加图片描述

接着我们设置的动画状态机如下图所示,而can Transition To Self是必须要记得不勾选的:

请添加图片描述

【4】解决遗留Bug

①在滑墙阶段,Player准备跳墙时,动画状态出错
请添加图片描述

DEBUG发现

Player在翻转后,也满足了接触墙面和真正滑墙的情况
请添加图片描述
因此修改方案如下:
请添加图片描述

②攀爬阶段,游戏对象的位置不固定,出现自由下落等状况
请添加图片描述

解决方案:
请添加图片描述

③攀爬阶段,游戏对象穿墙,进入了地图内。

这是因为你没有设置攀爬完成后,玩家角色应该出现的位置。

请添加图片描述
④跳墙的手感非常差!

之所以出现这种情况,是因为我们在跳墙到滑墙阶段,没有强制在空中进行翻转,这个问题后续会在有限状态机中解决。

【5】拓展

不修改动画状态机的各种状态,最快捷修改动画的操作。
请添加图片描述

这里视频就选择把角色移动状态换成了跑步状态,我记得游戏设计中是这样说明的,就是避免玩家觉得场景枯燥和觉得画面卡顿。

Part8:冲刺与残影

【1】原理

玩家的冲刺与残影效果的实现原理如下:

  1. 玩家冲刺效果实现

    • 当玩家按下冲刺按钮时,检查冲刺冷却时间是否已过,并在条件满足时触发冲刺操作。
    • 在冲刺操作中,玩家的速度被设置为一个较高的值,使其在短时间内快速移动。
    • 冲刺过程中,更新玩家位置,同时生成玩家的残影效果,以模拟高速移动的轨迹。
  2. 残影效果实现

    • 创建一个对象池用于管理残影对象,初始时创建一定数量的残影对象。
    • 在玩家冲刺时,从对象池中获取一个残影对象,并将其位置与玩家位置相同,显示在玩家后方。
    • 残影对象的透明度逐渐减小,使得残影逐渐消失,营造出残影效果。
    • 超过一定时间后,将残影对象放回对象池中以供下次使用。
  3. 对象池的作用

    • 对象池是一个预先创建并维护的对象集合,用于减少动态创建和销毁对象的开销。
    • 在需要时,从对象池中获取闲置的对象,避免了频繁的内存分配和回收操作。
    • 对象池能够提高性能,降低资源消耗,并且更加适用于需要频繁创建和销毁的对象,如残影效果

请添加图片描述

【2】C#脚本

PlayerController.cs

public class PlayerController : MonoBehaviour
{//...private bool isDashing;// 是否正在进行冲刺的标志public float dashTime = 0.2f;// 冲刺持续时间public float dashSpeed = 50f;// 冲刺速度public float distanceBetweenImages = 0.1f;// 冲刺图像之间的距离public float dashCoolDown = 0.2f;// 冲刺冷却时间private float dashTimeLeft;// 剩余冲刺时间private float lastImageXpos;// 上一个图像的X坐标位置private float lashDash = -100f;// 上次冲刺的时间private void Update(){//...CheckDash();// 调用检查冲刺的方法}private void CheckInput(){//...if (Input.GetButtonDown("Dash")){// 如果当前时间大于上次冲刺时间加上冲刺冷却时间if (Time.time >= (lashDash + dashCoolDown)){// 尝试执行冲刺操作AttemptToDash();}}}// 尝试进行冲刺的方法private void AttemptToDash(){// 设置正在进行冲刺的标志为真isDashing = true;// 设置剩余冲刺时间为设定的冲刺持续时间dashTimeLeft = dashTime;// 更新上次冲刺时间lashDash = Time.time;// 从玩家残影池中获取一个残影对象并显示PlayerAfterImagePool.Instance.GetFromPool();// 记录当前位置作为上一个图像的X坐标位置lastImageXpos = transform.position.x;}// 检查冲刺状态的方法private void CheckDash(){// 如果正在进行冲刺if (isDashing){// 如果剩余冲刺时间大于0if (dashTimeLeft > 0){// 设置正在移动和翻转的标志为假canMove = false;canFlip = false;// 设置刚体的速度,实现冲刺效果rigidbody2D.velocity = new Vector2(dashSpeed * facingDirection, rigidbody2D.velocity.y);// 减少剩余冲刺时间dashTimeLeft -= Time.deltaTime;// 如果玩家位置移动足够远,生成一个新的残影对象并显示if (Mathf.Abs(transform.position.x - lastImageXpos) > distanceBetweenImages){PlayerAfterImagePool.Instance.GetFromPool();lastImageXpos = transform.position.x;}}// 如果剩余冲刺时间小于等于0,或者玩家触碰到墙壁if (dashTimeLeft <= 0 || isTouchingWall){// 设置正在进行冲刺的标志为假isDashing = false;// 设置正在移动和翻转的标志为真canMove = true;canFlip = true;}}}
}

PlayerAfterImageSprite.cs

在每次玩家冲刺时,将创建一个与玩家精灵相同的残影对象,并在一定时间后将其放回对象池。

public class PlayerAfterImageSprite : MonoBehaviour
{[SerializeField]private float activeTime = 0.1f;// 残影持续时间private float timeActivated;// 激活时间记录private float alpha;// 当前透明度[SerializeField]private float alphaSet = 0.8f;// 初始透明度private float alphaMultiplier = 0.85f;// 透明度衰减系数private Transform player;// 玩家的Transform组件private SpriteRenderer spriteRenderer;// 当前对象的精灵渲染器组件private SpriteRenderer playerSpriteRenderer;// 玩家的精灵渲染器组件private Color color;// 当前颜色// 在启用对象时调用的方法private void OnEnable(){// 获取当前对象的精灵渲染器组件spriteRenderer = GetComponent<SpriteRenderer>();// 查找并获取标签为"Player"的游戏对象的Transform组件player = GameObject.FindGameObjectWithTag("Player").transform;// 获取玩家对象的精灵渲染器组件playerSpriteRenderer = player.GetComponent<SpriteRenderer>();// 设置初始透明度alpha = alphaSet;// 设置当前对象的精灵为与玩家相同的精灵spriteRenderer.sprite = playerSpriteRenderer.sprite;// 设置当前对象的位置与玩家位置相同transform.position = player.position;// 设置当前对象的旋转与玩家旋转相同transform.rotation = player.rotation;// 记录激活时间timeActivated = Time.time;}// 在每一帧更新时调用的方法private void Update(){// 根据透明度衰减系数更新透明度alpha *= alphaMultiplier;// 创建一个新的颜色,其中alpha值衰减color = new Color(1f, 1f, 1f, alpha);// 将新的颜色应用到当前对象的精灵渲染器spriteRenderer.color = color;// 如果当前时间超过了激活时间加上持续时间if (Time.time >= (timeActivated + activeTime)){// 将当前对象添加回对象池PlayerAfterImagePool.Instance.AddToPool(gameObject);}}
}

PlayerAfterImagePool.cs

对象池允许在需要时创建和回收残影对象,以提高性能和资源利用率。

public class PlayerAfterImagePool : MonoBehaviour
{[SerializeField]private GameObject afterImagePrefab;// 残影预制体private Queue<GameObject> availableObjects = new Queue<GameObject>();// 可用的对象队列// 单例模式的静态实例public static PlayerAfterImagePool Instance { get; private set; }// 在脚本实例被唤醒时调用的方法private void Awake(){// 设置单例实例为当前脚本实例Instance = this;// 初始化对象池GrowPool();}// 扩大对象池的方法private void GrowPool(){// 循环创建一定数量的残影实例,并添加到对象池中for (int i = 0; i < 10; i++){// 实例化一个残影预制体var instanceToAdd = Instantiate(afterImagePrefab);// 将实例的父对象设置为当前对象池instanceToAdd.transform.SetParent(transform);// 将实例添加到对象池中AddToPool(instanceToAdd);}}// 将对象添加到对象池的方法public void AddToPool(GameObject instance){// 将实例设为非激活状态instance.SetActive(false);// 将实例加入可用对象队列availableObjects.Enqueue(instance);}// 从对象池获取对象的方法public GameObject GetFromPool(){// 如果可用对象队列为空,扩大对象池if (availableObjects.Count == 0){GrowPool();}// 从可用对象队列中取出一个实例var instance = availableObjects.Dequeue();// 将实例设为激活状态instance.SetActive(true);// 返回获取到的实例return instance;}
}

【3】unity操作

首先是创建两个游戏对象,一个将作为预制体的AfterImage和存放这些预制体的PlayerAfterImagePool,需要注意的是,它们都需要清除Z轴的影响,另外不要多此一举去给残影添加名为PlayerTag,不然后续的打包后的游戏存在问题。

请添加图片描述
然后如下设置AfterImage,具体 有什么用,就看Unity中SortingLayer、Order in Layer和RenderQueue的讲解
请添加图片描述

之后的就把AfterImage拖拽到Asset文件夹里面,就能变成预制体了。
请添加图片描述
接着我们可以在PlayerAfterImagePool复用这个预制体。
请添加图片描述

不要忘记给Player游戏对象添加上Tag
请添加图片描述

接着就是到ProjectSettings里添加一个冲刺的操作按钮
请添加图片描述

按钮就如下图这样设置。
请添加图片描述

【4】拓展

①打包成游戏后,该如何显示游戏的Bug?
请添加图片描述

第2章:基本战斗

不要拘泥于代码,因为后续都会被删掉,

part9:战斗连击

【1】C#脚本

PlayerController.cs

添加了可以公开玩家能不能翻转的属性事件,这将会作用于后面的动画中。

	public void DisableFlip(){canFlip = false;}public void EnableFlip(){canFlip = true;}

PlayerCombatController.cs

  1. 输入检测:Update函数中,通过CheckCombatInput函数检测用户的输入,如果点击鼠标左键,战斗功能启用,就会标记为获取到输入,并记录输入的时间。
  2. 攻击触发:CheckAttacks函数中,如果获取到了输入且当前不在攻击状态,则将攻击状态设置为true,并根据攻击状态切换动画参数。
  3. 攻击命中框检测:CheckAttackHitBox函数中,通过Physics2D.OverlapCircleAll函数检测攻击命中框范围内的所有碰撞器(后续会为小怪添加)。
  4. 伤害应用:CheckAttackHitBox函数中,对于每个检测到的碰撞器,通过调用其父对象的SendMessage函数发送名为"Damage"的消息,同时传递了攻击伤害值。注意这里需要保证小怪有Damage函数,才能实现,不然会抱错。
  5. 攻击动画结束:FinishAttack1函数中,当攻击动画完成后,将攻击状态设置为false,并恢复动画参数,以便继续下一次攻击。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class PlayerCombatController : MonoBehaviour
{[SerializeField]private bool combatEnabled = true;  // 是否启用战斗控制[SerializeField]private float inputTimer = 0.2f, attack1Radius = 0.8f, attack1Damage = 10f;  // 输入计时器、攻击半径、攻击伤害[SerializeField]private Transform attack1HitBoxPos;  // 攻击命中框的位置[SerializeField]private LayerMask whatIsDamageable;  // 可造成伤害的层级掩码private bool gotInput, isAttacking, isFirstAttack;  // 是否获取到输入、是否正在攻击、是否是第一次攻击private float lastInputTime = Mathf.NegativeInfinity;  // 上次输入的时间private Animator animator;  // 动画控制器private void Start(){animator = GetComponent<Animator>();  // 获取角色动画控制器组件animator.SetBool("canAttack", combatEnabled);  // 设置动画参数"canAttack"为combatEnabled的值}private void Update(){CheckCombatInput();  // 检查战斗输入CheckAttacks();  // 检查攻击}private void CheckCombatInput(){if (Input.GetMouseButtonDown(0))  // 当鼠标左键点击时{if (combatEnabled)  // 如果允许战斗{gotInput = true;  // 标记为获取到输入lastInputTime = Time.time;  // 记录输入的时间}}}private void CheckAttacks(){if (gotInput){if (!isAttacking){gotInput = false;  // 重置输入标记isAttacking = true;  // 标记为正在攻击isFirstAttack = !isFirstAttack;  // 切换攻击状态animator.SetBool("attack1", true);  // 设置动画参数"attack1"为true,触发攻击动画animator.SetBool("firstAttack", isFirstAttack);  // 设置动画参数"firstAttack"为isFirstAttack的值animator.SetBool("isAttacking", isAttacking);  // 设置动画参数"isAttacking"为true}}if (Time.time >= lastInputTime + inputTimer)  // 如果距离上次输入的时间超过了输入计时器{gotInput = false;  // 重置输入标记}}private void CheckAttackHitBox(){Collider2D[] detectedObjects = Physics2D.OverlapCircleAll(attack1HitBoxPos.position, attack1Radius, whatIsDamageable);  // 检测攻击命中框范围内的所有碰撞器foreach (Collider2D collider2D in detectedObjects){collider2D.transform.parent.SendMessage("Damage", attack1Damage);  // 向碰撞器的父对象发送"Damage"消息,对其他存在Damge函数的游戏对象造成攻击伤害}}private void FinishAttack1(){isAttacking = false;  // 结束攻击状态animator.SetBool("isAttacking", isAttacking);  // 设置动画参数"isAttacking"为falseanimator.SetBool("attack1", false);  // 设置动画参数"attack1"为false,结束攻击动画}private void OnDrawGizmos(){Gizmos.DrawWireSphere(attack1HitBoxPos.position, attack1Radius);  // 在攻击命中框的位置绘制一个表示攻击范围的球体框架}
}

【2】连击动画

导入素材,分别是第一次攻击和第二次攻击动画,还有后面要实验的稻草人。
请添加图片描述

老样子,按照之前那样设置精灵图,就不再赘述了。先暂时不管稻草人先,自行到Player创建对应的两个攻击动画状态Attack1_1Attack1_2
请添加图片描述

然后我们的一个动画状态机则这样设置。
请添加图片描述

接着完成最后一步,绘制攻击的命中框,自行根据动画的表现,调整它的一个出现位置,如果觉得不麻烦,还可以自己允许关键帧操作,自己修改Player碰撞体的一个变化,下图则是放入Player游戏对象的组件设置。

自己看需要,对动画关键帧处添加事件,什么情况是不能转方向的?攻击要标记,要有攻击完成等等。

请添加图片描述

part10:怎么实现稻草人的生命周期动画

【1】C#脚本

CombatDummyController.cs
ParticleController.cs

【2】unity操作

重新修订图层和级数,新增敌人标签,结果如下:
请添加图片描述

首先是对Player游戏对象修改Layer,要避免子对象都成为了Player,毕竟它们只是用来当射线检测我们的场景的。
请添加图片描述

然后不要忘记设置那些是他可以破坏的对象。
请添加图片描述

projectsetting上这样设置,根据文档Physics 2D - Unity 手册的内容,可知我们就是选择不让物体被破坏时或者已经死亡的情况下,与其他层级因为游戏对象的碰撞体发生交互。
请添加图片描述

【3】稻草人

生成稻草人精灵图的操作就按照之前那样操作即可,创建稻草人这个游戏对象,通过如下方式设计它的一个生命周期。
请添加图片描述

CombatDummy的组件
请添加图片描述

Alive的组件,另外它的AnimationIdle状态,受击往左移动和受击往右移动都自行使用unityAnimation实现,这里就不啰嗦了。

请添加图片描述

Broken Top的组件
请添加图片描述

Broken Bottom的组件
请添加图片描述

最后我们稻草人的一个动画状态机如下图这样设置。
请添加图片描述

【4】预制体

制作打击特效也是一样的方法步骤,只是要变成预制体存放到专门的文件夹,之后要记得删除游戏对象,另外它的动画是需要在最后没有帧播放时,添加事件就选择C#脚本中的FinishAnim(),这样可以避免每次生成打击特效后没有及时清除。
请添加图片描述

part11+part12:死亡特效

自行按照视频的教程进行操作完成特效预制体的设置,懒得写具体的数值了。

小怪的设置应该是这样的。
请添加图片描述
专门分开代码逻辑层与动画表现层

- Enemy1(代码逻辑层)
-- Alive(负责动画表现、物理特性等属性)

请添加图片描述

第3章~第5章:改进有限状态机与简化动画状态机

不知道写什么内容好,结合我给的百度网盘链接看视频操作unity吧,我已经贴心地按章打包Asset那块了。

【1】理清思路

编写Enemy AI state machine状态切换图,不要担心自己写的动画状态图过于复杂,在实际的动画状态机的一个判断与转换,都是通过严谨代码逻辑实现了简化,而只要明白继承、接口的含义,就可以知道有限状态机该怎么有效分层。

请添加图片描述

制作一个新状态的流程Xxx是自定义命名的意思,注意命名规范

我感觉这很像MVC设计模式。

  1. 创建一个继承状态的特定状态类:XxxState:State,负责作为表现层与逻辑层之间的一个桥梁,比如对应的代码逻辑有着对应的动画表现。
  2. 创建状态数据类 D_XxxState,专门负责修改数值
  3. 创建敌人特定状态 E1_XxxState,专门
  4. 对敌人类声明状态
  5. 设置动画机

VS实现父子继承操作的快捷键

比如下面的移动状态继承状态这个父类,
请添加图片描述
重写函数也是
请添加图片描述

FSM怎么方便操作的

通过以上的步骤,我们就可以简化小怪的操作,更专注于动画表现与代码逻辑之间的关系。
请添加图片描述
第5章后续跟如下的不同之处,在于通过接口的方式进一步减低代码的耦合度。
请添加图片描述

可以自行设置数值
请添加图片描述

还有Input System怎么用,也自行看视频。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Quartz任务调度框架介绍和使用

一、Quartz介绍 Quartz [kwɔːts] 是OpenSymphony开源组织在Job scheduling领域又一个开源项目&#xff0c;完全由Java开发&#xff0c;可以用来执行定时任务&#xff0c;类似于java.util.Timer。但是相较于Timer&#xff0c; Quartz增加了很多功能&#xff1a; 1.持久性作业 …

PDF怎么转Word?8 个最佳 PDF 转 Word 转换器

PDF 转 Word 转换工具只是一个特殊程序&#xff0c;可以将 PDF&#xff08;本机和/或扫描&#xff09;转换为 Microsoft Office Word 格式。将 PDF 导出到 Word 的主要原因之一是满足可编辑文档的需求&#xff0c;尽管还有其他原因。 由于缺少 PDF 阅读器&#xff0c;您可以选…

AutoDev 1.1.3 登场,个性化 AI 辅助:私有化大模型、自主设计 prompt、定义独特规则...

在过去的半个月里&#xff0c;我们为开源辅助编程工具 AutoDev 添加了更强大的自定义能力&#xff0c;现在你可以&#xff1a; 使用自己部署的开源大模型自己配置 Intellij IDEA 中的行为自定义开发过程中的规范 当然了&#xff0c;如果您自身拥有开发能力的话&#xff0c;建议…

fastgpt构建镜像

1.把client目录复制到服务器 .next和node_modules文件夹不用上传到服务器 在服务器目录运行 docker build -t fastgpt:1.0.3 . 构建服务 再运行 docker ps 就可以看到容器了

数据结构(2)

冒泡排序&#xff1a; 1.比较相邻的两个元素。如果前一个元素比后一个元素大&#xff0c;则交换两者位置。 2.对每一对相邻元素做相同工作&#xff0c;从第一对元素到最后一对元素&#xff0c;最后的一个元素就是最大的元素。 for(int ia.length-1;i>0;i--){for (int j 0…

VS2019+Qt5.15.2 编译 QtWebEngine(带音视频解码)

前言 QtWebEngine 是 Qt 框架的一部分&#xff0c;用于构建现代 Web 浏览器功能。本篇教程将向您展示如何在 Visual Studio 2019 中编译 QtWebEngine 5.15.2 源码&#xff0c;并配置以支持音视频解码功能。 准备工作 1、源码下载 2、源码修改&#xff0c;参考Qt Code Review…

微信小程序纯前端从阿里云OSS下载json数据-完整版

起因 因为云开发开始收费(貌似很久了),准备改造在以前的小程序,数据转到oss上,小程序使用原生,不算专业领域, 所以先百度.... 网上的教程真的是千篇一律,大部分开局就是require(ali-oss); 好点的npm install ali-oss --save开局,拼凑操作到最后发现要用云开发,因为云…

HTTPS代理搭建技巧分享

今天我们来分享一下如何搭建一个能够实现中间人 检测和防护的HTTPS代理。保护我们的网络通信安全是至关重要的&#xff0c;让我们一起学习如何构建一个安全可靠的HTTPS代理吧&#xff01; 什么是中间人 &#xff1f; 首先&#xff0c;让我们来了解一下什么是中间人 。中间人 是…

RK3399平台开发系列讲解(内核调试篇)内核中内存泄漏的调试

🚀返回专栏总目录 文章目录 一、Linux 内核内存泄漏二、如何观测内核内存泄漏?三、kmemleak 工具沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 内核内存泄漏往往都会是很严重的问题,那么,我们该如何判断内存泄漏是否是内核导致的呢? 一、Linux 内核内存泄漏 …

GPT-3.5——从 人工智障 到 大人工智障

有人说&#xff0c;GPT是从人工智障到人工智能的蜕变&#xff0c;但是。。。 我认为&#xff0c;GPT是从 人工智障 到 大人工智障 的退化。。。 从 人工智障 到 大人工智障 GPT-3.5学术介绍No.1---- 西红柿炒钢丝球基本信息详细制作方法材料步骤 幕后花絮 No.2---- 顶尖数学家…

【数据分析入门】Jupyter Notebook

目录 一、保存/加载二、适用多种编程语言三、编写代码与文本3.1 编辑单元格3.2 插入单元格3.3 运行单元格3.4 查看单元格 四、Widgets五、帮助 Jupyter Notebook是基于网页的用于交互计算的应用程序。其可被应用于全过程计算&#xff1a;开发、文档编写、运行代码和展示结果。 …

自动驾驶合成数据科普一:不做真实数据的“颠覆者”,做“杠杆”

前言&#xff1a; 在7月底的一篇文章中&#xff0c;九章智驾提到&#xff0c;数据闭环能力是自动驾驶下半场的“入场券”&#xff0c;这一观点在行业内引起了广泛共鸣。 在数据闭环体系中&#xff0c;仿真技术无疑是非常关键的一环。仿真的起点是数据&#xff0c;而数据又分为真…

基于MATLAB开发AUTOSAR软件应用层Code mapping专题-part 3 Paramter标签页介绍

这页是参数设置的界面,那首先要知道什么是参数,参数就是算法中的系数这些可以更改的变量,接下来就是要学习如何创建参数,如下图: 打开模型资源管理器 选择model Workspace标签,点击上边工具栏里的创建参数的按钮(红色箭头指向的按钮),添加一个新的参数K,值设置为4,数…

linux部署clickhouse(单机)

一、下载安装 1.1、下载地址 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区阿里巴巴开源镜像站&#xff0c;免费提供Linux镜像下载服务&#xff0c;拥有Ubuntu、CentOS、Deepin、MongoDB、Apache、Maven、Composer等多种开源软件镜像源&#xff0c;此外还提供域名解析DNS、…

opencv进阶14-Harris角点检测-cv2.cornerHarris

类似于人的眼睛和大脑&#xff0c;OpenCV可以检测图像的主要特征并将这 些特征提取到所谓的图像描述符中。然后&#xff0c;可以将这些特征作为数据 库&#xff0c;支持基于图像的搜索。此外&#xff0c;我们可以使用关键点将图像拼接起 来&#xff0c;组成更大的图像。&#x…

软件开发bug问题跟踪与管理

一、Redmine 项目管理和缺陷跟踪工具 官网&#xff1a;https://www.redmine.org/ Redmine 是一个开源的、基于 Web 的项目管理和缺陷跟踪工具。它用日历和甘特图辅助项目及进度可视化显示&#xff0c;同时它又支持多项目管理。Redmine 是一个自由开源软件解决方案&#xff0c;…

通过DBeaver 给Postgre SQL表 设置主键自增

1.创建表 CREATE TABLE public.company ( id int4 NOT NULL , name text NOT NULL, age int4 NOT NULL, address bpchar(50) NULL, salary float4 NULL, join_date date NULL, CONSTRAINT company_pkey PRIMARY KEY (id) ); 2.插入数据&#xff08;不传入id&#xff…

机器学习:什么是分类/回归/聚类/降维/决策

目录 学习模式分为三大类&#xff1a;监督&#xff0c;无监督&#xff0c;强化学习 监督学习基本问题 分类问题 回归问题 无监督学习基本问题 聚类问题 降维问题 强化学习基本问题 决策问题 如何选择合适的算法 我们将涵盖目前「五大」最常见机器学习任务&#xff1a…

Android学习之路(7) Frament

Fragment 表示应用界面中可重复使用的一部分。fragment 定义和管理自己的布局&#xff0c;具有自己的生命周期&#xff0c;并且可以处理自己的输入事件。fragment 不能独立存在。它们必须由 activity 或其他 fragment 托管。fragment 的视图层次结构会成为宿主的视图层次结构的…

人事变动?前沃尔沃汽车大中华区总裁钦培吉将加盟吉利

根据消息&#xff0c;吉利控股集团高级副总裁杨学良在今天上午通过微博宣布&#xff0c;前沃尔沃汽车大中华区总裁钦培吉将加盟吉利。钦培吉将担任吉利汽车集团销售公司副总经理&#xff0c;并负责集团渠道发展委员会的主任一职&#xff0c;向吉利汽车集团的高级副总裁林杰报告…