输入缓冲与土狼时间的讨论与实现

作者:Fe indienova 2021-09-23
个人学习积累中,如有任何问题与错误,欢迎指出与讨论。

这系列将会记录我在搭建自己的 2D 平台游戏时遇到的一些问题与解决方案,核心目的均为更好的游戏体验与更棒的代码逻辑结构。所有代码基于 C# 与 Unity。


跳跃的手感能衡量一个 2D 平台游戏的好坏。——鲁迅

不知道你是处理玩家跳跃的判断条件的?反正就我而言,射线或者子物体检测地面图层:如果角色在地面上,则允许跳跃;反之则不允许。

但是这样在游玩的时候会导致一个问题:当你想要连跳时,单按跳跃键,你以为自己已经落到了地面,而实际上,你还在空中,从而造成了“按键失灵”的问题。这对于玩家的游玩体验有着相当大的影响。

而解决这个问题的方法,就是允许指令的预输入,在预输入后的一段时间内,若检测到条件满足,再执行操作——即“输入缓冲”。

不过,在介绍输入缓冲的方法前,我们先来了解一下计时器。

计时器

计时器,顾名思义,是为了计算一段时间,当计时器到达设定条件后,会执行相应的操作。

Unity 提供了一个类似的方法,

  1. Invoke("方法名(无参), 延迟时间")
复制代码

或者

  1. InvokeRepeating("方法名(无参), 延迟时间, 间隔时间")
复制代码

用于重复调用。但是限制较多,且不适用于我们的输入缓冲:它只能做到延迟调用,而不能在延迟的这段时间内一满足条件就调用。

另外还可以在协程中使用

  1. yield return new WaitForSeconds(具体秒数);
复制代码

等方法实现。同样的问题是,它也只能实现延迟调用。

那么,我们到底该怎么定义一个可用于输入缓冲的计时器呢?以下是个人常用的一种写法。

  1. <p>// 所用变量</p><p>private float timer;           // 计时器</p><p>private float timer_max = 2f;  // 限定时间</p><p>
  2. </p><p>// 初始化,一般在按下按键时执行,实现预输入</p><p>timer = timer_max;</p><p>
  3. </p><p>// 计时过程,一般放在 Update 里,每帧调用</p><p>if (timer != 0)</p><p>{</p><p>  timer -= Time.deltaTime;</p><p>  if (timer <= 0)</p><p>  {</p><p>    timer = 0;</p><p>    /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */</p><p>  }</p><p>  else</p><p>  {</p><p>    /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */</p><p>  }</p><p>}</p>
复制代码

主要思路就是利用Time.deltaTime来计算并减去时间,关于增量时间,这里有一篇不错的文章(https://blog.csdn.net/ChinarCSDN/article/details/82914420),就不再赘述。

那么,接下来,利用这个计时器,实现“输入缓冲”效果吧。

输入缓冲

让我们再明确下,我们想要随时能够输入跳跃指令,并让这个指令在内存中保存一定时间,在该段时间内只要满足条件(接触地面)就执行跳跃指令。以下是两种执行写法(第一种为我游戏中使用 / 第二种为在上方计时器模板上进行修改):

  1. <p>/* 所用变量 */</p><p>private float buffer_jump_counter = 0;    // 跳跃输入缓冲计数器</p><p>private float buffer_jump_max = 0.1f;     // 跳跃输入缓冲最大值</p><p>private bool hasJumpForce;            // 此时是否拥有跳跃力了,避免重复给跳跃力,该力会在接触地面后自动重置为 false</p><p>
  2. </p><p>/* 输入指令,Update()中 */</p><p>if (Input.GetButtonDown("Jump"))</p><p>{</p><p>  buffer_jump_counter = 0;</p><p>}</p><p>
  3. </p><p>/* 计时器与执行指令,Update()中 */</p><p>if (buffer_jump_counter < buffer_jump_max)</p><p>{</p><p>  buffer_jump_counter += (1 * Time.deltaTime);</p><p>  if (IsOnGround() && !hasJumpForce)</p><p>  {</p><p>    hasJumpForce = true;</p><p>
  4. </p><p>    //具体施加跳跃力操作</p><p>    rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);</p><p>    Debug.Log("输入缓冲,启动一次!");</p><p>  }</p><p>}</p>
复制代码

下面这种我未在游戏中测试过,不保证正确性。

  1. <p>/* 所用变量一致,不再赘述 */</p><p>
  2. </p><p>/* 输入指令,Update()中 */</p><p>buffer_jump_counter = buffer_jump_max;</p><p>
  3. </p><p>/* 计时器与执行指令,Update()中 */</p><p>if (buffer_jump_counter != 0)</p><p>{</p><p>  buffer_jump_counter -= Time.deltaTime;</p><p>  if (buffer_jump_counter <= 0)</p><p>  {</p><p>    buffer_jump_counter = 0;</p><p>    /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */</p><p>  }</p><p>  else</p><p>  {</p><p>    /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */</p><p>    if (IsOnGround() && !hasJumpForce)</p><p>    {</p><p>      hasJumpForce = true;</p><p>
  4. </p><p>      //具体施加跳跃力操作</p><p>      rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);</p><p>      Debug.Log("输入缓冲,启动一次!");</p><p>    }</p><p>  }</p><p>}</p>
复制代码

这样,我们就实现了输入缓冲的效果。输入缓冲还可以用在很多的地方,如游戏中在空中连续多次按下↓方向键实现砸击地面的效果......更多的用法,就留待各位自行尝试了。

除此之外,跳跃的输入缓冲还有一个好兄弟,“土狼时间”。

土狼时间

土狼时间,就是让玩家所操控的人物,能够在离开平台的一段时间内,仍能执行起跳操作。它的目的,也是优化操作,减少“操作失灵”的现象。那么,我们是不是也可以用个计时器,来实现呢?可以自己先想一想。

怎么样,有思路了吗?

我们只要把计时器启动的时间改为离开地面即可,当我们离开地面,又没有执行过跳跃,就可以在一定的时间内,执行跳跃指令。以下是两种执行方法(同样,第一种为我游戏中使用 / 第二种修改自计时器模板):

  1. <p>/* 所用变量 */</p><p>private float buffer_coyote_counter = 0;    // 跳跃土狼时间计数器</p><p>private float buffer_coyote_max = 0.1f;       // 跳跃土狼时间最大值</p><p>private bool hasJumpForce;              // 此时是否拥有跳跃力了,避免重复给跳跃力</p><p>
  2. </p><p>/* 初始化,在 Start()中 */</p><p>buffer_coyote_counter = buffer_coyote_max;</p><p>
  3. </p><p>/* 更新指令,该函数在 Update()中调用 */</p><p>void CheckForJump()</p><p>{</p><p>  if (IsOnGround() && rigidbody2D_Role.velocity.y < 0.05f && rigidbody2D_Role.velocity.y > -0.05f)</p><p>  {</p><p>    hasJumpForce = false;</p><p>    buffer_coyote_counter = 0;</p><p>  }</p><p>}</p><p>
  4. </p><p>/* 计时器与执行指令,Update()中 */</p><p>if (buffer_coyote_counter < buffer_coyote_max)</p><p>{</p><p>  if (!hasJumpForce && Input.GetButtonDown("Jump"))</p><p>  {</p><p>    hasJumpForce = true;</p><p>    buffer_coyote_counter = buffer_coyote_max;</p><p>    rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);</p><p>    Debug.Log("土狼时间,启动一次!");</p><p>  }</p><p>}</p><p>
  5. </p><p>if (buffer_coyote_counter < buffer_coyote_max)</p><p>  buffer_coyote_counter += Time.deltaTime;</p>
复制代码

下面这种我未在游戏中测试过,不保证正确性 * 2。

  1. <p>/* 所用变量一致,不再赘述 */</p><p>
  2. </p><p>/* 更新指令,该函数在 Update()中调用 */</p><p>void CheckForJump()</p><p>{</p><p>  if (IsOnGround() && rigidbody2D_Role.velocity.y < 0.05f && rigidbody2D_Role.velocity.y > -0.05f)</p><p>  {</p><p>    hasJumpForce = false;</p><p>    buffer_coyote_counter = buffer_coyote_max;</p><p>  }</p><p>}</p><p>
  3. </p><p>/* 计时器与执行指令,Update()中 */</p><p>if (buffer_coyote_counter != 0)</p><p>{</p><p>  buffer_coyote_counter -= Time.deltaTime;</p><p>  if (buffer_coyote_counter <= 0)</p><p>  {</p><p>    buffer_coyote_counter = 0;</p><p>    /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */</p><p>  }</p><p>  else</p><p>  {</p><p>    /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */</p><p>    if (!hasJumpForce && Input.GetButtonDown("Jump"))</p><p>    {</p><p>      hasJumpForce = true;</p><p>      buffer_coyote_counter = buffer_coyote_max;</p><p>      rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse);</p><p>      Debug.Log("土狼时间,启动一次!");</p><p>    }</p><p>  }</p><p>}</p>
复制代码

怎么样?这样就完美了吧。

其实关于游戏中的跳跃,还有很多的学问,例如如何合理高效的处理跳跃各个状态的动画(起跳、上升、最高点、下落、落地),跳跃中额外力的施加(如马里奥中的跳跃上升慢,下降快,并不只受到重力影响)......

其他的内容,就下次再说吧!

后记

我在学习本文相关内容时,借鉴了不少帖子、视频,包括但不限于:

译文|Gamemaker Studio 系列:2D 平台游戏的输入缓冲 ——highway★(https://indienova.com/indie-game-development/2d-platformer-input-buffering-design/)
使用 Unity 实现动作游戏的打击感 —— 奥飒姆 _Awesome(https://www.bilibili.com/video/BV1fX4y1G7tv)


来源:indienova
原文:https://indienova.com/indie-game-development/input-buffering-and-coyote-time/

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多