空洞騎士重現 - 運鏡

空洞騎士
今天的課題是運鏡,人們常說眼睛是靈魂之窗,而在遊戲中鏡頭扮演了同樣的角色,在空洞騎士這種類銀河惡魔城遊戲中,鏡頭是跟角色的移動有關,而角色又是由玩家操作,但玩家並沒有辦法完全控制鏡頭,有時反而是鏡頭的變化來引導玩家,而這個細節在空洞騎士中可謂發揚光大,現在就來學習空洞騎士中最基本的運鏡吧。

重現場景


這次要重現的場景跟上一篇一樣是宮殿廣場,我們可以直接沿用搭好場景,但由於這次鏡頭會移動,所以得考慮到 parallex scrolling,要了解這個現象可以仔細看影片中背景燈柱與前面橋墩的尖刺,會發現隨著鏡頭移動,兩者間的距離會改變甚至重疊,這點符合現實世界中的遠景動得比較慢。
這算是使用 perspective camera 的好處,但沒有設定好適當的 z 軸深度,當鏡頭動起來後就會發現某些東西移動不太同步,這通常是因為我們把東西放太遠然後直接把尺寸放大彌補,靜止時看不出來,但一動起來就很突兀。遇到時再調整就好,至於本次要重現的運鏡,包含兩種: 水平移動、仰望與俯視。

Camera Controller


在介紹要怎樣控制鏡頭前,我強烈推薦可以去看這個介紹影片,內容不僅介紹了 unity 的運鏡系統 (Cinemachine) 而且還是以空洞騎士為主題,真的太合適了XD,不過由於目前我們的場景還算是半開放式,還用不到那麼強大的功能,因此本篇會先用土法煉鋼的方式來處理運鏡!!!
首先從目標影片開頭會發現在角色移動之前,鏡頭就已經展現了影響力,當角色面向左邊或右邊時鏡頭會稍微偏移,以下圖為例會發現當面朝左邊時其實畫面正中央是落在小騎士左側,這讓畫面的左側透漏更多資訊,就彷彿玩家真的向左邊眺望,反之面朝右會讓右側佔有更多空間。
camera when facing left
為了辦到這點,先替 camera 新增一個 script,內容如下,先新增一個 Player 物件變數,然後從 Inspect 上將 Player 物件拖進來,讓我們能夠直接讀取到 Player 座標以及狀態,此外在 Player class 中加上一個 public 變數 IsFacingRight 來方便判定角色面向哪邊,另外可以注意採用這邊採用的是 LateUpdate,這個函數是在 Update 呼叫完後才會執行,也就是確保 Camera 位置的更新是在 Player 任何行動之後:

C#

[SerializeField] Player player; [SerializeField] float SmoothTrans = 5f; [SerializeField] float BiasX = 0.75f; [SerializeField] float BiasY = 1f; void LateUpdate() { Vector3 camPos = transform.position; if ( player.IsFacingRight ) // face to right { camPos.x = player.transform.position.x + BiasX; } else // face to left { camPos.x = player.transform.position.x - BiasX; } camPos.y = player.transform.position.y + BiasY; transform.position = Vector3.Lerp( transform.position, camPos, SmoothTrans * Time.deltaTime ); }
Bias 在 x 軸就根據面向哪邊來調整,至於 y 軸就預設偏高,因為小騎士很矮XD,此外這裡使用 Vector3.Lerp 來線性內插,此函數的功能等於 V = A + (B − A) × t,讓鏡頭的座標慢慢朝預設的位置移動,避免瞬間跳過去造成畫面斷裂感,可以調整 SmoothTrans 來感覺一下效果。
camera when running
同時可以講一下空洞騎士運鏡的另一特點,就是當小騎士在移動時,它的位置會落在畫面正中央,象徵他已經追上鏡頭了,同時這會讓移動的動畫更加清楚,要達成這點其實稍微調整一下 SmoothTrans 即可達成。

重直視角控制


我蠻喜歡的一個細節是當按下上下時,小騎士會有對應的動作,然後過一會鏡頭會移動!!! 讓玩家感覺角色的視野是同步的,要辦到這點可以結合上次學到的 Input System 跟 Animation State Machine,先擴增 animator 的狀態,新增 lookdown、lookup 分別會用到社群提供的 013.LookDown、 014.LookUp sprites。
animator
至於 lookup-back、lookdown-back 則是反向播放的動畫,為了是當玩家放開上下鍵時,讓小騎士把頭轉為原位,至於要如何反向播放可以參考:
  • Speed - 預設通常是 1,修改成負數就會反向播放,設定成 -2 是希望能快點把頭轉回來,我覺得這是 team cherry 設計巧思,當玩家要檢視上下環境時,通常是為了謹慎行事,所以會比較有耐心等動畫播完,但確認完後,就會立刻行動,這時候慢慢等動畫就有點惱人。
  • Cycle Offset - 0~1 的浮點數,這是控制動畫要從哪一幀播放,因為是要反著播,所以設 1 -> 最後一幀。
lookup-back animation setting
為了讓 Player 能接收到向上與向下看的動作,這邊設定了新的 Action - Look,清除掉本來無關的 Binding 並新增 "Up Down" 加上 Interaction - Hold,這是為了透過 hold time 來創造出時間差,讓小騎士先做出動作,過一會才讓鏡頭移動,之後就是新增鍵盤偵測 up、down。

hold 設定參數的 press point 用於輸入要按壓到甚麼程度才算觸發,對於搖桿等輸入裝置比較有意義,hold time 則是觸發多久才會執行。

input system
輸入設定好了之後就針對 Player script 撰寫相關的 event handler,started 是當 press 發生後立刻觸發,而 performed 則是 hold time 過了才觸發,然後兩者會對不同的變數賦值,lookInput 可以用在 Update:

C#

// Player.cs void Awake() { ... controls.Player.Look.started += ctx => lookInput = ctx.ReadValue<Vector2>(); controls.Player.Look.performed += ctx => ViewInput = ctx.ReadValue<Vector2>(); controls.Player.Look.canceled += ctx => { lookInput = Vector2.zero; ViewInput = Vector2.zero; }; } void Update() { ... if ( lookInput.y > 0 ) { anime.SetBool("lookup", true); } else if ( lookInput.y < 0 ) { anime.SetBool("lookdown", true); } else { anime.SetBool( "lookup", false); anime.SetBool( "lookdown", false); } ... }
lookInput 的數值可以用來更新 animator 的參數 - lookup, lookdown,這些參數就可以用來設定甚麼時候從 IDLE 狀態切到 LOOKUP / LOOkDOWN 以及切回,至於 ViewInput public 變數則是用來設定 Camera 的 Y 軸偏移,且只會在 hold time 之後才會更新數值:

C#

// CameraPositionController.cs void LateUpdate() { ... float delta = 0f; if ( player.ViewInput.y > 0 ) { delta = ViewUpY; // 3 } else if ( player.ViewInput.y < 0 ) { delta = -ViewDownY; // 5 } camPos.y = player.transform.position.y + BiasY + delta; ... }

成品


這是最終成品,成功完成了水平方向的運鏡並加上仰望與俯視的視角,開發到這個地步後就會注意到一些程式上的 BUG,像是影片中快速左右切換再奔跑,有一小段時間小騎士是直接水平滑行,推測是前面的動畫還沒播完,就直接開始移位了,看起來還有很多地方要注意呢!! 順邊說一些其他需要注意的地方:
  • 清晰的動畫 - 空洞騎士中,即使小騎士在奔跑中,他本身的動作依舊是清晰的,不過在本次重現過程,會感覺當鏡頭移動時,角色動畫也變得模糊,可以試著降低動畫速度來改善。
  • Frame rate - 如果測試過程有發現鏡頭移動時,畫面會出現短暫水平分割線,這通常是因為 GPU 更新畫面速度比螢幕還要快導致的,可以檢查右上角的 Stats 看看 frame rate (FPS) 是不是超過螢幕上限,如果超過的話可以點選左上角的畫質設定,把 VSync 打開,這樣會確保畫面更新得等到螢幕顯示完成,另外也可以在任意物件中進行底下強制設定 FPS / VSync:

    C#

    void Awake() { QualitySettings.vSyncCount = 60; Application.targetFrameRate = 1; }

總結


恭喜完成了水平向的小騎士控制了ヽ(●´∀`●)ノ,接下來要朝進階課題 - 垂直運動邁進,也就是空洞騎士中最重要的跳躍,包含短跳、長跳、二段跳、落下,以及垂直軸的場景構圖等等。