彈道計算是游戲里常見的問題,其中關於擊中移動目標的自動計算提前量的話題,看似簡單,其實還是挺復雜的數學。網上這方面的資料還真不多,而且都是寫的含含糊糊。抽空總結一下自己的方法。
討論的前提是,假設目標是在3D空間里以勻速直線方式運動。
1.直線彈道
在不考慮重力和空氣阻力影響的情況下,子彈的彈道呈直線運動。這種情況下,其實是個純平面幾何空間的問題,不需要微積分和線代知識。
分析的情況如下圖:
圖片:tj-1.jpg
雖然在3D空間飛行,但火炮命中時,命中點和火炮位置、飛機初始位置處於一個三角形上,只需要平面幾何知識就能解決問題。在這個三角形中,飛機起始位置P和火炮T的位置是確定的,飛機的飛行方向也是確定的,所以θ角是已知的,D的長度也是已知的,F和G的長度雖然不知道,但在命中點H相遇的時候經過的時間t都是一樣的,所以F/G的比例實際等於兩者速度的比例,而兩者的速度都是已知的。這樣就可以用高中的余弦公式來解決這個求邊長的問題:
圖片:tj02.jpg
其中V_p和V_g分別代表飛機的速度和炮彈飛行的速度,這是一個標准的1元2次方程,化簡、消元對某這么一個非數學專業的來說太麻煩了,直接用求根公式求解吧,有好的化簡方法請指教。
圖片:tj03.jpg
在Unity中實現的方法:
復制代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
Vector3 hitPoint = Vector3.zero;//存放命中點坐標
//假設飛機物體是aircraft,炮塔物體是gun 兩者間的方向向量就是兩種世界坐標相減
Vector3 D = gun.transform.position - aircraft.transform.position;
//用飛機transform的TransformDirection方法把前進方向變換到世界坐標,就是飛機飛行的世界方向向量了
Vector3 aircraftDirection = aircraft.transform.TransformDirection(Vector3.foward);
//再用Vector3.Angle方法求出與飛機前進方向之間的夾角
float THETA = Vector3.Angle(D,aircraftDirection);
float DD = D.magnitude;//D是飛機炮塔間方向向量,D的magnitude就是兩種間距離
float A =1-Mathf.Pow((gunVelocity/aircraftVelocity),2);//假設炮彈的速度是gunVeloctiy飛機的飛行線速度是aircraftVeloctiy
float B = -(2*DD*Mathf.Cos(THETA**Mathf.Deg2Rad));//要變換成弧度
float C = DD*DD;
float DELTA = B*B-4*A*C;
if (DELTA>=0){//如果DELTA小於0,無解
float F1 = (-B+Mathf.Sqrt(B*B-4*A*C))/(2*A);
float F2 = (-B-Mathf.Sqrt(B*B-4*A*C))/(2*A);
if(F1
F1 = F2;
//命中點位置等於 飛機初始位置加上計算出F邊長度乘以飛機前進的方向向量,這個乘法等於把前進的距離變換成世界坐標的位移
hitPoint = aircraft.transform.position + aircraftDirection * F1;
}
|
假設你的炮彈是個Prefab叫projectilePrefab,帶有一個剛體,那么可以這樣生成炮彈實例:
復制代碼
1
2
3
4
5
6
|
if(hitPoint != Vector3.zero){//如果有解
//生成一個炮彈實例,位置在炮塔的位置,方向是從炮塔指向命中點
GameObject obj = (GameObject)Instantiate(projectilePrefab,gun.transform.position,Quaternion.LookRotation(hitPoint));
//假設muzzleVelocity是設定的炮彈速度(0,0,muzzleVelocity)表示往正z方向運動,用TransformDirection把這個速度變換成世界坐標的速度向量
obj.rigidbody.velocity = obj.transform.TransformDirection(new Vector3(0,0,muzzleVelocity));
}
|
經過以上計算,炮彈可以准確的命中飛行中的目標,只要目標是按照固定速度和方位角飛行的,可以百發百中。當然也會有無解的情況,所以計算的時候判斷了Delta,一共也就是幾條語句。
2.拋物線彈道
考慮進重力影響,炮彈的彈道就是一個拋物線方程,而目標還是在3D空間的勻速直線運動,一個空間直線方程。
圖片:tj04.jpg
一個曲線方程和一個直線方程,以隱含參數t(飛行時間)求共同解(相交)問題,列方程組:
圖片:tj05.jpg
其中Vp和Vg分別代表飛機和炮彈飛行速度,角度Theta是炮彈射出時的仰角,t是飛行時間。這是個非齊次非線性隱含微分方程組,以某人的數學基礎,看不出有什么特殊解的方法,用常規的迭代逼近求解吧,求達人提供更好方法。
迭代的過程大致是:
1.用一組預測的xy落點坐標帶入拋物線方程
2.求出發射的角度和飛行時間t
3.將時間帶t入直線方程,求出相應的xy坐標
4.將這個坐標與之前猜測的xy坐標進行比較,如果差值小於允許誤差,迭代結束返回結果
5.如果差值大於誤差,將這個新的xy作為下一次計算的預測xy,返回步驟1
這個過程的物理含義可以這樣理解:瞄准飛機現在的位置發射,等炮彈飛到的時候飛機已經往前飛行了一段距離,把炮彈飛行時間乘以飛機速度,得到飛機在該時刻的實際位置,下次瞄准這個位置,再計算,因為目標變了,炮彈的飛行時間也變了,所以該時刻飛機位置也不同了,就這樣不停循環,炮彈落點追趕飛機位置,直到兩者差距無窮小。
針對拋物線方程把t帶入,得到:
圖片:tj06.jpg
然后用基礎代數方法進行推導化簡,再用通用求根公式得到:
圖片:tj07.jpg
這樣θ角就可以通過預測的落點坐標、炮彈初速度、重力加速度g來求出。
迭代是有很多技巧的,這些內容需要復習大學微積分課程。好的迭代方法能夠快速收斂,最大化的解釋運算開銷。望高數達人提供更佳的迭代方法。
在Unity中實現,有幾個核心思想:
1.迭代體用函數遞歸來實現
2.拋物線本身是2D曲線,所以其實不需要3重坐標就能計算,每次運算時把z指向預測的落點,第3個坐標可以無視
3.各種變換可以快速的通過向量、矩陣運算得到,很方便,不需要總是借助transform
復制代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
//拋物線方程 X Y代表預測落點,V代表炮彈初速,G是重力加速度 返回值是Vector2,其中x是發射角,y是飛行時間
Vector2 formulaProjectile(float X,float Y,float V,float G){
if(G ==0){//如果無重力 問題就成了簡單的三角函數 THETA等於atan(y/x) 飛行時間就等於(Y/sin(THETA))(斜邊長)再除以速度
float THETA = Mathf.Atan(Y/X);
float T = (Y/Mathf.Sin(THETA))/V;
return(new Vector2(THETA,T));
}else{//用上面的公式進行計算
float DELTA = Mathf.Pow(V,4)-G*(G*X*X-2*Y*V*V);
if(DELTA < 0){//DELTA小於0無解
return Vector2.zero;
}
float THETA1 = Mathf.Atan((-(V*V)+Mathf.Sqrt(DELTA))/(G*X));
float THETA2 = Mathf.Atan((-(V*V)-Mathf.Sqrt(DELTA))/(G*X));
if(THETA1>THETA2)//取較小值
THETA1 = THETA2;
float T = X/(V*Mathf.Cos(THETA1));//用拋物線水平運動方程計算飛行時間 比較簡單
return new Vector2(THETA1,T);
}
}
//目標運動的直線方程 VT是目標運動速度 PT是目標當前位置 DT是目標運動方向 TT是運動時間 返回值是目標經過時間TT以后的實際位置
Vector3 formulaTarget(float VT,Vector3 PT,Vector3 DT,float TT){
//簡單的一句話搞定直線方程計算 目標實際位置=目標當前位置+目標運動方向向量*(目標飛行速度*目標飛行時間)
return PT + DT * (VT * TT);
}
//主迭代函數 參數灰常多 用於算法演示 實際使用是可以簡化的
//gunVelocity:炮彈初速度 gunPosition:炮塔世界坐標 aircraftVelocity:飛機線速度 aircraftPosition:飛機當前位置世界坐標
//aircraftDirection:飛機飛行方向向量 hitPoint:預測的命中點 G:重力加速度 accuracy:計算精度 小於這個值認為計算完成 diff:上次迭代的差值
//返回值是炮塔發射時瞄准點的坐標(注意不是實際命中點)
Vector3 calculateNoneLinearTrajectory(float gunVelocity,Vector3 gunPosition,float aircraftVelocity,
Vector3 aircraftPosition,Vector3 aircraftDirection,Vector3 hitPoint,float G,float accuracy,float diff){
//如果預測命中點是0 無解 返回0
if(hitPoint == Vector3.zero){
return Vector3.zero;
}
//把炮塔正z指向預測命中點在炮塔高度的一個水平面上的投影點
//這樣就構造了一個以炮塔為原點,以重力方向為-y軸 以炮塔正前方為x軸的標准拋物線2D坐標系,這個要自己體會下
Vector3 gunDirection = new Vector3(hitPoint.x,gunPosition.y,hitPoint.z) - gunPosition;
//構造一個從世界坐標到炮塔坐標的旋轉矩陣
Quaternion gunRotation = Quaternion.FromToRatation(gunDirection,Vector3.forward);
//把預測命中點變換到炮塔坐標(減法是計算相對坐標差,再旋轉到炮塔當前坐標來)
Vector3 localHitPoint = gunRotation * (hitPoint - gunPosition);
float V = gunVelocity;
float X = localHitPoint.z;//注視方向 前方是z,也就是拋物線坐標里的X
float Y = localHitPoint.y;
Vector2 TT = formulaProjectile(X,Y,V,G);//用拋物線方程計算射擊仰角和飛行時間
if(TT == Vector2.zero){//如果無解 返回
return Vector3.zero;
}
float VT = aircraftVelocity;
Vector3 PT = aircraftPosition;
Vector3 DT = aircraftDirection;
float T = TT.y;//TT的y是用拋物線方程計算出的彈丸飛行時間
Vector3 newHitPoint = formulaTarget(VT,PT,DT,T);//帶入直線方程計算目標實際位置 注意目標的計算是在3D世界坐標進行的
float diff1 = (newHitPoint - hitPoint).magnitude;//判斷預測點和實際目標位置的距離
if (diff1 > diff){//如果距離大於上一次計算的距離 那么要么迭代算法有問題 是發散的 要么就無解 返回0
return Vector3.zero;
}
if(diff1
gunRotation = Quaternion.Inverse(gunRotation);//把剛才構造的旋轉矩陣進行逆變換,從炮塔坐標變回世界坐標
Y = Mathf.Tan(TT.x)*X;//TT的x是炮彈射出的仰角tan(仰角)*水平距離=垂直高度了(三角函數),這才是瞄准點的高度
return gunRotation * new Vector3(0,Y,X) + gunPosition;//把瞄准點變換回世界坐標 注意X其實是Z
}
//即不是無解 也未達到精度要求 遞歸調用繼續迭代 其中預測命中點用目標軌跡方程計算出的新位置取代 參考差值用本次計算的差值取代
return calculateNoneLinearTrajectory(gunVelocity,gunPosition,aircraftVelocity,aircraftPosition,aircraftDirection,newHitPoint,G,accuracy,diff1);
}
|
一個炮彈運動軌跡方程 一個目標運動軌跡方程,加一個迭代函數,就能完成計算拋物線彈道命中直線勻速移動目標的問題。實際使用的時候,可以先用方法1直線彈道算出一個命中點,作為初始預測點帶入進行迭代,可以減少迭代次數。過程里使用了大量簡化的向量和矩陣運算,對這部分不熟的讀起來可能費勁。
在幾千米范圍以內的飛機,飛行速度在300-700km/h,炮彈出膛速度在500m/s(2戰水平,其實高射炮出膛速度不止這么點),命中精度10m以內的前提下,基本上4次迭代以內可以完成。
3.更多復雜因素的計算
在實際情況中,還可能有更多的影響。比如目標不是勻速直線而是加速運動或者曲線運動,比如空氣阻力對彈道的影響,彈丸質心不在幾何中心時與重力、空間阻力夾角產生的偏轉力矩,炮彈在移動的平台上射擊移動的目標。另外炮彈出射點是從炮口算起,在旋轉炮塔和炮管的情況下,這個出射點其實是個球面軌跡而不是個固定點。火炮發現目標到炮口轉動到合適位置的時間里,目標又發生了位移,所以還要計算這個炮塔旋轉的提前量。這一系列的復雜問題其實都可以通過聯立方程組,然后迭代求解的方法實現,原理完全一樣,只是計算復雜度大大增加。
比如在考慮空氣阻力等情況下,炮彈的軌跡方程會是這種形式:
圖片:tj08.jpg
這些影響因素可能是線性的、2次乃至高次的。根據上面的算法,只要把拋物線方程組變成這個新的高次方程組求解,也可以適用。
再比如目標飛行的不是直線而是圓形,那么把目標的方程組變換成圓方程,也可以適用。當然在目標軌跡是非線性軌跡的情況下,迭代就不能用這種線性的迭代了,否則迭代結果會一會收斂一會發散,常規的方法是用目標軌跡函數的導函數計算迭代,這部分實在很難做到通用,需要根據具體情況調整。
給出一個概念方程組:
圖片:tj09.jpg
把這些方程組中按照影響關系進行多次分步迭代,最終能得到合適的解或者判斷無解。這組方程可以應對各種復雜情況的組合,實際上軍事上火控系統正是這樣計算的。不過在游戲中,通常不需要這么復雜的計算,只要簡單的模擬就足夠了,所以只是從概念層面討論一下,如果這些復雜因素都考慮進去,完全可以作成一個可視的軍事仿真的程序來了。
慣例...只寫原理不付DEMO大致是沒多少人看的,附上自己寫的demo
圖片:tj10.jpg
Reset Target:設置目標飛機以隨機方位角和速度飛行
Gravity ON/OFF:設置是否開啟重力
Aiming Mode: Manual/Auto 設置瞄准模式人工/自動
人工模式下:wsad鍵上下左右旋轉炮管方位角 空格鍵擊發
自動模式下:炮管自動瞄准提前量瞄准點,空格擊發 百發百中
AutoCam ON/OFF:設置飛機小鏡頭的顯示模式,關閉自動鏡頭可以用按鈕調整鏡頭的角度 + -按鈕縮放鏡頭
圖片:tj12.jpg
圖片:tj17.jpg
Change Focus:改變主鏡頭焦點,在高射炮和目標飛機之間切換,以飛機為焦點時的鏡頭:
圖片:tj11.jpg
如果無法命中,會發出提示音效
不同視角下的效果:百發百中的彈道 只給了炮彈一個初速度和方向 然后靠碰撞檢測顯示爆炸效果,過程完全靠物理引擎控制。
圖片:tj13.jpg
圖片:tj14.jpg
圖片:tj16.jpg
圖片:tj19.jpg
人工發射模式:
圖片:tj15.jpg
圖片:tj20.jpg
設置參數:
速度的單位都是m/s,長度單位都是m
Range是飛機初始位置的范圍
Gravity