输入缓冲与土狼时间的实现|Unity2D学习日记(一)

引言

个人学习积累中,如有任何问题与错误,欢迎指出与讨论。

这系列将会记录我在搭建自己的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来计算并减去时间,关于增量时间,这里有一篇不错的文章,就不再赘述。

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

输入缓冲

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

/* 所用变量 */
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★

使用Unity实现动作游戏的打击感 —— 奥飒姆_Awesome