前言:
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如何設計和實現. 望一同努力.
寫在最后:
如果你覺得這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客能否給自己帶來一點小小的收益. 無論多少, 都是對樓主一種由衷的肯定.
