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

Unity Platformer 2D: Ragdoll

Edit icon 沒有留言
Unity

持續上一篇的練習

想按照之前在 3D 製作 ragdoll(布娃娃)的方法,在 2D 如法炮製建立 ragdoll,當角色死亡時,切換角色成 ragdoll,讓物理模擬演出死亡動態。

結論,因參數設計不良,做出來的效果表現沒有達到預期的那樣好,且不預期奇怪的情況也很多,可能還需花費很多時間去調整吧。

尋找教學

Charter Joint(角色關節),之前在 3D 使用這組件(Component)建立部位關節(Joint)的連結,但 Physical 2D 沒有與 Charter Joint 同名的組件。看了一些教學與文件, Physical 2D 世界還真得不太一樣, 好多以前在 3d 沒有見過的 Joint,也找到 Unity 內建提供為了方便 2D Platformer(平台遊戲)製作的 Effector 功能,不過這是後話了。

參考 [0][1] 教學。

建立 Colliders & Rigibody

重製原先的 Player Prefab,在新的 Prefab 上建立 ragdoll 所需要的物理組件,以避免原先的 Player Prefab 太多物件過於複雜,難以控制。

接著為角色的每個部位,左右手,左右腳,身體軀幹,建立適合的 Colliders,並且加入 Rigibody 2D,讓物理控制其物件的碰撞等模擬。

那麼使用 Box Collider 2D 簡易建立就好,還是使用 Polygon Collider 2D 完整包覆腳色部位,主要要看效能以及表現。

效能,Box Collider 2D 相較另一個方案來說,最為減省 CPU 運算量,而複雜的 Polygon Collider 2D,則需要花點時間去運算其多邊型的碰撞。

表現,以目前測試的練習,受限於美術表現以及架構設計(僅少量的部位-左右手、左右腳以及身體軀幹),使用 Box Collider 2D 已經足夠。Polygon Collider 2D 完整包覆的表現,反倒覺得表現上沒有比較好。

每個應用不同而有不同的選擇方式。

使用 Box Collider

Box Collider

使用 Polygon Collider

Polygon Collider

建立 Joints

參考 [0] 的製作方式,角色關節使用 Hinge Joint 2D 來建立, 鉸鏈關節。物理模擬一個結點連接著另一個結點,旋轉移動會相互聯動。

以這次練習來說,Hinge Joint 2D 的參數筆記:

Hinge 參數示意圖

Hinge Joint 2D Inspector View

  • Enable Collision: 與連接的 rigibody 需不需要碰撞測試。以這次練習所使用的角色圖的關係,不需要。
  • Conntected Rigid Body: 與哪個 rigidbody 連接。左右腳以及左右手的 hinge joint 都連接到身體的 rigibody。
  • Auto Configure Connected: 是否自動設定連結參數。看情況用,可以先勾選自動設定後,在關閉重新手動設定一次。
  • Anchor: 該物件錨點,local space
  • Conntected Anchor: 連接 rigidbody 的錨點,local space。在角色的關節製作,這兩個錨點的位置會是一樣。
  • Motor: 是否有自行轉動的馬力。沒有使用到。
  • Angle Limits: 旋轉角度限制。用於左右腳關節,避免腳旋轉 180 度。
  • Break Force: 多少力會使得該關節斷裂。避免手腳被扯斷的獵奇情況,設定為 Infinity,永遠不會被扯斷。
  • Break Torque: 多少扭力會使得該關節斷裂。同上,設定 Infinity。

此外,避免左右手以及左右腳的 Colliders 互相碰撞影響,還需調整所在 Layer (e.g. Player) 的物裡碰撞設定 (Layer Collision Matrix),讓 Player Layer 的 Colliders 不會互相碰撞。

Layer Collision Matrix

Layer Collision Matrix

Ragdoll 切換

寫 Code 來切換角色物件,建立 RagdollController 並且掛在 Player 物件上:

using UnityEngine;

public class RagdollController : MonoBehaviour
{
public GameObject Prefab;

public GameObject DoRagdoll()
{
var root = this.transform;
var go = GameObject.Instantiate(this.Prefab);
go.transform.SetParent(root.parent);
go.gameObject.name = root.gameObject.name;

foreach (var goTrans in go.GetComponentsInChildren<Transform>())
{
var ch = root.FindDeep(goTrans.name);
if (ch != null)
{
this.CopyTransform(ch, goTrans.transform);
}
}

GameObject.Destroy(this.gameObject);
return go;
}

void CopyTransform(Transform src, Transform dst)
{
dst.localPosition = src.localPosition;
dst.localRotation = src.localRotation;
dst.localScale = src.localScale;
}
}

DoRagdoll,建立一份 ragdoll prefab instance,然後設定該 instance 中的所有 transforms 參數,使得跟 player 當下的 transforms 一致,確保換成 ragdoll 後,物件還是長的一樣。

比較特別是使用 FindDeep,這是 transform 的擴充方法(extend method):

public static class TransformExtension
{
public static Transform FindDeep(this Transform aParent, string aName)
{
if (aParent.name == aName)
{
return aParent;
}

foreach (Transform child in aParent)
{
var result = child.FindDeep(aName);
if (result != null)
{
return result;
}
}

return null;
}
}

使用 transform.find 的方式,只是單純這樣實做很快,優化以後再說。

死亡控制器

判斷什麼時候死亡,然後觸發 Ragdoll 切換後,給 Ragdoll 一組力讓它演出死亡的動態。

using UnityEngine;

[RequireComponent(typeof(RagdollController))]
public class DieController : MonoBehaviour
{
public float DieExplostionForce = 50000;
public float DieExplostionRadius = 300;
public float DieUpliftModifier = 0.2f;

void OnCollisionEnter2D(Collision2D c)
{
// 判斷碰到怪物就死亡
if (c.collider.tag == "Monster")
{
var r = this.GetComponent<RagdollController>();
var go = r.DoRagdoll();
var rigid = go.transform.FindDeep("body").GetComponent<Rigidbody2D>();

// 給與一個推力
rigid.AddExplosionForce(
this.DieExplostionForce,
c.collider.transform.position,
this.DieExplostionRadius,
this.DieUpliftModifier
);
}
}
}

其中 AddExplosionForce 是從網路上找到的擴充,有 Rigidbody.AddExplosionForce,但 Rigidbody2D 卻沒有相同的函數,因此需要自行新增擴充函數。

public static class Rigidbody2DExtension
{
public static void AddExplosionForce(this Rigidbody2D body, float explosionForce, Vector2 explosionPosition, float explosionRadius)
{
var dir = ((Vector2)body.transform.position - explosionPosition);
float wearoff = 1 - (dir.magnitude / explosionRadius);
if (wearoff > 0)
{
body.AddForce(dir.normalized * explosionForce * wearoff);
}
}

public static void AddExplosionForce(this Rigidbody2D body, float explosionForce, Vector2 explosionPosition, float explosionRadius, float upliftModifier)
{
var dir = ((Vector2)body.transform.position - explosionPosition);
float wearoff = 1 - (dir.magnitude / explosionRadius);
Vector3 baseForce = dir.normalized * explosionForce * wearoff;
body.AddForce(baseForce);

float upliftWearoff = 1 - upliftModifier / explosionRadius;
Vector3 upliftForce = Vector2.up * explosionForce * upliftWearoff;
body.AddForce(upliftForce);
}
}
  • explosionForce: 爆炸力道
  • explosionPosition: 爆炸位置,world space
  • explosionRadius: 爆炸影響半徑

Demo

Demo1

好似還可以的動態

Demo2

因為 Colliders 設定不是很好,有會這個奇怪結果

Source Code

Github

Series

Reference

沒有留言: