游戏开发中的Vector3运算法则的优化

写在前面:游戏中很多地方都会涉及到数学运算,而在数学运算中,也是有运算效率的区分的,加(+)减法(-)是最快的,其次是乘法(*)和取余(%),接着才是除法(/),开根号(√)的运算会比前面几者都要慢。所以本文章主要处理开根号和除法,尽量在游戏开发中避免。

一、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;
    }
}

这个方法多用在一些顶点计算,计算某个点是否在一个三角形内部,如果三角形三个点构成的向量跟目标点的方向都是同一侧,都是居左或者都是居右,那就说明该点在三角形内部。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页