移動的種類
上一篇 設計我們的遊戲物理-簡易重力篇 我們己可讓物件垂直掉落,接下來說要設計的是移動。
我們玩了一些遊戲後大約可以看出的移動可以分為二種:
- 接下按鈕後,直接以某速度等速移動、放開鈕直接停止。
- 按下按鈕後,慢慢加速至某速度的加速度移動、放開鈕慢慢停止。
第一種是最直觀的操作,第二種有點像是在冰上滑動或像是車子會慢慢的加減速一樣。
移動量怎麼給?
在開始移動前,先說明一下移動量要怎麼給。
- 每次加固定量
- 時內加固定量
每次加固定量,就是每次執行到移動命令時,就固定加一個移動量 moveunit。
pos = pos + moveunit;
時間內加固定量,就像是一秒內要移動的量為 moveunit,所以每次移動時要乘上與上個 frame 的時間差 deltatime。
pos = pos + moveunit * deltatime;
這二者有何不同?
如果我們做用第一個 “每次加固定量" 看起來及執行起來其實沒什麼問題,可是當我們遊戲效能很好時,比如一秒可以跑 120 張,那物件就會移動 120 * moveunit 的距離;若效能不好時比如一秒 18 張,那物件就移動 18 * moveunit 的距離。這樣子的移動有時多有時少,跟本不能玩阿!
所以第二種算法 “時間內加固定量" 出現來解決這個問題,當我們遊戲效能很好時,比如一秒可以跑 120 張,每個 frame 與上個 frame 的時間差就會比較小,所以我們移動量少;當遊戲效能不好時,比如一秒跑 18 張,每個 frame 與上個 frame 的時間差就會比較大,所以我們移動量大。這時子計算起來一秒內大家的移動量會是一樣的,現在做遊戲都會使用第二種方法來移動。
當使用方法二時, moveunit 也就會是我們口中常說的速度 speed,每秒移動多少的量。
開始移動吧
假設我們現在要用一個搖桿用來操作一個物件做 x 軸上的移動,移動速度值是依搖桿推動量(0 ~ 1)來決定。
這裡列一下我們將需要那些東西:
- 物件目前速度:velocity
- 操作時移動的速度:speed
- 搖桿的 x 軸推動量:xInput
- frame 與 frame 之間的時間差: deltaTIme
- 加/減速度:acceleration
- 內插:Mathf.Lerp
xInput 可以由真實搖桿、虛擬搖桿取值來設定,這裡我們暫時用鍵盤來設定,按下就是全速,放開就是停止。(如果你想用滑鼠我也不反對)
if (Input.GetKey(KeyCode.LeftArrow)) { xInput = -1.0f; } if (Input.GetKeyUp(KeyCode.LeftArrow)) { xInput = 0.0f; } if (Input.GetKey(KeyCode.RightArrow)) { xInput = 1.0f; } if (Input.GetKeyUp(KeyCode.RightArrow)) { xInput = 0.0f; }
等速移動很簡單,就只要依搖桿推動來量計算出 velocity 在 x 軸上的速度。
velocity.x = speed * xInput;
加速移動,就需一個叫加速度 acceleration 的東西,配合內插法計算讓物件漸漸的把速度提升到 speed * xInput 的數值。這算法是我在 3DBuzz 裡學到的,寫起來還蠻簡潔的,大家可以參考看看。
velocity.x = Mathf.Lerp(velocity.x, speed * xInput, deltatime * acceleration);
如上公式,velocity.x 會一直接近 speed * xInput,速度增加量會因 acceleration 的大小而變化,acceleration 越大 velocity.x 就會越快接近 speed * xInput,這樣就能做到我們想要的加速度移動了。
再來就是我們熟悉的算法,用 velocity * deltaTIme 來算出就是這個 frame 時的移動量,角色就用這個值來移動了!
deltaMove = velocity * deltatime;
二個併一個
聰明的你,一定會發現,在這個公式裡
velocity.x = Mathf.Lerp(velocity.x, speed * xInput, deltatime * acceleration);
如果我們把 acceleration 設很大(比如來個 10000),讓 deltatime * acceleration 在這個 frame 一下子就達到或超過 1,這樣子結果不就會變成等速移動了?
沒錯!如果大家不計較效能,我們只要一條這個公式,再去依狀況來調整 acceleration,就能同時做到等速移動及加速度移動了,不用寫判斷式來分二種計算方式。
要合併或分開都各有好處,大家聰明的挑選吧!
加入環境因素吧
移動時只有考慮到自己,好像也是有點無聊,再加入一點環境的因素好了。
這裡我們加入二個參數
- 環境速度:envSpeed
- 環境加速度:envAcceleration
然後程式可以寫成
velocity.x = Mathf.Lerp(velocity.x, (speed + envSpeed) * xInput, deltatime * (acceleration + envAcceleration));
然後再寫個函式來設定這二個參數
public void AddEnvSpeed(float val) { envSpeed += val; } public void AddEnvAcc(float val) { envAcceleration += val; }
為什麼要用加的?因為這樣子可以吃多個環境影響因素, 讓各個因素疊加起來來影響我們的角色。
再來,我們就可以在不同的地形上加入一個 Trigger,當角色移至該處時 envSpeed 或 envAcceleration 會跟著變化,做出不同的移動感。
public float envSpeed = 0; public float envAcceleration = 0; void OnTriggerEnter(Collider other) { if (other.tag == "Player") { other.gameObject.SendMessage("AddEnvSpeed", envSpeed); other.gameObject.SendMessage("AddEnvAcc", envAcceleration); } } void OnTriggerExit(Collider other) { if (other.tag == "Player") { other.gameObject.SendMessage("AddEnvSpeed", -envSpeed); other.gameObject.SendMessage("AddEnvAcc", -envAcceleration); } }
記得有 Enter 就要有 Exit 不然我們的參數就變不回來了
看吧,這樣子角色在這個世界裡移動起來時是不是生動多了呢?
另外,我們也能利用同樣的手法來製作超簡易的裝備影響,再建個 eqvSpeed、eqvAcceleration,然後再把它們加進公式裡,裝上裝備後就去改這二個值(如:裝了鞋子速度加快、拿了鎚子起步變慢等)。簡易的裝備做法可以先自 High 一下,但做大系統時請好好的再規畫哦!
下一篇可能是講跳躍吧?
完整程式碼:
- MyPlayerMove
using UnityEngine; using System.Collections; public class MyPlayerMove : MonoBehaviour { public Vector3 velocity = Vector3.zero; public Vector3 deltaMove = Vector3.zero; public float speed = 2; public float acceleration = 1.0f; float xInput = 0; float zInput = 0; public float envSpeed = 0; public float envAcceleration = 0; // Use this for initialization void Start () { } // Update is called once per frame void Update () { float deltatime = Time.deltaTime; HandleKeyInput(deltatime); } void LateUpdate () { float deltatime = Time.deltaTime; UpdateHorForce(deltatime); UpdateMovement(deltatime); } void UpdateMovement(float deltatime) { deltaMove = velocity * deltatime; gameObject.transform.Translate(deltaMove, Space.World); //recalute velocity by final deltaMove. if (deltatime > 0) { velocity = deltaMove / deltatime; } } void HandleKeyInput(float deltatime) { //X axis. if (Input.GetKey(KeyCode.LeftArrow)) { xInput = -1.0f; } if (Input.GetKeyUp(KeyCode.LeftArrow)) { xInput = 0.0f; } if (Input.GetKey(KeyCode.RightArrow)) { xInput = 1.0f; } if (Input.GetKeyUp(KeyCode.RightArrow)) { xInput = 0.0f; } //Z axis. if (Input.GetKey(KeyCode.UpArrow)) { zInput = 1.0f; } if (Input.GetKeyUp(KeyCode.UpArrow)) { zInput = 0.0f; } if (Input.GetKey(KeyCode.DownArrow)) { zInput = -1.0f; } if (Input.GetKeyUp(KeyCode.DownArrow)) { zInput = 0.0f; } } void UpdateHorForce(float deltatime) { //float val = Mathf.Lerp(velocity.x, speed * xInput, deltatime * acceleration); float val = speed * xInput; SetForceX(val); val = Mathf.Lerp(velocity.z, (speed + envSpeed) * zInput, deltatime * (acceleration + envAcceleration)); SetForceZ(val); } public void SetForceX(float x) { velocity.x = x; } public void SetForceZ(float z) { velocity.z = z; } public void AddEnvSpeed(float val) { envSpeed += val; } public void AddEnvAcc(float val) { envAcceleration += val; } }
- MyEvn
using UnityEngine; using System.Collections; public class MyEvn : MonoBehaviour { public float envSpeed = 0; public float envAcceleration = 0; // Use this for initialization void Start () { } // Update is called once per frame void Update () { } void OnTriggerEnter(Collider other) { if (other.tag == "Player") { other.gameObject.SendMessage("AddEnvSpeed", envSpeed); other.gameObject.SendMessage("AddEnvAcc", envAcceleration); } } void OnTriggerExit(Collider other) { if (other.tag == "Player") { other.gameObject.SendMessage("AddEnvSpeed", -envSpeed); other.gameObject.SendMessage("AddEnvAcc", -envAcceleration); } } }
- 參考:
3DBuzz http://www.3dbuzz.com/