概述
遊戲現在似乎已經成為了大家繞不開的一個娛樂方式,從大型端游到手遊,到頁遊,再到各種 APP 裡面的 H5 小遊戲,它以各種方式入侵了我們的生活。那麼在享受遊戲的同時,作為一名前端開發,也開始思考如何開發一款遊戲,在技術層面它應當具備什麼?
除了基本的遊戲畫面、動效開發、渲染功能,還有一項值得探究的東西,那就是物理引擎。一個好的物理引擎,保證了遊戲內的互動體驗和現實中相似,給人提供了更優質的體驗。
現在好用的物理引擎有很多,大部分都是開箱即用,但物理引擎的基礎和底層邏輯是什麼樣子的,可能有些人並不瞭解。從這期開始我們將分多個部分介紹物理引擎的基礎,讓大家對此更加了解。以下的內容部分參考自《遊戲物理引擎開發》。
何為引擎
引擎是什麼?當然這裡是延展了汽車中引擎的概念。在汽車中,引擎——一種能量轉換的裝置,將其他能變為機械能提供給汽車,是汽車能夠運動的核心模組。
那對應的,在開發當中的引擎是什麼呢?在我的理解中,是一個可以將一個個功能快速加入到專案中,並且保證功能運轉的核心模組。渲染引擎,就能夠快速實現內容的渲染。遊戲引擎,就能夠快速實現一個遊戲的基礎開發。而物理引擎,就是可以快速模擬現實中物理狀態。
瞭解完引擎是什麼之後,我們可以再來關注一下,引擎有什麼特點。引擎最大的特點就是兩個——快速,通用。它能夠快速實現需要的功能,並且有很強的通用性,它並非是針對某個專門的業務開發的,而是針對一大類情況進行開發,所以必須擁有強大的通用性。
快速意味著需要做到功能完善,API 封裝完整,使用便捷。通用意味著程式碼本身的邏輯需要足夠底層,應用最基本的邏輯才能做到最大的通用性。
物理引擎的基礎
一個物理引擎的基礎是什麼呢?那就是物理和數學。其實一個物理系統在遊戲中的體現是整個物理系統內各個物件的位置。每一幀都需要計算物體的位置,使得他們能出現在正確的地方。所以符合物理學規律的數學運算,是一個物理引擎的基礎。我們下面的一切都是以此為依據來進行闡述的。
程式碼中的數學
首先要看一下,在遊戲世界中,數學在哪些地方起到了作用。無論是在二維還是三維的世界中,針對物件的位置的描述都是由向量來完成的。而向量的處理,免不了的就是向量本身的一些分解、加減、點積、向量積等知識。
所以我們要先建立一個最基礎的向量類:
class Vector {
x: number
y: number
z: number
constructor(x: number,y: number,z: number) {
this.x = x
this.y = y
this.z = z
}
setX(x: number) {
this.x = x
}
setY(y: number) {
this.y = y
}
setZ(z: number) {
this.z = z
}
}
向量的分解,應用的是三角函式的內容,將一個向量通過角度分解到 x 軸、y 軸、z 軸,或者根據不同軸上的座標來計算對應的角度。
三角函式
而向量的計算原理,就不仔細闡述了。在遊戲世界中,最後都會被分解到對應座標軸的方向進行計算,即便是點積或者向量積也不例外。所以只要熟練運用三角函式和向量計算公式,就能夠進行向量的處理了。
我們將給向量增加以下計算方法:
class VectorOperation {
add (vectorA: Vector, vectorB: Vector) { // 向量相加
return new Vector(
vectorA.x + vectorB.x,
vectorA.y + vectorB.y,
vectorA.z + vectorB.z
)
}
minus (vectorA: Vector, vectorB: Vector) { // 向量相減
return new Vector(
vectorA.x - vectorB.x,
vectorA.y - vectorB.y,
vectorA.z - vectorB.z
)
}
multiply (vectorA: Vector, times: number) { // 向量縮放
return new Vector(
vectorA.x * times,
vectorA.y * times,
vectorA.z * times
)
}
dotProduct (vectorA: Vector, vectorB: Vector) { // 向量點積
return vectorA.x* vectorB.x + vectorA.y* vectorB.y + vectorA.z* vectorB.z
}
vectorProduct (vectorA: Vector, vectorB: Vector) { // 向量外積
return new Vector(
vectorA.y * vectorB.z - vectorA.z * vectorB.y,
vectorA.z * vectorB.x - vectorA.x * vectorB.z,
vectorA.x * vectorB.y - vectorA.y * vectorB.x,
)
}
}
而在遊戲物理學中,還需要用到一門很重要的數學知識,那就是微積分。
這麼說大家可能體會不到,都是一些基礎的物理內容,為什麼會用到微分和積分呢?來舉個例子,先看看最基本的速度公式,先從平均速度開始,是經過的路程除以經過的時間:
$$ v = \frac {s_{1} - s_{0}}{t_{1} - t_{0}} \tag{平均速度}$$
然後是某個時刻的速度的計算,其實就是在平均速度的基礎上,將時間差縮小到無窮小:
$$ v = \lim_{\Delta t \to 0} \frac {\Delta s}{\Delta t} = \frac{ds}{dt} \tag{速度}$$
微分的原理
這就是微分的應用。那麼積分的應用呢?
再來看看最基本的速度和路程的公式,在勻速運動中的公式如下,其中 t 為運動時間:
$$ s_{1} = s_{0} + v_{0}t \tag{勻速運動}$$
其實這個公式的本質應該是:
$$ s_{1} = s_{0} + \int_{t_{0}}^{t_{1}}v_{0}dt \tag{勻速運動}$$
以上只是微積分的簡單應用,說明了在遊戲中微積分的使用也十分重要,那麼我們在程式碼中也應該加入對應的方法。
物理基礎
在一個虛擬的世界裡面,我們要是想要獲得和現實一樣的體驗,也必然要遵循現實中的物理法則,不可能出現蘋果朝天上飛的狀況。由此我們先來構建一個模擬真實環境的物件。
在物理引擎中,一個物體應該具有什麼樣子的屬性呢?最重要的就是上文提到的位置資訊,那麼對應的,是改變位置的資訊,也就是速度。隨之又引出了一個值,那就是改變速度的資訊,也就是加速度了。在這樣的基礎上,我們可以得到一個最基本的物體所應該擁有的屬性:
class GameObj {
pos: Vector
velocity: Vector
acceleration: Vector
constructor(pos?: Vector, velocity?: Vector, acceleration?: Vector) {
this.pos = pos || new Vector(0, 0, 0)
this.velocity = velocity || new Vector(0, 0, 0)
this.acceleration = acceleration || new Vector(0, 0, 0)
}
setPos (pos: Vector) {
this.pos = pos
}
setVelocity (velocity: Vector) {
this.velocity = velocity
}
setAcceleration (acceleration: Vector) {
this.acceleration = acceleration
}
}
我們現在擁有了最基本的一個物體,想要使這個物體融入物理體系由我們任意操作,就需要將物體與力結合在一起。而結合兩者的,正是牛頓三大定律。
首先是牛頓第二定律,作用力可以改變物體的運動狀態。用一個簡單的公式表達就是:
$$ \vec F = m\vec a \tag{牛頓第二定律}$$
那也就是說,我們要結合加速度和力的話,需要給物體一個變數,那就是質量 m。那我們給上述物件再新增上質量屬性:
class GameObj {
mess: number // 質量不得為 0
constructor(mess?: number) {
if (mess > 0) {
this.mess = mess
}
}
setMess (mess: number) {
if (mess > 0) {
this.mess = mess
}
}
}
但是這個時候我們會有兩個問題:第一,物體的質量不能為0,如果設定了0,就會導致質量設定出錯;第二,某些物體,我們需要它有著無窮大的質量,比如地面,牆體,我們是需要它在遊戲場景中保持固定的。那麼一方面是不允許出現的0,另一方面是難以設定的無窮大,應該怎麼辦呢?
在遊戲物理學中提出了一個概念,叫做逆質量,巧妙的解決了這個問題。逆質量其實就是質量的倒數,也就是 $\frac{1}{m}$,這樣的話,我們只需要將需要固定的物體的逆質量設定為0就可以使其的質量無窮大了,並且也避免了質量設定為0的情況。
class GameObj {
inverseMess: number // 質量不得為 0
constructor(inverseMess?: number) {
if (inverseMess >= 0) {
this.inverseMess = inverseMess
}
}
setInverseMess (inverseMess: number) {
if (inverseMess >= 0) {
this.inverseMess = inverseMess
}
}
}
結語
到這裡為止,我們所需要的最基礎的數學和物理知識已經成功的被注入了物理引擎中,但是這僅僅是一個物理引擎的基石,在此基礎上,我們還要新增各種各樣的東西,比如重力、阻力、動量、角動量、碰撞等等一系列的內容。在這後面還有很長的路要走,我將會在這個系列中一一展示。
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: