說到unity的物理系統,大家肯定第一反應肯定是「不就是rigidbody和collider那些東西嗎,我會」。但是提及背後的原理,我敢說99%的人是不知道的。unity的物理系統很強大沒錯,然而當它不能滿足我們的需求時,我們就需要自己寫一套物理系統了。今天這個系列文章分享的是如何不依賴unity的api自己搭建一個簡單的2D物理系統。 說是物理系統,其實只有最基礎的部分,如下是演示之一:
(藍色方塊會不斷移動,檢測到碰撞進入時會變紅色,檢測到碰撞退出時會變藍色)
這裏麪包含了這麼幾個功能:
爲了簡化問題,規定了以下條件:
第一篇文章要實現的功能如下,即一個剛體受重力影響撞到碰撞體,然後停止。對應工程裡的Test/Scenes/CollisionEvent場景。
這裏面按照邏輯可以分爲三步:
剛體受重力影響往下移動
剛體知道自己碰到了物體
剛體把自己的速度設成0
先來看UML圖:
JCollisionController,碰撞檢測基礎類別,包含一個BoxColiider2D。(因爲偷懶,工程裡直接使用的是unity的BoxCollider2D,自己實現的話難度不大,注意當transform旋轉和變化scale時,bounds的大小是會實時發生變化的)
JPlatform,平臺類,會受到碰撞,不會主動發起碰撞,用於地面或牆壁等靜止不動的物體。
JRigidbody,剛體類,會受重力影響,可以設定速度進行移動,會主動發起碰撞,用於玩家、敵人、子彈等各種會動的物體。
CollisionInfo,記錄剛體碰撞資訊,會用於遊戲邏輯中,比如說要實現一個剛體碰撞到牆壁改變方向的功能就會用到這個資訊。
RaycastOrigins,記錄射線檢測的起點。具體後面會講。
JPhysicsManger,管理所有平臺和剛體類,收集和處理碰撞資訊,發送碰撞事件。
JPhysicsSetting,記錄物體系統用到的設定資訊,包含重力是多少,以及哪些layer和哪些layer會發生碰撞。
先來思考一個問題,如何知道當前幀有哪些碰撞體進入到了其他碰撞體的檢測範圍或者是離開了其他碰撞體的檢測範圍?這個進入和退出肯定是隻有第一次纔會判定,,比如說第1幀A進入了B的範圍,這個時候應該觸發OnCollisionEnter事件,第2幀如果A還在B的範圍,就不應該觸發這個事件了。
爲了實現這個功能,我們需要在某個特定時機收集當前幀都有哪些碰撞體和哪些碰撞體發生碰撞放在列表裏,在全部收集完之後,和上一幀收集到的碰撞資訊列表進行比較,如果多了上一幀的碰撞資訊沒有的,說明應該發送OnCollisionEnter事件,如果上一幀的某個碰撞資訊在當前幀的列表沒有了,說明應該發送OnCollisionExit事件。
這部分程式碼在JPhysicsManager中:
private void HandleCollidersEnter()
{
// New Collisions This Frame
foreach( var currentFrameCollision in _currentFrameHitColliders )
{
if( !_lastFrameHitColliders.Contains( currentFrameCollision ) )
{
//發送碰撞事件
this.ContactEvent( currentFrameCollision, true );
_lastFrameHitColliders.Add( currentFrameCollision );
}
}
......
}
private void ContactEvent( CollisionInfo collisionInfo, bool isBeginEvent )
{
if( collisionInfo.hitCollider == null || collisionInfo.collider == null )
{
return;
}
if( collisionInfo.collider.isTrigger || collisionInfo.hitCollider.isTrigger )
{
// Trigger Event
this.SendCollisionMessage( collisionInfo, isBeginEvent, true );
}
else
{
// Collison Event
this.SendCollisionMessage( collisionInfo, isBeginEvent, false );
}
}
剩下的問題就是在哪個時機進行收集和處理碰撞資訊。這一點只要參照unity自己的順序就可以了。
【雨松Mono:Unity宣告週期圖】
//In JPhysicsManager
private void FixedUpdate()
{
foreach( var pair in _rigidbodies )
{
var rigidbody = pair.Value;
if( !rigidbody.isActiveAndEnabled || !rigidbody.gameObject.activeInHierarchy )
{
continue;
}
rigidbody.Simulate( Time.fixedDeltaTime );
......
}
}
//In JPhysicsManager
private IEnumerator UpdateCollisions()
{
while( true )
{
yield return _waitForFixedUpdate;
this.HandleCollidersEnter();
this.HandleCollidersExit();
_currentFrameHitColliders.Clear();
_currentFrameHitRigidbodies.Clear();
}
}
接下來要講的是剛體類的Simulation方法裡都做了什麼事情:
public override void Simulate( float deltaTime )
{
base.Simulate( deltaTime );
//受重力的影響
var gravity = _physicsManager.setting.gravity;
var gravityRatio = gravityScale * deltaTime;
_velocity.x += gravity.x * gravityRatio;
_velocity.y += gravity.y * gravityRatio;
_movement.x = _velocity.x * deltaTime;
_movement.y = _velocity.y * deltaTime;
// 在碰撞檢測前,重置一些狀態
this.ResetStatesBeforeCollision();
if( this.selfCollider == null || !this.selfCollider.enabled )
{
return;
}
//碰撞檢測
this.CollisionDetect();
//移動位置
this.Move();
//根據設定檢測結果調整速度,比如水平方向撞到了物體那麼水平方向速度爲0
this.FixVelocity();
// 在碰撞檢測後,重置一些狀態
this.ResetStatesAfterCollision();
}
重點在於碰撞檢測函數 CollisionDetect。
如圖,一個物體從左下角移動到右上角,那麼它的檢測範圍應該是虛線包裹的區域。
如果要準確的檢測得使用類似unity的BoxCast,會比較麻煩且耗效能,所以我使用了一種方法來近似這個過程。 用若幹條RayCast來近似達到BoxCast的效果。
接下來考慮剛體靜止不動的情況,如圖,當剛體不動時,正好貼着牆,按照剛纔發射線的方式,射線長度是0,會判斷爲 什麼也檢測不到,肯定是錯誤的。所以在靜止不動時,會有一個最小射線長度。
此時又會產生新的問題,比如圖中這種情況,貼着牆的情況往右移動,豎直方向上的射線會檢測到牆壁,這是不對的,正確的碰撞結果應該是隻有右側碰撞到了牆壁。
這個問題的解決辦法是把射線起點放在碰撞體的內部。
水平方向的碰撞檢測核心程式碼如下:
_expandWidth代表最小射線長度,_shrinkWidth代表射線起點往裏縮的距離
var rayOrigin = ( directionX == 1 ) ? _raycastOrigins.bottomRight : _raycastOrigins.bottomLeft;
var rayLength = Mathf.Abs( _movement.x ) + _shrinkWidth;
if( _movement.x == 0f )
{
rayLength += _expandWidth;
}
for( int i = 0; i < this.horizontalRayCount; i++ )
{
_raycastDirection.x = 1.0f;
_raycastDirection.y = 0.0f;
_raycastDirection.x *= directionX;
_raycastDirection.y *= directionX;
var hitCount = Physics2D.RaycastNonAlloc( rayOrigin, _raycastDirection, _raycastHit2D, rayLength, this.collisionMask );
for( int j = 0; j < hitCount; j++ )
{
var hit = _raycastHit2D[j];
if( _ignoredColliders.Contains( hit.collider ) )
{
continue;
}
HandleHorizontalHitResult( hit.collider, hit.point, hit.distance, directionX );
}
rayOrigin.y += _horizontalRaySpace;
}
HandleHorizontalHitResult填充碰撞資訊,把碰撞資訊新增到JPhysicsManager中,還需要調整剛體的移動距離:
private void HandleHorizontalHitResult( Collider2D hitCollider, Vector2 hitPoint, float hitDistance, int directionX )
{
//Trigger
if( HitTrigger( hitCollider, hitPoint, directionX, null ) )
{
return;
}
// Collision Info
_collisionInfo.collider = this.selfCollider;
_collisionInfo.hitCollider = hitCollider;
_collisionInfo.position = hitPoint;
// Collision Direction
if( directionX == -1 )
{
_collisionInfo.isLeftCollision = true;
}
if( directionX == 1 )
{
_collisionInfo.isRightCollision = true;
}
//Push Collision
if( !_currentDetectionHitColliders.Contains( hitCollider ) )
{
_physicsManager.PushCollision( _collisionInfo );
_currentDetectionHitColliders.Add( hitCollider );
}
//Fix movement
if( _movement.x != 0.0f )
{
if( Mathf.Abs( hitDistance - _shrinkWidth ) < Mathf.Abs( _movement.x ) )
{
_movement.x = ( hitDistance - _shrinkWidth ) * directionX;
}
}
}
爲什麼要調整移動距離?看下面 下麪這張圖,上面的物體上一幀還在平臺上面,按照它的移動速度,當前幀它將會穿過平臺,此時就必須把它的位置調整到剛好貼合平臺上。
由於篇幅有限,在本篇文章中射線檢測我們暫時使用unity的Physics2D提供的方法。在之後的文章中,我們會使用一個四元樹結構對場景空間中的物體進行管理,然後用自己來實現射線檢測。
這就是本節全部內容。
github工程
對應的是Test/Scenes/CollisionEvent
關於作者:
CSDN部落格:https://blog.csdn.net/j756915370
知乎專欄:https://zhuanlan.zhihu.com/c_1241442143220363264
交流學習羣:891809847