關於 web service, unity, blogger 等軟體工程筆記

Unity Platformer 2D: Character Movement using Physical

Edit icon 沒有留言
Unity

物理與碰撞

想要在 Unity 中使用其物理引擎控制 2D 遊戲物件,Rigidbody2D,這一個組件(Component)是最重要的核心。把 Rigidbody2D 加上 2D 碰撞體(2D Colliders)放在同一個 GameObject 上,即可在遊戲中看到自由落體以及物件碰撞等物理模擬效果。

這次練習中,嘗試使用 Unity 物理組件以及上次 FGJ 活動中小組繪製的人物,練習 2D 角色(Charater or Avatar)控制程式撰寫。

準備角色,先在角色 GameObject 加上 Rigidbody2D 以及 Colliders,角色的 Colliders 從範例以及理解的經驗來看,角色腳部最好是一個 CircleCollider,身體根據需求搭配其他的 Colliders,例如 BoxCollider。

腳部使用 CircleCollider,與地板接觸永遠只有一個點。想到最主要好處莫過於是爬坡物理的實現,沒有什麼比 Circle 還要更好爬上斜坡的,能想像推動一個圓形的輪胎以及一個方形的箱子,哪種容易推上斜坡不言而喻。

另一個好處應該是在平台邊緣的物裡表現上,腳部為 BoxCollider 總是可以很極限站在平台邊緣,都還不會掉落。而腳部為 CircleCollider,則會因為接觸點而會自動滑落。

Box Collider

Box Collider

Circle Collider

Circle Collider

最後,設定 Rigidbody 的限制條件,若是要製作像瑪莉歐那種 2D 角色控制,得把 Freeze Rotation Z 給打勾,不要讓 Rigidbody 旋轉,不然就像以下示意動畫那樣,角色不會前進,而是撲倒在地……。

沒有限制旋轉的後果

Without freeze rotation z

角色移動

要控制角色物件上的剛體(Rigidbody)移動,最簡單的方式就是對其 Rigidbody 施力。在物理更新 FixedUpdate 中,加入從 Input 取得玩家輸入的方向資料,對 Rigidbody 施力,其程式碼:

using UnityEngine;

public class PlayerMoveController : MonoBehaviour {

public float MoveForce = 356;

void FixedUpdate()
{
var dir = Input.GetAxis("Horizontal");
var r = this.GetComponent<Rigidbody2D>();
r.AddForce(Vector2.right * this.MoveForce * dir);
}
}

可這有個問題,按住方向鍵不放會一直對 Rigidbody 施力,導致最後物件跑得飛快。故加上最大速度條件限制,避免物體因持續施力,導致加速太多速度太快。

if (r.velocity.x * dir < this.MaxSpeed)
{
r.AddForce(Vector2.right * this.MoveForce * dir);
}

if (Mathf.Abs(r.velocity.x) > this.MaxSpeed)
{
r.velocity = new Vector2(Mathf.Sign(r.velocity.x) * this.MaxSpeed, r.velocity.y);
}

第一個條件判斷設計非常精妙,r.velocity.x * dir 的計算值若是小於 0,表示目前物件移動方向與控制方向相反,故要加速。若計算值大於 0,表示控制方向與物件移動方向相同,若目前速度沒有超過最大速度,則繼續施力加速。

第二個條件判斷最大速度限制,超過就強制設定為限制的最大速度。Mathf.Sign 取值正負號,回傳 +1 或是 -1。

接著加上 Flip 左右翻轉的功能,根據目前移動方向,調整物件左右朝向,以獲得較好的視覺效果。簡單的做法直接調整 transform.localScale.x,直接乘上 -1 完成左右翻轉。

if (this.transform.localScale.x * dir < 0)
{
this.Flip();
}
void Flip()
{
var s = this.transform.localScale;
s.x *= -1;
this.transform.localScale = s;
}

一個超級簡單的角色移動控制就完成了,做過一次才知道超級簡單。想想之前不知道為什麼在 FGJ 2016 會場上,可以為了角色控制,弄了好幾個小時都還沒有搞定……。

以下為 PlayerMoveController 完整的程式碼。

using UnityEngine;

public class PlayerMoveController : MonoBehaviour {

public float MaxSpeed = 20;
public float MoveForce = 356;

void FixedUpdate()
{
var dir = Input.GetAxis("Horizontal");
var r = this.GetComponent<Rigidbody2D>();
if (r.velocity.x * dir < this.MaxSpeed)
{
r.AddForce(Vector2.right * this.MoveForce * dir);
}

if (Mathf.Abs(r.velocity.x) > this.MaxSpeed)
{
r.velocity = new Vector2(Mathf.Sign(r.velocity.x) * this.MaxSpeed, r.velocity.y);
}

if (this.transform.localScale.x * dir < 0)
{
this.Flip();
}
}

void Flip()
{
var s = this.transform.localScale;
s.x *= -1;
this.transform.localScale = s;
}
}

來跳躍吧

想想有點角色移動有點貧乏,嘗試實現角色跳耀的功能。跳躍,其實就是對角色剛體施一個向上的力,物理重力會把該剛體向下拉,最後落到地面上過程。

另外在向上跳躍的過程中,不能再給予向上的力,不然就會像火箭那樣,藉由引擎持續出力,飛到外太空去了。需判斷角色是否在地面上,只有當角色在地面上時,才能施予向上的力。

判斷在地面的想法很單純,角色物件中心向下打一條射線(Ray),計算碰到地板物件的距離長短,來判斷是否在地面上。但得定義距離大於多少時算是在空中,這距離的計算挺麻煩的。因此改用另外一種方式。

在角色腳部更下方建立一個 Detector,由角色物件中心到這 Detector 的兩點連線(Line Segment),去問物理引擎有沒有與任何地板物件相交(Physics2D.Linecast),有表示角色物件目前在地板上,反之亦然。

建立在地板偵測功能:

using UnityEngine;

public class GroundDetector : MonoBehaviour {

public Transform Checker;
public bool Grounded
{
get;
private set;
}

void Update()
{
if (this.Checker != null)
{
this.Grounded = Physics2D.Linecast(
this.transform.position,
this.Checker.position,
LayerMask.GetMask("Ground"));
}
}
}

接下來對角色移動控制器處理做些修改,在 Update 中判斷是否按下跳躍:

void Update()
{
var gounded = this.GetComponent<GroundDetector>().Grounded;
if (Input.GetButtonDown("Jump") && gounded)
{
this.jumpFlag = true;
}
}

在 FixedUpdate 中加入跳躍控制施力的程式碼:

void FixedUpdate()
{

if (this.jumpFlag)
{
r.AddForce(Vector2.up * this.JumpForce);
this.jumpFlag = false;
}
...
}

最後調整物理參數,下圖結果看起來還算可以。

跳躍功能展示

跳躍功能的測試

關於原始碼

PlayerMoveController 完整程式碼, GroundDetector 則可以參考上節:

using UnityEngine;

public class PlayerMoveController : MonoBehaviour {

public float MaxSpeed = 20;
public float MoveForce = 356;

public float JumpForce = 100;

bool jumpFlag = false;

void Update()
{
var gounded = this.GetComponent<GroundDetector>().Grounded;
if (Input.GetButtonDown("Jump") && gounded)
{
this.jumpFlag = true;
}
}

void FixedUpdate()
{
var dir = Input.GetAxis("Horizontal");
var r = this.GetComponent<Rigidbody2D>();
if (r.velocity.x * dir < this.MaxSpeed)
{
r.AddForce(Vector2.right * this.MoveForce * dir);
}

if (Mathf.Abs(r.velocity.x) > this.MaxSpeed)
{
r.velocity = new Vector2(Mathf.Sign(r.velocity.x) * this.MaxSpeed, r.velocity.y);
}

if (this.transform.localScale.x * dir < 0)
{
this.Flip();
}

if (this.jumpFlag)
{
r.AddForce(Vector2.up * this.JumpForce);
this.jumpFlag = false;
}
}

void Flip()
{
var s = this.transform.localScale;
s.x *= -1;
this.transform.localScale = s;
}
}

完整練習範例放置在 Github,week1。

Github

Series

Reference

沒有留言: