前言

說到unity的物理系統,大家肯定第一反應肯定是“不就是rigidbody和collider那些東西嗎,我會”。但是提及背後的原理,我敢說99%的人是不知道的。unity的物理系統很強大沒錯,然而當它不能滿足我們的需求時,我們就需要自己寫一套物理系統了。今天這個系列文章分享的是

如何不依賴unity的api自己搭建一個簡單的2D物理系統

。github工程。 說是物理系統,其實只有最基礎的部分,如下是演示之一:

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

(藍色方塊會不斷移動,檢測到碰撞進入時會變紅色,檢測到碰撞退出時會變藍色) 這裡麵包含了這麼幾個功能:

剛體的移動

(對應unity的

rigidbody類

碰撞檢測的判定

(對應unity的

Physics類

碰撞檢測判定成功後的事件派發

(對應unity的

OnCollisionEnter和OnCollisionExit

為了簡化問題,規定了以下條件:

只有AABB(軸對稱包圍盒)

剛體在運動過程中Collider的大小不會發生變化

類功能說明

第一篇文章要實現的功能如下,即

一個剛體受重力影響撞到碰撞體,然後停止

。對應工程裡的

Test/Scenes/CollisionEvent

場景。

這裡面按照邏輯可以分為三步:

剛體受重力影響往下移動

剛體知道自己碰到了物體

剛體把自己的速度設成0

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

先來看UML圖:

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

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宣告週期圖】

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

FixedUpdate

,剛體進行移動碰撞檢測,JPhysicsManager收集碰撞資訊

//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

);

……

}

}

WaitForFixedUpdate

,處理碰撞檢測資訊,傳送OnCollisionXXX等碰撞檢測事件

//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

。 如圖,一個物體從左下角移動到右上角,那麼它的檢測範圍應該是

虛線包裹的區域

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

如果要準確的檢測得使用類似unity的

BoxCast

,會比較麻煩且耗效能,所以我使用了一種方法來近似這個過程。 用若干條

RayCast

來近似達到

BoxCast

的效果。

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

水平方向上:

射線起點在右邊緣上(如果是往左移動則是在左邊緣上),射線長度是當前幀這個剛體水平方向移動的距離

豎直方向上:

射線起點在上邊緣上(如果是往下移動則是在下邊緣上),射線長度是當前幀這個剛體垂直方向移動的距離

接下來考慮剛體靜止不動的情況,如圖,當剛體不動時,正好貼著牆,按照剛才發射線的方式,射線長度是0,會判斷為 什麼也檢測不到,肯定是錯誤的。所以

在靜止不動時,會有一個最小射線長度

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

此時又會產生新的問題,比如圖中這種情況,貼著牆的情況往右移動,豎直方向上的射線會檢測到牆壁,這是不對的,正確的碰撞結果應該是隻有右側碰撞到了牆壁。

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

這個問題的解決辦法是

把射線起點放在碰撞體的內部

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

水平方向的碰撞檢測核心程式碼如下:

_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

}

}

}

為什麼要調整移動距離

?看下面這張圖,上面的物體上一幀還在平臺上面,按照它的移動速度,當前幀它將會穿過平臺,此時就

必須把它的位置調整到剛好貼合平臺上

【物理篇】從零搭建2D物理系統①——剛體和碰撞檢測事件

由於篇幅有限,在本篇文章中射線檢測我們暫時使用unity的

Physics2D

提供的方法。在之後的文章中,我們會使用

一個四叉樹結構對場景空間中的物體進行管理

,然後用自己來實現射線檢測。

這就是本節全部內容。 github工程 對應的是

Test/Scenes/CollisionEvent

水曜日雞

,簡稱

水雞

,ACG宅。曾參與索尼中國之星專案研發,具有2D聯網多人動作遊戲開發經驗。

CSDN部落格:

https://

blog。csdn。net/j75691537

0

知乎專欄:

https://

zhuanlan。zhihu。com/c_12

41442143220363264

交流學習群:891809847