引言
个人学习积累中,如有任何问题与错误,欢迎指出与讨论。
这系列将会记录我在搭建自己的 2D 平台游戏时遇到的一些问题与解决方案,核心目的均为更好的游戏体验与更棒的代码逻辑结构。所有代码基于 C# 与 Unity。
跳跃的手感能衡量一个 2D 平台游戏的好坏。——鲁迅
不知道你是处理玩家跳跃的判断条件的?反正就我而言,射线或者子物体检测地面图层:如果角色在地面上,则允许跳跃;反之则不允许。
但是这样在游玩的时候会导致一个问题:当你想要连跳时,单按跳跃键,你以为自己已经落到了地面,而实际上,你还在空中,从而造成了“按键失灵”的问题。这对于玩家的游玩体验有着相当大的影响。
而解决这个问题的方法,就是允许指令的预输入,在预输入后的一段时间内,若检测到条件满足,再执行操作——即“输入缓冲”。
不过,在介绍输入缓冲的方法前,我们先来了解一下计时器。
计时器
计时器,顾名思义,是为了计算一段时间,当计时器到达设定条件后,会执行相应的操作。
Unity 提供了一个类似的方法,
Invoke("方法名(无参), 延迟时间")
或者
InvokeRepeating("方法名(无参), 延迟时间, 间隔时间")
用于重复调用。但是限制较多,且不适用于我们的输入缓冲:它只能做到延迟调用,而不能在延迟的这段时间内一满足条件就调用。
另外还可以在协程中使用
yield return new WaitForSeconds(具体秒数);
等方法实现。同样的问题是,它也只能实现延迟调用。
那么,我们到底该怎么定义一个可用于输入缓冲的计时器呢?以下是个人常用的一种写法。
// 所用变量 private float timer; // 计时器 private float timer_max = 2f; // 限定时间 // 初始化,一般在按下按键时执行,实现预输入 timer = timer_max; // 计时过程,一般放在 Update 里,每帧调用 if (timer != 0) { timer -= Time.deltaTime; if (timer <= 0) { timer = 0; /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */ } else { /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */ } }
主要思路就是利用Time.deltaTime来计算并减去时间,关于增量时间,这里有一篇不错的文章(https://blog.csdn.net/ChinarCSDN/article/details/82914420),就不再赘述。
那么,接下来,利用这个计时器,实现“输入缓冲”效果吧。
输入缓冲
让我们再明确下,我们想要随时能够输入跳跃指令,并让这个指令在内存中保存一定时间,在该段时间内只要满足条件(接触地面)就执行跳跃指令。以下是两种执行写法(第一种为我游戏中使用 / 第二种为在上方计时器模板上进行修改):
/* 所用变量 */ private float buffer_jump_counter = 0; // 跳跃输入缓冲计数器 private float buffer_jump_max = 0.1f; // 跳跃输入缓冲最大值 private bool hasJumpForce; // 此时是否拥有跳跃力了,避免重复给跳跃力,该力会在接触地面后自动重置为 false /* 输入指令,Update()中 */ if (Input.GetButtonDown("Jump")) { buffer_jump_counter = 0; } /* 计时器与执行指令,Update()中 */ if (buffer_jump_counter < buffer_jump_max) { buffer_jump_counter += (1 * Time.deltaTime); if (IsOnGround() && !hasJumpForce) { hasJumpForce = true; //具体施加跳跃力操作 rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse); Debug.Log("输入缓冲,启动一次!"); } }
下面这种我未在游戏中测试过,不保证正确性。
/* 所用变量一致,不再赘述 */ /* 输入指令,Update()中 */ buffer_jump_counter = buffer_jump_max; /* 计时器与执行指令,Update()中 */ if (buffer_jump_counter != 0) { buffer_jump_counter -= Time.deltaTime; if (buffer_jump_counter <= 0) { buffer_jump_counter = 0; /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */ } else { /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */ if (IsOnGround() && !hasJumpForce) { hasJumpForce = true; //具体施加跳跃力操作 rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse); Debug.Log("输入缓冲,启动一次!"); } } }
这样,我们就实现了输入缓冲的效果。输入缓冲还可以用在很多的地方,如游戏中在空中连续多次按下↓方向键实现砸击地面的效果......更多的用法,就留待各位自行尝试了。
除此之外,跳跃的输入缓冲还有一个好兄弟,“土狼时间”。
土狼时间
土狼时间,就是让玩家所操控的人物,能够在离开平台的一段时间内,仍能执行起跳操作。它的目的,也是优化操作,减少“操作失灵”的现象。那么,我们是不是也可以用个计时器,来实现呢?可以自己先想一想。
怎么样,有思路了吗?
我们只要把计时器启动的时间改为离开地面即可,当我们离开地面,又没有执行过跳跃,就可以在一定的时间内,执行跳跃指令。以下是两种执行方法(同样,第一种为我游戏中使用 / 第二种修改自计时器模板):
/* 所用变量 */ private float buffer_coyote_counter = 0; // 跳跃土狼时间计数器 private float buffer_coyote_max = 0.1f; // 跳跃土狼时间最大值 private bool hasJumpForce; // 此时是否拥有跳跃力了,避免重复给跳跃力 /* 初始化,在 Start()中 */ buffer_coyote_counter = buffer_coyote_max; /* 更新指令,该函数在 Update()中调用 */ void CheckForJump() { if (IsOnGround() && rigidbody2D_Role.velocity.y < 0.05f && rigidbody2D_Role.velocity.y > -0.05f) { hasJumpForce = false; buffer_coyote_counter = 0; } } /* 计时器与执行指令,Update()中 */ if (buffer_coyote_counter < buffer_coyote_max) { if (!hasJumpForce && Input.GetButtonDown("Jump")) { hasJumpForce = true; buffer_coyote_counter = buffer_coyote_max; rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse); Debug.Log("土狼时间,启动一次!"); } } if (buffer_coyote_counter < buffer_coyote_max) buffer_coyote_counter += Time.deltaTime;
下面这种我未在游戏中测试过,不保证正确性 * 2。
/* 所用变量一致,不再赘述 */ /* 更新指令,该函数在 Update()中调用 */ void CheckForJump() { if (IsOnGround() && rigidbody2D_Role.velocity.y < 0.05f && rigidbody2D_Role.velocity.y > -0.05f) { hasJumpForce = false; buffer_coyote_counter = buffer_coyote_max; } } /* 计时器与执行指令,Update()中 */ if (buffer_coyote_counter != 0) { buffer_coyote_counter -= Time.deltaTime; if (buffer_coyote_counter <= 0) { buffer_coyote_counter = 0; /* 计时器到点结束执行的内容,超出限定时间,类似于延迟执行的部分 */ } else { /* 计时器还在计算时的内容,在限定时间内,输入缓冲就可以放在这 */ if (!hasJumpForce && Input.GetButtonDown("Jump")) { hasJumpForce = true; buffer_coyote_counter = buffer_coyote_max; rigidbody2D_Role.AddForce(new Vector2(0f, jumpForce), ForceMode2D.Impulse); Debug.Log("土狼时间,启动一次!"); } } }
怎么样?这样就完美了吧。
其实关于游戏中的跳跃,还有很多的学问,例如如何合理高效的处理跳跃各个状态的动画(起跳、上升、最高点、下落、落地),跳跃中额外力的施加(如马里奥中的跳跃上升慢,下降快,并不只受到重力影响)......
其他的内容,就下次再说吧!
后记
我在学习本文相关内容时,借鉴了不少帖子、视频,包括但不限于:
- 译文|Gamemaker Studio 系列:2D 平台游戏的输入缓冲 ——highway★(https://indienova.com/indie-game-development/2d-platformer-input-buffering-design/)
- 使用 Unity 实现动作游戏的打击感 —— 奥飒姆 _Awesome(https://www.bilibili.com/video/BV1fX4y1G7tv)
题图来源:youtube(有修改)
我感觉这个操作没必要,直接把触发范围扩大(比如把人物与地面的碰撞,交给附加在人物脚底下延伸了一小段距离的碰撞盒)就好了。
最近由 OxyPlus 修改于:2021-09-21 12:50:57倒不是说不行,只是觉得没必要专门设计一套东西。
@OxyPlus:触发范围扩大的话,我当时到没想过,按这个思路的话,触发器要多个:一个检测是否到达预输入的范围,一个检测是否真正在地面上。单靠拉大原有的一个检测地面的触发器不能实现输入缓冲的延迟输入。文章里的这个方法我认为是输入缓冲的一种通用解法,它不仅限于处理跳跃机制。另外开销也是一块问题,我不清楚Unity的机制,但是简单的计数器应该会比触发器更节省点资源。
@Fe:嗯。想了想,这操作还是有必要。扩大判定范围的办法,在人物移动速度很快或很慢的时候会不好做。
@OxyPlus:嗯,碰撞盒还需要根据时间进行调整,调试时也会伴随着一些物理的不确定性