关于随机数生成算法,这是所有其它随机生成算法必须用到的基础。
在游戏中自定义一个随机生成函数是很有必要的,比如要做一个联网的扑克游戏,开始发牌的时候并不用把所有的牌都一张张发给玩家,只需要发送一个用于生成随机数的种子,玩家就可以知道完整的发牌顺序。这里有个前提,在种子相同时每个玩家生成的随机数序列必须是一样的,如果使用系统api自带的随机函数很难保证这一点,自定义随机函数就没问题了。
再比如游戏中的粒子系统,粒子运动看似杂乱无章,但是只要给的随机种子一致并且更新步长也一致,两个粒子系统中的粒子是可以做到完全同步的,这就为联网游戏中同步粒子动画提供了一个思路。
再如随机生成场景类游戏,联网的各玩家只需要一个随机种子就可以同步整个场景。
关于具体随机数生成算法,网上介绍很多,这里摘抄两版:
一种,快速但不是很精确:
static unsigned int rand_seed = 0;
void SrandFast(unsigned int seed)
{
rand_seed = seed;
}
unsigned int RandFast()
{
//return rand();
//1 线性同余方法(LCG) Xn+1 ≡ (a* Xn + c )% m 弊端是相邻点之间的相关性 将相邻3个数作为坐标绘制点,会聚集到n个明显的平面
rand_seed = (rand_seed * 16807) % ((1 << 31) - 1);
return rand_seed%RandMax;
}
另一种,精确但不是很快速:
#define LOWER_MASK 0x7fffffff
#define M 397
#define MATRIX_A 0x9908b0df
#define N 624
#define TEMPERING_MASK_B 0x9d2c5680
#define TEMPERING_MASK_C 0xefc60000
#define TEMPERING_SHIFT_L(y) (y >> 18)
#define TEMPERING_SHIFT_S(y) (y << 7)
#define TEMPERING_SHIFT_T(y) (y << 15)
#define TEMPERING_SHIFT_U(y) (y >> 11)
#define UPPER_MASK 0x80000000
static int k = 1;
static unsigned long mag01[2] = {0x0, MATRIX_A};
static unsigned long ptgfsr[N]={0};
static bool init = true;
void Srand(unsigned int seed)
{
if (seed == 0)
{
seed = 1;
}
ptgfsr[0] = seed;
for(k = 1; k < N; k++)
{
ptgfsr[k] = 69069 * ptgfsr[k - 1];
}
k = 1;
init = false;
}
unsigned int Rand()
{
if (init)
{
Srand(1);
init = false;
}
//马特赛特旋转法
int kk;
unsigned long y;
if(k == N)
{
for(kk = 0; kk < N - M; kk++)
{
y = (ptgfsr[kk] & UPPER_MASK) | (ptgfsr[kk + 1] & LOWER_MASK);
ptgfsr[kk] = ptgfsr[kk + M] ^ (y >> 1) ^ mag01[y & 0x1];
}
for(; kk < N - 1; kk++)
{
y = (ptgfsr[kk] & UPPER_MASK) | (ptgfsr[kk + 1] & LOWER_MASK);
ptgfsr[kk] = ptgfsr[kk + (M - N)] ^ (y >> 1) ^ mag01[y & 0x1];
}
y = (ptgfsr[N - 1] & UPPER_MASK) | (ptgfsr[0] & LOWER_MASK);
ptgfsr[N - 1] = ptgfsr[M - 1] ^ (y >> 1) ^ mag01[y & 0x1];
k = 0;
}
y = ptgfsr[k++];
y ^= TEMPERING_SHIFT_U(y);
y ^= TEMPERING_SHIFT_S(y) & TEMPERING_MASK_B;
y ^= TEMPERING_SHIFT_T(y) & TEMPERING_MASK_C;
y ^= TEMPERING_SHIFT_L(y);
return y%RAND_MAX;
}
其它衍生随机数生成:其中列举了一些易错点
随机生成方向,对于2D的很简单,随机一个角度然后sin cos即可。
对于3D的就不简单了, 首先想到的方法一,在立方体内随机取点然后归一化,结果是不准确的,仔细观察立方体八个角上概率会偏大,当然如果不介意些许误差也是可以用的。
void RandDir(vec3& dir)
{
//不准确 加上判断球内就准确了
dir = vec3(RandRange(-1.0f,1.0f),RandRange(-1.0f,1.0f),RandRange(-1.0f,1.0f));
dir.Normalize();
}
又想到了方法二:先将向量a[0,0,1]绕y轴随机旋转0~360度,然后再绕x轴旋转0~360度。这个方法貌似正确,其实错的更离谱,仔细观察会发现靠近x轴正负1位置概率偏大。
//float theta1 = RandRange(0.0f,TWOPI);
//float theta2 = RandRange(0.0f,TWOPI);
//float len = RandRange(-1.0,1.0);
//float radLevel = cosf(theta1)*len; //水平半径
//float ex = radLevel * cosf(theta2);
//float ey = sinf(theta1)*len;
//float ez = radLevel * sinf(theta2);
主要出错在第二步,因为正确做法需要的是绕新frame下的x轴旋转,算法变得复杂。关于下面公式是由自己写的小工具自动推导出来的,工具源码有空再贴出来。
void RandDir(vec3& dir)
{
float A = RandRange(0.0f,TWOPI);
float B = RandRange(0.0f,TWOPI);
float cosA = cos(A);
float cosB = cos(B);
float sinA = sin(A);
float sinB = sin(B);
dir.x =(((1-cosB)*cosA*cosA+cosB)*sinA-(1-cosB)*sinA*cosA*cosA);
dir.y = -(sinA*sinB*sinA+cosA*sinB*cosA) ;
dir.z =(((1-cosB)*sinA*sinA+cosB)*cosA-(1-cosB)*sinA*cosA*sinA);
}
随机生成球内一个点:第一步先用上面的方法随机取一个方向,第二步在半径内随机取一个长度开三次方后付给方向,开三次方是必须的,否则靠近球心处概率偏大,当然如果正好需要这种边缘弱化的分布也可以不开方。(对于2D一般是开二次方)
vec3 RandomPosInSphere(float rad)
{
vec3 pos = RandDir();
pos *= pow(randRange(0.0f,rad),1.0f/3);
return pos;
}
随机生成在三角面上一个点的位置:
void RandomPosOnTrigon(const vec3& pos0,const vec3& pos1,const vec3& pos2,vec3& res)
{
//错误的算法一:直接插值不是均匀分布,靠近重心密度更大
//vec3 rat;
//rat[0] = Rand()%1000;
//rat[1] = Rand()%1000;
//rat[2] = Rand()%1000;
//float sum = (rat[0]+rat[1]+rat[2]);
//if (sum==0)
//{
// res = pos0;
// return;
//}
//sum = 1.0f/sum;
//rat *= sum;
//pos0 = pos0*rat[0] + pos1*rat[1] + pos2*rat[2]; //错误的算法二:先插值一条边,再用得到的点和对顶点插值,不是均匀分布,靠近对顶点密度更大
//正确的算法一:先插值一条边,再用得到的点和对顶点插值,插值时比率要开方, 因为微分法面积随距离平方而变大
//此方法可以方便的扩展到3维, 取三棱锥内的随机点:先取底面随机点,再和最上面的顶点根据距离开三次方插值
float rat1 = (Rand()%1000)/999.0f;//0~1
//插值pos0, pos1
res = pos0*rat1 + pos1*(1-rat1);
float rat2 = sqrt((Rand()%1000)/999.0f);//sqrt (0~1)
//插值posMid, pos2
res = res*rat2 + pos2*(1-rat2); 正确的算法二:取平行四边形内的随机点,如果不在三角形内则旋转对称
//float rat1 = (Rand()%1000)/999.0f;//0~1
//float rat2 = (Rand()%1000)/999.0f;
//if (rat1+rat2>1)
//{
// //不在三角形内 旋转对称
// rat1 = 1-rat1;
// rat2 = 1-rat2;
//}
//res = pos0 + (pos1-pos0)*rat1 + (pos2-pos0)*rat2;
}
有时可能需要的不是平均分布随机函数而是高斯分布:
//正太分布随机函数:平均值0, 标准偏差1。 68.26%的概率在[-1,1]内
float RandGauss( )
{
static float U, V;
static int phase = 0;
float Z; if(phase == 0)
{
U = Rand() / (RandMax + 1.0);
V = Rand() / (RandMax + 1.0);
Z = sqrt(-2.0 * log(U))* sin(2.0 * _PI * V);
}
else
{
Z = sqrt(-2.0 * log(U)) * cos(2.0 * _PI * V);
}
phase = 1 - phase;
return Z;
} //正太分布随机函数:平均值m, 标准偏差sd
float RandGauss(float m,float sd)
{
float X = m + sd* RandGauss();
return X;
}完