Unity 2D 自定义碰撞系统(一)
很久之前就想要用Unity實現一個比較復古的碰撞效果。
但是由于Unity的剛體是基于物理運算的,在發生碰撞的時候,會出現反彈等我們不希望出現的效果。
所以通過查看了一些類似的插件和官方的一些項目作為參考,實現了一個沒有力的概念的碰撞系統。
效果
可以看出手感已經很平滑了,而且對于較為邊緣的碰撞,也會自動向外部偏移,這個主要利用了切線的方向,后面會詳細提到。
這一節主要討論碰撞的算法實現,后續可能會更新一套完整的自定義物理系統的架構,可以擴展出許多好玩的效果比如:
冰
推箱子
減速帶
減速區
自定義碰撞算法
1.核心函數
Rigidbody2D.Cast(Vector2 direction, ContactFilter2D contactFilter, List<RaycastHit2D> results, float distance = Mathf.Infinity);該函數會預先檢測移動范圍內所有的碰撞體。
該函數并不會導致物體移動。
物體移動使用Rigidbody2D.position。
參數和返回值
該函數返回值為int類型,表示檢測到的碰撞物體的數量。
direction:表示運動方向。
contactFilter:表示碰撞設置,是一個struct,可以設置碰撞的layer,是否忽略trigger等。
results:返回發生碰撞的信息,也是一個struct,包括碰撞點,碰撞法線,碰撞點到物體的距離等信息。
distance:移動的距離。
Collider2D也擁有Cast函數,與Rigidbody2D的Cast方法的參數和返回值是相同的,不過兩個函數也有不同點。
Rigidbody2D.Cast應該在FixedUpdate中調用,同時最終的移動采用Rigidbody.position進行移動。
Collider2D.Cast應該在Update中調用,最終的移動采用transform.Translate進行移動。
Rigidbody2D.Cast會檢測該物體下所有碰撞體集合的信息。
Collider2D.Cast只會檢測當前碰撞體的碰撞信息。
2.初始化
[RequireComponent(typeof(Collider2D))] public class PhysicalObject : MonoBehaviour {private const float MIN_MOVE_DISTANCE = 0.001f;private new Rigidbody2D rigidbody2D;private ContactFilter2D contactFilter2D;private readonly List<RaycastHit2D> raycastHit2DList = new List<RaycastHit2D>();public LayerMask layerMask;public Vector2 velocity;void Start(){rigidbody2D = GetComponent<Rigidbody2D>();if (rigidbody2D == null)rigidbody2D = gameObject.AddComponent<Rigidbody2D>();rigidbody2D.hideFlags = HideFlags.NotEditable;rigidbody2D.bodyType = RigidbodyType2D.Kinematic;rigidbody2D.simulated = true;rigidbody2D.useFullKinematicContacts = false;rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;rigidbody2D.sleepMode = RigidbodySleepMode2D.NeverSleep;rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;rigidbody2D.gravityScale = 0;contactFilter2D = new ContactFilter2D{useLayerMask = true,useTriggers = false,layerMask = layerMask};}private void OnValidate(){contactFilter2D.layerMask = layerMask;} }這里我們將Rigidbody2D的bodyType設置為Kinematic類型,并且useFullKinematicContacts為false。
Kinematic類型的剛體會返回碰撞信息,但是并不會對剛體造成物理影響,這正是我們需要的。
useFullKinematicContacts之所以設置為false,是因為我們采用Cast的方式進行碰撞檢測而不是采用OnCollisionXX函數,所以為了減少不必要的性能消耗。
3.碰撞檢測
private void Update() {velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical")); }private void FixedUpdate() {Movement(velocity * Time.deltaTime * 5f); }private void Movement(Vector2 deltaPosition) {if (deltaPosition == Vector2.zero)return;Vector2 updateDeltaPosition = Vector2.zero;float distance = deltaPosition.magnitude;Vector2 direction = deltaPosition.normalized;if (distance <= MIN_MOVE_DISTANCE)distance = MIN_MOVE_DISTANCE;rigidbody2D.Cast(direction, contactFilter2D, raycastHit2DList, distance);Vector2 finalDirection = direction;float finalDistance = distance;foreach (var hit in raycastHit2DList){//DoSth}updateDeltaPosition += finalDirection * finalDistance;}我們在FixedUpdate中調用Movement方法,并且檢測所有的碰撞信息。
在執行完Cast方法后,該方法會將所有的碰撞信息保存在raycastHit2DList中。
在Unity5.6版本之前,使用Foreach遍歷每一幀都會GC,所以5.6之前對List的遍歷需要使用For循環或者枚舉器。(詳細:https://www.jianshu.com/p/03760933e2fa)
個人測試Foreach的效率比For要好,大概2:2.5的樣子,所以這里用的Foreach,測試版本Unity2018.3和2019.1。
接下來就是重頭戲了,碰撞檢測的方法。
不過在那之前,我們先明確一些東西。
首先傳入Cast方法的Direction和Distance是固定的,這就意味著這次檢測的方向是固定的,同時也就說明,最終移動的方向一定是檢測的方向,否則這次檢測將失去意義。
所以對于最終移動的方向和距離,我們只需要關心它的距離就好了。
并且最終的距離一定會小于Distance。
在距離上,我們遍歷所有的碰撞信息,并且找出最短的那個,這說明這個物體是距離我們最近的,同時也是我們應該停止的地方。
但是當我們發生碰撞的時候,無論什么方向,都無法進行移動了。
Debug一下
黃色線代表移動方向,白色線代表碰撞法線的方向。
這時候我們發現無論我們朝著哪個方向進行移動,碰撞的法線方向始終是固定的,并且發生碰撞時,兩物體之間的距離始終是0。
所以我們得出的結論就是,碰撞法線和距離是固定,與我們要移動的方向無關。
既然碰撞法線和距離是一定的,也就是說,無論我們朝著什么方向移動,他都不會改變,所以此時我們需要判斷移動的方向與碰撞法線的方向,來決定該如何移動。
此時,我們加入一個點乘判斷,這個判斷表示,如果我們想要移動的方向與碰撞法線的方向基本相反(projection < 0),moveDistance = hit.distance,也就是0,表示無法移動。
如果想要移動的方向與碰撞法線方向呈90度(projection == 0)或者基本相同(projection > 0)說明我們要移動的方向并不會收到碰撞的限制,所以此時將moveDistance = distance,也就是初始運動的距離。
但是還有一個問題就是,雖然我們能夠正確的進行移動了,但是當我們靠近物體的時候,只有在兩方向夾角小于等于90度的時候才能夠進行移動,如果我希望即使靠近物體,仍舊能夠取得移動距離在可移動方向的分量怎么辦?
可能有點繞,直白一點可以表示為,貼著墻摩擦前進。
于是我們需要引入切線來達到這個目的。
那么如何獲取正確的需要移動的切線方向呢?
首先我們需要明確的是,切線方向是碰撞法線的切線方向,而不是移動方向的。
其次切線的方向要保持和移動方向基本相同,因為是移動方向的分量。
(切線用品紅色表示)
所以我們首先通過法線方向獲取切線的方向,然后判斷切線方向與移動方向點積,來確定切線的方向(實際上這里用切線表述并不準確,但是為了簡單明了還是采用切線來表述,實際表述應該是移動方向在切線方向的分量)
并且確定移動距離在切線方向的分量,也就是切線方向需要移動的距離。
在獲得這些信息之后,我們還需要做一次Cast檢測,來確定切線方向的移動會不會碰撞到物體
//Class private readonly List<RaycastHit2D> tangentRaycastHit2DList = new List<RaycastHit2D>();{//Addif (tangentDot != 0){rigidbody2D.Cast(tangentDirection, contactFilter2D, tangentRaycastHit2DList, tangentDistance);foreach (var tangentHit in tangentRaycastHit2DList){Debug.DrawLine(tangentHit.point, tangentHit.point + tangentDirection, Color.magenta);if (Vector2.Dot(tangentHit.normal, tangentDirection) >= 0)continue;if (tangentHit.distance < tangentDistance)tangentDistance = tangentHit.distance;}updateDeltaPosition += tangentDirection * tangentDistance;} }首先我們需要在Class中聲明一個盛放切線碰撞信息的容器。
然后進行切線方向的碰撞檢測。
同樣切線方向的移動也需要判斷與法線的點積來確定如何移動。
最終效果。
如果想要做邊緣自動偏移的話,將BoxCollider2D的Edge Radius設置一下就好了,這樣正方形會變成圓角的正方形。
至此,關于碰撞檢測算法已經基本完成了。
在此之前,我曾經嘗試過很多種實現方式,但是最后采用這種方式,并且這種方式足夠優雅~
只用了為數不多的代碼。
并且實際上很多問題已經解決掉了,所以對這些東西感興趣的朋友也可以自己嘗試一下,這里也是提供一個參考。
完整代碼:
using System.Collections; using System.Collections.Generic; using UnityEngine;[RequireComponent(typeof(Collider2D))] public class PhysicalObject : MonoBehaviour {private const float MIN_MOVE_DISTANCE = 0.001f;private new Collider2D collider2D;private new Rigidbody2D rigidbody2D;private ContactFilter2D contactFilter2D;private readonly List<RaycastHit2D> raycastHit2DList = new List<RaycastHit2D>();private readonly List<RaycastHit2D> tangentRaycastHit2DList = new List<RaycastHit2D>();public LayerMask layerMask;[HideInInspector]public Vector2 velocity;void Start(){collider2D = GetComponent<Collider2D>();rigidbody2D = GetComponent<Rigidbody2D>();if (rigidbody2D == null)rigidbody2D = gameObject.AddComponent<Rigidbody2D>();rigidbody2D.hideFlags = HideFlags.NotEditable;rigidbody2D.bodyType = RigidbodyType2D.Kinematic;rigidbody2D.simulated = true;rigidbody2D.useFullKinematicContacts = false;rigidbody2D.collisionDetectionMode = CollisionDetectionMode2D.Continuous;rigidbody2D.sleepMode = RigidbodySleepMode2D.NeverSleep;rigidbody2D.interpolation = RigidbodyInterpolation2D.Interpolate;rigidbody2D.constraints = RigidbodyConstraints2D.FreezeRotation;rigidbody2D.gravityScale = 0;contactFilter2D = new ContactFilter2D{useLayerMask = true,useTriggers = false,layerMask = layerMask};}private void OnValidate(){contactFilter2D.layerMask = layerMask;}private void Update(){velocity = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));}private void FixedUpdate(){Movement(velocity * Time.deltaTime * 5f);}private void Movement(Vector2 deltaPosition){if (deltaPosition == Vector2.zero)return;Vector2 updateDeltaPosition = Vector2.zero;float distance = deltaPosition.magnitude;Vector2 direction = deltaPosition.normalized;if (distance <= MIN_MOVE_DISTANCE)distance = MIN_MOVE_DISTANCE;rigidbody2D.Cast(direction, contactFilter2D, raycastHit2DList, distance);Vector2 finalDirection = direction;float finalDistance = distance;foreach (var hit in raycastHit2DList){float moveDistance = hit.distance;Debug.DrawLine(hit.point, hit.point + hit.normal, Color.white);Debug.DrawLine(hit.point, hit.point + direction, Color.yellow);float projection = Vector2.Dot(hit.normal, direction);if (projection >= 0){moveDistance = distance;}else{Vector2 tangentDirection = new Vector2(hit.normal.y, -hit.normal.x);float tangentDot = Vector2.Dot(tangentDirection, direction);if (tangentDot < 0){tangentDirection = -tangentDirection;tangentDot = -tangentDot;}float tangentDistance = tangentDot * distance;if (tangentDot != 0){rigidbody2D.Cast(tangentDirection, contactFilter2D, tangentRaycastHit2DList, tangentDistance);foreach (var tangentHit in tangentRaycastHit2DList){Debug.DrawLine(tangentHit.point, tangentHit.point + tangentDirection, Color.magenta);if (Vector2.Dot(tangentHit.normal, tangentDirection) >= 0)continue;if (tangentHit.distance < tangentDistance)tangentDistance = tangentHit.distance;}updateDeltaPosition += tangentDirection * tangentDistance;}}if (moveDistance < finalDistance){finalDistance = moveDistance;}}updateDeltaPosition += finalDirection * finalDistance;rigidbody2D.position += updateDeltaPosition;} }總結
以上是生活随笔為你收集整理的Unity 2D 自定义碰撞系统(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows 下 C++ 利用 Ope
- 下一篇: POE供电那点事