輸入緩衝與土狼時間的討論與實現

Fe發表於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/

相關文章