前言:
08年的時候, 寫過一個台球游戲, 用的是java, 不過代碼真的是用傳說中的神器notepad寫的(你信嗎? 其實是用GVIM寫的, ^_^), 很多類都在同一java文件中編寫. 可見當時的JAVA水平真的不咋地, 時過進遷, 還是一樣的不咋地.
這邊是當時的CSDN下載鏈接: java(台球游戲), 實現比較簡單. 后來寫過一個版本, 比這個要強大許多, 可惜源碼丟失了.
效果展示入下圖所示:
本文想講述下台球游戲中核心算法的實現, 以及游戲AI的設計技巧. 當然自己也有個小願望, 希望能實現一個html5版的台球游戲.
基礎物理知識:
• 摩擦阻力
其滿足牛頓第二定律:
f = m * a
速度與加速度關系公式:
vt = v0 + a * t
地面摩擦力與運動物體的方向相反, 阻礙物體的向前運動.
• 動量守恆
假設物體A質量為m1, 速度為v1, 物體B質量為m2, 速度為v2, 碰撞后速度分別為v1', v2'.
則滿足動量守恆定律:
m1 * v1 + m2 * v2 = m1 * v1' + m2 * v2'
• 碰撞類型和能量守恆定律
1). 完全彈性碰撞
動能沒有損失, 則滿足如下公式:
1/2 * m1 * v1^2 + 1/2 * m2 * v2^2 = 1/2 * m1 * v1'^2 + 1/2 * m2 * v2'^2
注: 前后物體的動能保持均衡, 沒有其他能量的轉化.
結合之前的動量守恆定律, 我們可以進一步得到:
v1' = [(m1-m2) * v1 + 2 * m2 * v2] / (m1 + m2)
v2' = [(m2-m1) * v2 + 2 * m1 * v1] / (m1 + m2)
2). 完全非彈性碰撞
則存在其他能量的轉化, 動能不守恆.
且此時兩物體粘連, 速度一致, 即v1'=v2', 此時動能損失最大.
3). 彈性碰撞
介於完全彈性碰撞和完全非彈性碰撞兩者之間. 動能有損失的.
物理模型:
台球游戲中, 最核心的就是其物理模型的抽象及其碰撞算法的執行過程了.
鑒於是2D版的台球游戲, 因此我們對物理模型做下簡化, 球運動的方向必然穿越球的中心.
把每個台球抽象為圓(x, y, radius), 而台球桌邊框抽象為線段((x1, y1), (x2, y2)).
• 碰撞檢測
1). 檢測球與球碰撞
我們假定球A(x1, y1, r), 球B(x2, y2, r). 則滿足條件:
(x1 - x2) ^ 2 + (y1 - y2) ^ 2 <= (2*r) ^ 2
則發生碰撞, 否則沒有發生碰撞
2). 檢測球與球台邊框碰撞
相對比較簡單. 求球心到邊框的垂直距離即可, 若小於等於則發生碰撞, 若大於則沒有.
• 碰撞反應
1). 球與球的碰撞反應
動量是向量, 其在正交的兩個方向上, 互相守恆. 我們選取兩球圓心的直線為x軸, 垂直於圓心直線的為y軸. 如上圖所述.
x軸上滿足動量守恆:
m1 * Vx + m2 * Ux = m1 * Vx' + m2 * Ux';
並假定兩球碰撞是完全彈性碰撞, 兩球質量相等m1=m2, 依據基礎物理知識篇的結論.
Vx' = [(m1-m2) * Vx + 2 * m2 * Ux] / (m1 + m2) = Ux;
Ux' = [(m2-m1) * Ux + 2 * m1 * Vx] / (m1 + m2) = Vx;
在X軸方向, 兩球交換速度, 而在Y軸方向, 兩球分速度不變.
Vy' = Vy;
Uy' = Uy;
最終碰撞后的速度公式為:
V' = Vx' + Vy' = Ux + Vy;
U' = Ux' + Uy' = Vx + Uy;
2). 球與邊框的碰撞反應
把台球邊框視為質量無窮大, 則簡單把運動的球, 其在垂直邊框的分方向反向即可.
假定碰撞碰撞平面為x軸
Vx' = Vx;
Vy' = -Vy;
最終速度公式為:
V' = Vx' + Vy' = Vx - Vy;
碰撞執行算法:
游戲的主循環往往遵循如下代碼結構:
while ( true ) { game.update(time_interval); game.render(); }
這個時間間隔(time_interval), 由游戲的FPS來確定. 以24幀為例, 每40毫秒刷新一次.
對於台球本身而言, 若以該time_interval為更新周期, 使得運動的球體滿足:
Vt = V0 + a * t
運行距離為:
S = V0 * t + 1/2 * a * t^2.
然后來檢測球體是否發生了碰撞, 然后進行碰撞反應處理. 看似沒有問題.
但是當球體初速度很快時, 在time_interval中有可能, 發生穿越現象.
如下圖所展示的現象:
紫色球在t2時刻, 和藍球檢測到碰撞, 但實際上, 在紫球在t1~t2之間的某時刻和藍球發生了碰撞.
為了解決該問題, 在具體的算法中, 需要引入更細的時間分片slice, 該過程在具體的update中進行模擬.
整個台球場景的更新函數:
void update(time_interval) { while time_interval > 0: // 碰撞檢測 if detectionCollide(time_interval, least_time, ball_pairs): // 游戲更新least_time billiards.update(least_time) // 對碰撞的兩球進行碰撞反應 collideReaction(ball_pairs=>(ball, other)) // time_interval 減少 least_time time_interval -= least_time else: // 游戲更新least_time billiards.update(time_interval) time_interval = 0 }
注: 碰撞反應, 按物理模型篇講述的來.
而具體的碰撞檢測算法為:
/* @brief 在time_interval 時間內, 返回最先碰撞的球或台球邊, 以及時間點 */ bool detectionCollide(time_interval, least_time, ball_pairs) { res = false; least_time = time_interval; foreach ball in billiards: foreach otherBall in billiards: // 求出兩球的距離 S = distance(ball, otherBall) // 以某一球作為參考坐標系, 則令一球速度向量變為 U’=U-V // 在圓心的直線作為x軸 Ux(relative) = Ux(other ball) - Vx(ball) // 若該方向使得兩球遠離, 則直接忽略 if Ux(relative) < 0: continue // 某該方向使得兩球接近, 則可求其碰撞的預期時間點 A' = 2 * A; // 加速度為原來的兩倍 // 取兩者最小的時間點 delta_time = min(time_interval, Ux(relative) / Ax’) // 預期距離 小於 兩球距離,則在time_interval中不會發生碰撞 if 1/2 * Ax’ * delta_time ^ 2 + Ux(relative) * delta_time < S - 2*r: continue // 解一元二次方程, 使用二分搜索逼近求解 res_time <= slove(1/2 * Ax’ * x ^ 2 + Ux(relative) * x = S - 2 * r) if res_time < least_time: ball_pairs <= (ball, otherBall) least_time = res_time res = true foreach wall in billiards: S = distance(ball, wall) // 設垂直於平面的方向為x軸 if Vx < 0: continue // 取兩者最小的時間點 delta_time = min(time_interval, Vx / Ax) // 預期距離 小於 兩球距離,則在time_interval中不會發生碰撞 if 1/2 * Ax * delta_time ^ 2 + Vx * delta_time < S - r: continue // 解一元二次方程, 使用二分搜索逼近求解 res_time <= slove(1/2 * A * x ^ 2 + Vx * x = S - r) if res_time < least_time: ball_pairs <= (ball, walll) least_time = res_time res = true return res }
注: 對於一元二次方程, 也可以借助分1000個細粒度時間片, 然后計算逼近求解.
台球模擬碰撞算法過程, 大致就是如上所述.
計算最復雜的時刻, 其實就是開球, 打散一堆球的時候.
總結:
本文參考了"NEHE的OPENGL中文教程 第30課 碰撞檢測與模型運動". 當然實現台球游戲, 未必真的需要該算法, 很多開發者直接使用box2d就能完美並輕松的實現. 參考"使用 cocos2d-x Box2d 的實現". 后續的文章, 想講述下台球游戲的AI如何設計和實現. 望一同努力.
寫在最后:
如果你覺得這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客能否給自己帶來一點小小的收益. 無論多少, 都是對樓主一種由衷的肯定.