設計我們的遊戲-簡易移動篇

移動的種類

上一篇 設計我們的遊戲物理-簡易重力篇 我們己可讓物件垂直掉落,接下來說要設計的是移動。

我們玩了一些遊戲後大約可以看出的移動可以分為二種:

  • 接下按鈕後,直接以某速度等速移動、放開鈕直接停止。
  • 按下按鈕後,慢慢加速至某速度的加速度移動、放開鈕慢慢停止。

第一種是最直觀的操作,第二種有點像是在冰上滑動或像是車子會慢慢的加減速一樣。

移動量怎麼給?

在開始移動前,先說明一下移動量要怎麼給。

  • 每次加固定量
  • 時內加固定量

每次加固定量,就是每次執行到移動命令時,就固定加一個移動量 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);
		}
	}
}

發表留言