遊戲的物理和數學:Unity中的彈道和移動目標提早量計算

 

下載地址:https://www.jianguoyun.com/p/DZPN6ocQ2siRBhihnx8算法

 

 彈道計算是遊戲裏常見的問題,其中關於擊中移動目標的自動計算提早量的話題,看似簡單,其實仍是挺複雜的數學。網上這方面的資料還真很少,並且都是寫的含含糊糊。抽空總結一下本身的方法。

討論的前提是,假設目標是在3D空間裏以勻速直線方式運動。

1.直線彈道
在不考慮重力和空氣阻力影響的狀況下,子彈的彈道呈直線運動。這種狀況下,實際上是個純平面幾何空間的問題,不須要微積分和線代知識。
分析的狀況以下圖: 
 
        雖然在3D空間飛行,但火炮命中時,命中點和火炮位置、飛機初始位置處於一個三角形上,只須要平面幾何知識就能解決問題。在這個三角形中,飛機起始位置P和火炮T的位置是肯定的,飛機的飛行方向也是肯定的,因此θ角是已知的,D的長度也是已知的,F和G的長度雖然不知道,但在命中點H相遇的時候通過的時間t都是同樣的,因此F/G的比例實際等於二者速度的比例,而二者的速度都是已知的。這樣就能夠用高中的餘弦公式來解決這個求邊長的問題:
                    
其中V_p和V_g分別表明飛機的速度和炮彈飛行的速度,這是一個標準的1元2次方程,化簡、消元對某這麼一個非數學專業的來講太麻煩了,直接用求根公式求解吧,有好的化簡方法請指教。

 
在Unity中實現的方法:
函數

 1 Vector3 hitPoint = Vector3.zero;//存放命中點座標
 2 //假設飛機物體是aircraft,炮塔物體是gun 二者間的方向向量就是兩種世界座標相減
 3 Vector3 D = gun.transform.position - aircraft.transform.position;
 4 //用飛機transform的TransformDirection方法把前進方向變換到世界座標,就是飛機飛行的世界方向向量了
 5 Vector3 aircraftDirection = aircraft.transform.TransformDirection(Vector3.foward);
 6 //再用Vector3.Angle方法求出與飛機前進方向之間的夾角
 7 float THETA = Vector3.Angle(D,aircraftDirection);
 8 float DD = D.magnitude;//D是飛機炮塔間方向向量,D的magnitued就是兩種間距離
 9 float A =1-Mathf.Pow((gunVelocity/aircraftVelocity),2);//假設炮彈的速度是gunVeloctiy飛機的飛行線速度是aircraftVeloctiy
10 float B = -(2*DD*Mathf.Cos(THETA**Mathf.Deg2Rad));//要變換成弧度
11 float C = DD*DD;
12 float DELTA = B*B-4*A*C;
13 if (DELTA>=0){//若是DELTA小於0,無解
14    float F1 = (-B+Mathf.Sqrt(B*B-4*A*C))/(2*A);
15    float F2 = (-B-Mathf.Sqrt(B*B-4*A*C))/(2*A);
16    if(F1<F2)//取較小的一個
17    F1 = F2;
18    //命中點位置等於 飛機初始位置加上計算出F邊長度乘以飛機前進的方向向量,這個乘法等於把前進的距離變換成世界座標的位移
19    hitPoint = aircraft.transform.position + aircraftDirection * F1;       
20 }

 

假設你的炮彈是個Prefab叫projectilePrefab,帶有一個剛體,那麼能夠這樣生成炮彈實例:post

 

1 if(hitPoint != Vector3.zero){//若是有解
2     //生成一個炮彈實例,位置在炮塔的位置,方向是從炮塔指向命中點
3     GameObject obj =  (GameObject)Instantiate(projectilePrefab,gun.transform.position,Quaternion.LookRotation(hitPoint));
4     //假設muzzleVelocity是設定的炮彈速度(0,0,muzzleVelocity)表示往正z方向運動,用TransformDirection把這個速度變換成世界座標的速度向量
5    obj.rigidbody.velocity = obj.transform.TransformDirection(new Vector3(0,0,muzzleVelocity));
6 }

 

通過以上計算,炮彈能夠準確的命中飛行中的目標,只要目標是按照固定速度和方位角飛行的,能夠百發百中。固然也會有無解的狀況,因此計算的時候判斷了Delta,一共也就是幾條語句。

2.拋物線彈道
考慮進重力影響,炮彈的彈道就是一個拋物線方程,而目標仍是在3D空間的勻速直線運動,一個空間直線方程。
 

一個曲線方程和一個直線方程,以隱含參數t(飛行時間)求共同解(相交)問題,列方程組:

 
其中Vp和Vg分別表明飛機和炮彈飛行速度,角度Theta是炮彈射出時的仰角,t是飛行時間。這是個非齊次非線性隱含微分方程組,以某人的數學基礎,看不出有什麼特殊解的方法,用常規的迭代逼近求解吧,求達人提供更好方法。
迭代的過程大體是:
        1.用一組預測的xy落點座標帶入拋物線方程
       2.求出發射的角度和飛行時間t
       3.將時間帶t入直線方程,求出相應的xy座標
       4.將這個座標與以前猜想的xy座標進行比較,若是差值小於容許偏差,迭代結束返回結果
       5.若是差值大於偏差,將這個新的xy做爲下一次計算的預測xy,返回步驟1
   這個過程的物理含義能夠這樣理解:瞄準飛機如今的位置發射,等炮彈飛到的時候飛機已經往前飛行了一段距離,把炮彈飛行時間乘以飛機速度,獲得飛機在該時刻的實際位置,下次瞄準這個位置,再計算,由於目標變了,炮彈的飛行時間也變了,因此該時刻飛機位置也不一樣了,就這樣不停循環,炮彈落點追趕飛機位置,直到二者差距無窮小。

針對拋物線方程把t帶入,獲得:

 
而後用基礎代數方法進行推導化簡,再用通用求根公式獲得:
 
這樣θ角就能夠經過預測的落點座標、炮彈初速度、重力加速度g來求出。
迭代是有不少技巧的,這些內容須要複習大學微積分課程。好的迭代方法可以快速收斂,最大化的解釋運算開銷。望高數達人提供更佳的迭代方法。
在Unity中實現,有幾個核心思想:
   1.迭代體用函數遞歸來實現
   2.拋物線自己是2D曲線,因此其實不須要3重座標就能計算,每次運算時把z指向預測的落點,第3個座標能夠無視
   3.各類變換能夠快速的經過向量、矩陣運算獲得,很方便,不須要老是藉助transform
spa

 

 1 //拋物線方程 X Y表明預測落點,V表明炮彈初速,G是重力加速度 返回值是Vector2,其中x是發射角,y是飛行時間
 2 Vector2 formulaProjectile(float X,float Y,float V,float G){
 3 if(G ==0){//若是無重力 問題就成了簡單的三角函數 THETA等於atan(y/x) 飛行時間就等於(Y/sin(THETA))(斜邊長)再除以速度
 4     float THETA = Mathf.Atan(Y/X);
 5     float T = (Y/Mathf.Sin(THETA))/V;
 6     return(new Vector2(THETA,T));
 7 }else{//用上面的公式進行計算
 8     float DELTA = Mathf.Pow(V,4)-G*(G*X*X-2*Y*V*V);
 9     if(DELTA < 0){//DELTA小於0無解
10         return Vector2.zero;
11     }
12     float THETA1 = Mathf.Atan((-(V*V)+Mathf.Sqrt(DELTA))/(G*X));
13     float THETA2 = Mathf.Atan((-(V*V)-Mathf.Sqrt(DELTA))/(G*X));
14     if(THETA1>THETA2)//取較小值
15         THETA1 = THETA2;
16     float T = X/(V*Mathf.Cos(THETA1));//用拋物線水平運動方程計算飛行時間 比較簡單
17     return new Vector2(THETA1,T);
18     }
19 }
20 //目標運動的直線方程 VT是目標運動速度 PT是目標當前位置 DT是目標運動方向 TT是運動時間 返回值是目標通過時間TT之後的實際位置
21 Vector3 formulaTarget(float VT,Vector3 PT,Vector3 DT,float TT){
22 //簡單的一句話搞定直線方程計算 目標實際位置=目標當前位置+目標運動方向向量*(目標飛行速度*目標飛行時間)
23     return PT + DT * (VT * TT);
24 }
25 //主迭代函數 參數灰常多 用於算法演示 實際使用是能夠簡化的
26 //gunVelocity:炮彈初速度 gunPosition:炮塔世界座標 aircraftVelocity:飛機線速度 aircraftPosition:飛機當前位置世界座標
27 //aircraftDirection:飛機飛行方向向量 hitPoint:預測的命中點 G:重力加速度 accuracy:計算精度 小於這個值認爲計算完成 diff:上次迭代的差值
28 //返回值是炮塔發射時瞄準點的座標(注意不是實際命中點)
29 Vector3 calculateNoneLinearTrajectory(float gunVelocity,Vector3 gunPosition,float aircraftVelocity,
30 Vector3 aircraftPosition,Vector3 aircraftDirection,Vector3 hitPoint,float G,float accuracy,float diff){
31 //若是預測命中點是0 無解 返回0
32 if(hitPoint == Vector3.zero){
33     return Vector3.zero;
34 }
35 //把炮塔正z指向預測命中點在炮塔高度的一個水平面上的投影點
36 //這樣就構造了一個以炮塔爲原點,以重力方向爲-y軸 以炮塔正前方爲x軸的標準拋物線2D座標系,這個要本身體會下
37 Vector3 gunDirection = new Vector3(hitPoint.x,gunPosition.y,hitPoint.z) - gunPosition;
38 //構造一個從世界座標到炮塔座標的旋轉矩陣
39 Quaternion gunRotation = Quaternion.FromToRatation(gunDirection,Vector3.forward);
40 //把預測命中點變換到炮塔座標(減法是計算相對座標差,再旋轉到炮塔當前座標來)
41 Vector3 localHitPoint = gunRotation * (hitPoint - gunPosition);
42 float V = gunVelocity;
43 float X = localHitPoint.z;//注視方向 前方是z,也就是拋物線座標裏的X
44 float Y = localHitPoint.y;
45 Vector2 TT = formulaProjectile(X,Y,V,G);//用拋物線方程計算射擊仰角和飛行時間
46 if(TT == Vector2.zero){//若是無解 返回
47     return Vector3.zero;
48 }
49 float VT = aircraftVelocity;
50 Vector3 PT = aircraftPosition;
51 Vector3 DT = aircraftDirection;
52 float T = TT.y;//TT的y是用拋物線方程計算出的彈丸飛行時間
53 Vector3 newHitPoint = formulaTarget(VT,PT,DT,T);//帶入直線方程計算目標實際位置 注意目標的計算是在3D世界座標進行的
54 float diff1 = (newHitPoint - hitPoint).magnitude;//判斷預測點和實際目標位置的距離
55 if (diff1 > diff){//若是距離大於上一次計算的距離 那麼要麼迭代算法有問題 是發散的 要麼就無解 返回0
56     return Vector3.zero;
57 }
58 if(diff1<accuracy){//若是距離小於但願的精度 找到結果 返回瞄準點 炮彈是拋物線 發射時不能瞄準命中點 要計算瞄準點
59     gunRotation = Quaternion.Inverse(gunRotation);//把剛纔構造的旋轉矩陣進行逆變換,從炮塔座標變回世界座標
60     Y = Mathf.Tan(TT.x)*X;//TT的x是炮彈射出的仰角tan(仰角)*水平距離=垂直高度了(三角函數),這纔是瞄準點的高度
61    return gunRotation * new Vector3(0,Y,X) + gunPosition;//把瞄準點變換回世界座標 注意X實際上是Z
62 }
63 //即不是無解 也未達到精度要求 遞歸調用繼續迭代 其中預測命中點用目標軌跡方程計算出的新位置取代 參考差值用本次計算的差值取代
64 return calculateNoneLinearTrajectory(gunVelocity,gunPosition,aircraftVelocity,aircraftPosition,aircraftDirection,newHitPoint,G,accuracy,diff1);
65 }

 

一個炮彈運動軌跡方程 一個目標運動軌跡方程,加一個迭代函數,就能完成計算拋物線彈道命中直線勻速移動目標的問題。實際使用的時候,能夠先用方法1直線彈道算出一個命中點,做爲初始預測點帶入進行迭代,能夠減小迭代次數。過程裏使用了大量簡化的向量和矩陣運算,對這部分不熟的讀起來可能費勁。
在幾公里範圍之內的飛機,飛行速度在300-700km/h,炮彈出膛速度在500m/s(2戰水平,其實高射炮出膛速度不止這麼點),命中精度10m之內的前提下,基本上4次迭代之內能夠完成。

3.更多複雜因素的計算
   在實際狀況中,還可能有更多的影響。好比目標不是勻速直線而是加速運動或者曲線運動,好比空氣阻力對彈道的影響,彈丸質心不在幾何中心時與重力、空間阻力夾角產生的偏轉力矩,炮彈在移動的平臺上射擊移動的目標。另外炮彈出射點是從炮口算起,在旋轉炮塔和炮管的狀況下,這個出射點實際上是個球面軌跡而不是個固定點。火炮發現目標到炮口轉動到合適位置的時間裏,目標又發生了位移,因此還要計算這個炮塔旋轉的提早量。這一系列的複雜問題其實均可以經過聯立方程組,而後迭代求解的方法實現,原理徹底同樣,只是計算複雜度大大增長。

好比在考慮空氣阻力等狀況下,炮彈的軌跡方程會是這種形式:        

 
這些影響因素多是線性的、2次乃至高次的。根據上面的算法,只要把拋物線方程組變成這個新的高次方程組求解,也能夠適用。
再好比目標飛行的不是直線而是圓形,那麼把目標的方程組變換成圓方程,也能夠適用。固然在目標軌跡是非線性軌跡的狀況下,迭代就不能用這種線性的迭代了,不然迭代結果會一會收斂一會發散,常規的方法是用目標軌跡函數的導函數計算迭代,這部分實在很難作到通用,須要根據具體狀況調整。

給出一個概念方程組:
 
把這些方程組中按照影響關係進行屢次分步迭代,最終能獲得合適的解或者判斷無解。這組方程能夠應對各類複雜狀況的組合,實際上軍事上火控系統正是這樣計算的。不過在遊戲中,一般不須要這麼複雜的計算,只要簡單的模擬就足夠了,因此只是從概念層面討論一下,若是這些複雜因素都考慮進去,徹底能夠做成一個可視的軍事仿真的程序來了。

慣例...只寫原理不付DEMO大體是沒多少人看的,附上本身寫的demo

Reset Target:設置目標飛機以隨機方位角和速度飛行
Gravity ON/OFF:設置是否開啓重力
Aiming Mode: Manual/Auto 設置瞄準模式人工/自動
人工模式下:wsad鍵上下左右旋轉炮管方位角 空格鍵擊發
自動模式下:炮管自動瞄準提早量瞄準點,空格擊發 百發百中
AutoCam ON/OFF:設置飛機小鏡頭的顯示模式,關閉自動鏡頭能夠用按鈕調整鏡頭的角度 + -按鈕縮放鏡頭


Change Focus:改變主鏡頭焦點,在高射炮和目標飛機之間切換,以飛機爲焦點時的鏡頭:

 
若是沒法命中,會發出提示音效
 
不一樣視角下的效果:百發百中的彈道 只給了炮彈一個初速度和方向 而後靠碰撞檢測顯示爆炸效果,過程徹底靠物理引擎控制。




人工發射模式:

 

設置參數:
速度的單位都是m/s,長度單位都是m
Range是飛機初始位置的範圍
Gravity Modifier:重力縮放因子,由於場景和模型是1:10比例,因此重力設置爲1/10,不然就像玩具,模擬的效果不會真實
其餘參數都會用這個因子縮放,因此按照實際狀況設置就行。
code

相關文章
相關標籤/搜索