写在前面:游戏中很多地方都会涉及到数学运算,而在数学运算中,也是有运算效率的区分的,加(+)减法(-)是最快的,其次是乘法(*)和取余(%),接着才是除法(/),开根号(√)的运算会比前面几者都要慢。所以本文章主要处理开根号和除法,尽量在游戏开发中避免。
一、Vector3.Distance优化
经常在游戏开发中会用到计算两个点的距离,当距离小于或者大于某个值时做什么做什么事,多用于小怪的AI计算,但是,这种计算Distance的数学公式是:
距离 = 两点每个分量的差值相加,并开根号。
float Vector3.Distance(Vector3 a,Vector b)
{
float sqr_x = (a.x - b.x) * (a.x - b.x);
float sqr_y = (a.y - b.y) * (a.x - b.x);
float sqr_z = (a.z - b.z) * (a.z - b.z);
return Mathf.Sqrt(sqr_x + sqr_y + sqr_z);
}
会发现,该运算会使用开根号的方法去计算两个点之间的距离。需要做的优化方法也很简单,我们不使用开根号后的值,我们只使用开根号前的值。
扩张下面这个方法:
float SqrDistance(Vector3 a,Vector b)
{
float sqr_x = (a.x - b.x) * (a.x - b.x);
float sqr_y = (a.y - b.y) * (a.x - b.x);
float sqr_z = (a.z - b.z) * (a.z - b.z);
return sqr_x + sqr_y + sqr_z;
}
那么在判断只需要这么修改:
if(SqrDistance(point1,point2) < minDistance * minDistance)
{
//DoSomething...
}
二、除法的优化
除法的优化,是针对于除数是已知的条件下,那么可以相对应的优化。先提前计算好1/n的值后,然后用乘法去乘上计算出来的1/n的值即可。
举个栗子,已知一个变量 float a = 2.333f;我想计算 a/4的值:
用计算器可以算出1/4=0.25; 所以要计算a/4就变成了
float a = 2.333f;
float value = a * 0.25f;//代替a/4
三、向量夹角优化
我们经常用扇形来做是否攻击到或者做小怪AI侦查,用扇形来进行判断。那么就一定要进行夹角的计算。
扇形有个角度,所以会先判断角度,再判断距离。
因为扇形有个朝向,所以会用到transform.forward。
如下图所示:
所以默认代码将会如下:
//forward是扇形朝向,一般是就是人物朝向(forward本身就是单位向量)
//pos是人物位置
//target是要计算的目标位置
//angle是攻击的角度范围,这是总的角度范围,需要除以2来求出一边的角度。
//distance是扇形的距离
bool CheckFanShape(Vector3 forward,Vector3 pos,Vector3 target,float angle,float distance)
{
Vector3 target_dir = (target - pos).normalize;//算出当前点目标点的向量
float _angle = Vector3.Angle(target_dir, forward);//计算出人物朝向跟目标朝向的角度。用unity的api
Vector3 _disntace = Vector3.Distance(pos,target);//计算两点之间的距离
return _angle <= angle / 2 && _disntace <= diantace;
}
会发现,这里用到了Angle来计算夹角,用Distance来计算距离。
disntace的优化上面提到了。现在要做的就是Angle函数的优化。
其实Unity.Angle这个函数还有一些坑:Angle角度 public static float Angle(Vector3 from, Vector3 to); 返回的角度总是两个向量之间的较小的角(实测返回不大于 180 度, 并不是 unity 文档中说的锐角)
所以我们需要自己去实现以下夹角。当然你可以如下计算夹角:
float angle = Vector3.Angle (fromVector, toVector); //求出两向量之间的夹角
Vector3 normal = Vector3.Cross (fromVector,toVector);//叉乘求出法线向量
angle *= Mathf.Sign (Vector3.Dot(normal,upVector)); //求法线向量与物体上方向向量点乘,结果为1或-1,修正旋转方向
这样是可以,但这样会用到三角函数,也不是一个优解,所以我们还是得从数学公式出发:
向量baia=(x1,y1),向量b=(x2,y2)
a·b=x1x2+y1y2=|a||b|cosθ(θ是a,b夹角)
cosθ = a·b/(|a||b|)
解释下来就是,cos的夹角 = 向量a * 向量b /(向量a.Distance * 向量b.Distance)
向量a * 向量b = x1x2+y1y2
这次要稍微注意一点,当这个向量a和向量b是单位向量,也就是这个disntace=1时,cosθ就等价于 向量a * 向量b,就等价于x1x2+y1y2
又已知cosθ的函数:
这里我们只需要用到的区间是[-π,π] 也就是-180度到180度。我们计算夹角用-180到180足够了。
可以发现,距离0度越近,cosθ越大,也就是说|θ|越小,cosθ越大,所以cosθ和|θ|成反比。
所以这个夹角我们直接用cosθ去代替θ,我们的角度判断可以一步一步拆解,如下:
设我们要求的夹角为θ1,目标夹角要求为θ2
我们需要计算 |θ1| < |θ2| 为true的情况。
∵ |θ1| < |θ2|
∴ cosθ1 > cosθ2 (上面解释的成反比)
∴ 当我们夹角是单位向量时,x1x2+y1y2 > cosθ2
前半部分很好计算,主要是后面的cosθ2,这个三角函数怎么简化呢?
(所有的三角函数,sinθ cosθ ,因为都是固定的一个数,所以我们可以记好,然后通过读表的方式读取sin和cos的值。一般请教下,θ值都是int就足够了,如果θ 值需要有小数点,那就扩大θ 的精度,表格中保存更多的θ 值。)
我们就通过读取表格的方式算出了cosθ2的值。
写成代码就是:
Dictionary<int,float> cosTable = new Dictionary<int,float>();
float Utility.Cosf(int angle)
{
return cosTable[angle];
}
Vector3 Utility.Normalize(Vector3 dir)
{
return dir.normalize;//这里留个悬念,后面会讲到这个normalize的优化。
}
bool CheckAngle(Vector3 forward,Vector3 pos,Vector3 target,int angle)
{
//因为这个算的是2D的,所以我们取的是x和z的值
Vector3 dir = Utility.Normalize(target - pos);
float cos_value = forward.x * forward.z + dir.x * dir.z;
float target_cos_value = Utility.Cosf(angle);//注意,这里的angle是除以2以后的。
return cos_value > target_cos_value;
}
这样我们就完成了角度的检测优化,用来判断角度是否小于一定的角度,而且策划想配置几度就几度,因为我们的cos计算是直接读表取出的。
上面之前说到,必须要是单位向量,才能把cosθ后面的/(|a||b|)给约掉,因为长度都为1,不管任何数,除以1都是本身。
那么,向量长度变为1,我们俗称的归一化,有什么好的好的优化呢?下面就讲到了。
四、Normalize的优化计算
正常情况下,我们需要将向量归一化,那么就必须计算出向量的长度,然后当前向量去除以本身长度,就求出了归一化后的向量了。
向量.normalize = 向量 / 向量.Distance
代码如下:
Vector3 Normalize(Vector3 dir)
{
float sqr_len = dir.x *dir.x + dir.y * dir.y + dir.z * dir.z;
return dir / Mathf.Sqrt(sqr_len);
}
可以发现,我们不仅要用上除法,还要计算平方根,而且,这时我们的第一个优化distance的办法就不适用了,因为我们必须计算平方根进行除法。那怎么办呢?
所以我们需要对除法加上开根号同时优化,也就是说,如果我们可以把 1/ Mathf.Sqrt(n) 的值求出,那么Normalize就等价于:
dir * value
那么这个value公式怎么求呢?
//这个函数就是用来计算1/sqr(x)的
float InvSqrt (float x)
{
float xhalf = 0.5f*x;
int i = *(int*)&x;
i = 0x5f3759df - (i>>1);
x = *(float*)&i;
x = x*(1.5f - xhalf*x*x);
return x;
}
这个函数就能很方便计算出1/ Mathf.Sqrt(n)的值。这个函数是需要操作指针。所以在C#这个有语言里算是不安全的代码。得加上unsafe标签,并且要设置允许unsafe代码。
五、其他一些在项目中可以会经常用到的Vector3公式
①计算多个点的中心点位置。
原理很简答,就是把所有点相加,然后除以一个这些点的总个数,就能算出中心点了。
Vector3 GetCenter(Vector3[] points)
{
Vector3 center = Vector3.zero;
for (int i = 0; i < points.Length; i++)
{
center += points[i];
}
return center / points.Length;
}
②计算一个点位于一条向量是左边还是右边。(这里是基于2D平面的,所以用Vector2代替,如果是3D的,就没法定义左边右边的含义了。)
原理是:向量a.x * 向量b.y - 向量a.y * 向量b.x
enum MyDirectionType//所在方向的偏向哪一边
{
Left,
InLine,
Right
}
//start 是向量的开始位置
//end 是向量的结束位置
//calPoint 是要计算的目标点
MyDirectionType GetDirection(Vector3 start,Vector3 end,Vector3 calPoint)
{
//根据3个点的参数,计算出两条向量
Vector3 dir1 = end - start;
Vector3 dir2 = calPoint - start;
float value = dir1.x * dir2.y - dir1.y * dir2.x;
if (value < 0)
{
return MyDirectionType.Right;
}
else if (value == 0)
{
return MyDirectionType.InLine;
}
else
{
return MyDirectionType.Left;
}
}
这个方法多用在一些顶点计算,计算某个点是否在一个三角形内部,如果三角形三个点构成的向量跟目标点的方向都是同一侧,都是居左或者都是居右,那就说明该点在三角形内部。