[unity 教程]游戏的物理和数学:Unity 中的弹道和移动⽬标提前
量计算
弹道计算是游戏⾥常见的问题,其中关于击中移动⽬标的⾃动计算提前量的话题,看似简单,其实还是挺复杂的数学。⽹上这⽅⾯的资料还真不多,⽽且都是写的含含糊糊。抽空总结⼀下⾃⼰的⽅法。
讨论的前提是,假设⽬标是在3D 空间⾥以匀速直线⽅式运动。
1.直线弹道
在不考虑重⼒和空⽓阻⼒影响的情况下,⼦弹的弹道呈直线运动。这种情况下,其实是个纯平⾯⼏何空间的问题,不需要微积分和线代知识。 分析的情况如下图:
虽然在3D 空间飞⾏,但⽕炮命中时,命中点和⽕炮位置、飞机初始位置处于⼀个三⾓形上,只需要平⾯⼏何知识就能解决问题。在这个三⾓形中,飞机起始位置P 和⽕炮T 的位置是确定的,飞机的飞⾏⽅向也是确定的,所以θ⾓是已知的,D 的长度也是已知的,F 和G 的长度虽然不知道,但在命中点H 相遇的时候经过的时间t 都是⼀样的,所以F/G 的⽐例实际等于两者速度的⽐例,⽽两者的速度都是已知的。这样就可以⽤⾼中的余弦公式来解决这个求边长的问题:
其中V_p 和V_g 分别代表飞机的速度和炮弹飞⾏的速度,这是⼀个标准的1元2次⽅程,化简、消元对某这么⼀个⾮数学专业的来说太⿇烦了,直接⽤求根公式求解吧,有好的化简⽅法请指教。
在Unity 中实现的⽅法:
假设你的炮弹是个Prefab 叫projectilePrefab,带有⼀个刚体,那么可以这样⽣成炮弹实例:
经过以上计算,炮弹可以准确的命中飞⾏中的⽬标,只要⽬标是按照固定速度和⽅位⾓飞⾏的,可以百发百中。当然也会有⽆解的情况,所以计算的时候判断了Delta, ⼀共也就是⼏条语句。
2.抛物线弹道
考虑进重⼒影响,炮弹的弹道就是⼀个抛物线⽅程,⽽⽬标还是在3D 空间的匀速直线运动,⼀个空间直线⽅程。 123456789
101112131415161718
1920Vector3 hitPoint = ;//存放命中点坐标//假设飞机物体是aircraft,炮塔物体是gun 两者间的⽅向向量就是两种世界坐标相减Vector3 D = ansform.position - ansform.position;//⽤飞机transform 的TransformDirection ⽅法把前进⽅向变换到世界坐标,就是飞机飞⾏的世界⽅向向
量了Vector3 aircraftDirection = ansform.TransformDirection(Vector3.foward);//再⽤Vector3.Angle ⽅法求出与飞机前进⽅向之间的夹⾓float THETA = Vector3.Angle(D,aircraftDirection);float DD = D.magnitude;//D 是飞机炮塔间⽅向向量,D 的magnitued 就是两种间距离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<F2)//取较⼩的⼀个 F1 = F2; //命中点位置等于 飞机初始位置加上计算出F 边长度乘以飞机前进的⽅向向量,这个乘法等于把前进的距离变换成世界坐标的位移 hitPoint = ansform.position + aircraftDirection * F1; }
123
4
5
6if(hitPoint != ){//如果有解 //⽣成⼀个炮弹实例,位置在炮塔的位置,⽅向是从炮塔指向命中点 GameObject obj = (GameObject)Instantiate(ansform.position,Quater
nion.LookRotation(hitPoint)); //假设muzzleVelocity 是设定的炮弹速度(0,0,muzzleVelocity)表⽰往正z ⽅向运动,⽤TransformDirection 把这个速度变换成世界坐标的速度向量 obj.rigidbody.velocity = ansform.TransformDirection(new Vector3(0,0,muzzleVelocity));}
⼀个曲线⽅程和⼀个直线⽅程,以隐含参数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
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//抛物线⽅程 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⽆解
;
}
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 == ){
unity 教程;
}
//把炮塔正z指向预测命中点在炮塔⾼度的⼀个⽔平⾯上的投影点
//这样就构造了⼀个以炮塔为原点,以重⼒⽅向为-y轴以炮塔正前⽅为x轴的标准抛物线2D坐标系,这个要⾃⼰体会下
Vector3 gunDirection = new Vector3(hitPoint.x,gunPosition.y,hitPoint.z) - gunPosition;
⼀个炮弹运动轨迹⽅程 ⼀个⽬标运动轨迹⽅程,加⼀个迭代函数,就能完成计算抛物线弹道命中直线匀速移动⽬标的问题。实际使⽤的时候,可以先⽤⽅法1直线弹道算出⼀个命中点,作为初始预测点带⼊进⾏迭代,可以减少迭代次数。过程⾥使⽤了⼤量简化的向量和矩阵运算,对这部分不熟的读起来可能费劲。 在⼏千⽶范围以内的飞机,飞⾏速度在300-700km/h,炮弹出膛速度在500m/s (2战⽔平,其实⾼射炮出膛速度不⽌这么点),命中精度10m 以内的前提下,基本上4次迭代以内可以完成。
3.更多复杂因素的计算
在实际情况中,还可能有更多的影响。⽐如⽬标不是匀速直线⽽是加速运动或者曲线运动,⽐如空⽓阻⼒对弹道的影响,弹丸质⼼不在⼏何中⼼时与重⼒、空间阻⼒夹⾓产⽣的偏转⼒矩,炮弹在移动的平台上射击移动的⽬标。另外炮弹出射点是从炮⼝算起,在旋转炮塔和炮管的情况下,这个出射点其实是个球⾯轨迹⽽不是个固定点。⽕炮发现⽬标到炮⼝转动到合适位置的时间⾥,⽬标⼜发⽣了位移,所以还要计算这个炮塔旋转的提前量。这⼀系列的复杂问题其实都可以通过联⽴⽅程组,然后迭代求解的⽅法实现,原理完全⼀样,只是计算复杂度⼤⼤增加。
⽐如在考虑空⽓阻⼒等情况下,炮弹的轨迹⽅程会是这种形式:
这些影响因素可能是线性的、2次乃⾄⾼次的。 根据上⾯的算法,只要把抛物线⽅程组变成这个新的⾼次⽅程组求解,也可以适⽤。
再⽐如⽬标飞⾏的不是直线⽽是圆形,那么把⽬标的⽅程组变换成圆⽅程,也可以适⽤。当然在⽬标轨迹是⾮线性轨迹的情况下,迭代就不能⽤这种线性的迭代了,否则迭代结果会⼀会收敛⼀会发散,常规的⽅法是⽤⽬标轨迹函数的导函数计算迭代, 这部分实在很难做到通⽤,需要根据具体情况调整。
给出⼀个概念⽅程组:
把这些⽅程组中按照影响关系进⾏多次分步迭代,最终能得到合适的解或者判断⽆解。这组⽅程可以应对各种复杂情况的组合,实际上军事上⽕控系统正是这样计算的。不过在游戏中,通常不需要这么复杂的计算,只要简单的模拟就⾜够了,所以只是从概念层⾯讨论⼀下,如果这些复杂因素都考虑进去,完全可以作成⼀个可视的军事仿真的程序来了。
惯例...只写原理不付DEMO ⼤致是没多少⼈看的,附上⾃⼰写的demo
Reset Target:设置⽬标飞机以随机⽅位⾓和速度飞⾏
Gravity ON/OFF :设置是否开启重⼒
Aiming Mode: Manual/Auto 设置瞄准模式⼈⼯/⾃动 38394041424344454647484950515253
5455
565758
59
60
61
6263
64
65//构造⼀个从世界坐标到炮塔坐标的旋转矩阵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 == ){//如果⽆解 返回 ;}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 ;}if(diff1<accuracy){//如果距离⼩于希望的精度 到结果 返回瞄准点 炮弹是抛物线 发射时不能瞄准命中点 要计算瞄准点 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,accurac y,diff1);
}
⼈⼯模式下:wsad键上下左右旋转炮管⽅位⾓空格键击发
⾃动模式下:炮管⾃动瞄准提前量瞄准点,空格击发百发百中
AutoCam ON/OFF:设置飞机⼩镜头的显⽰模式,关闭⾃动镜头可以⽤按钮调整镜头的⾓度 + -按钮缩放镜头
Change Focus:改变主镜头焦点,在⾼射炮和⽬标飞机之间切换,以飞机为焦点时的镜头:
如果⽆法命中,会发出提⽰⾳效
不同视⾓下的效果:百发百中的弹道只给了炮弹⼀个初速度和⽅向然后靠碰撞检测显⽰爆炸效果,过程完全靠物理引擎控制。⼈⼯发射模式:
设置参数:
速度的单位都是m/s,长度单位都是m
Range是飞机初始位置的范围
Gravity Modifier:重⼒缩放因⼦,因为场景和模型是1:10⽐例,所以重⼒设置为1/10,否则就像玩具,
模拟的效果不会真实
其他参数都会⽤这个因⼦缩放,所以按照实际情况设置就⾏。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论