神奇数学之鱼的游动
有趣的事
不知道你有没有玩过捕鱼达人?在2009年相当火的一款手机游戏,即使到现在,还是有不少人热衷于发射各种各样的炮弹去抓捕那些漂亮的鱼儿,我以前也经常玩这个游戏作为消遣,而且在游戏中我还成为了百万富翁,唉,说多了都是泪,我喜欢这个游戏,更喜欢那些婀娜多姿的鱼儿
这些鱼儿沿着各种各样的路线游动,伴随着妖艳的姿势,我试着想如何才能实现这样的运动,或者可以更漂亮一些吗?
神奇的数学
作为数学专业的我来说,不管这些鱼的路径是什么样,在我看来都能转换为一个数学方程式,数学就是这么的神奇,让我从小迷恋,直到永远,让我们一点一点的还原这些数学方程式吧
直线
最简单的运动方式莫过于直线了吧,你很快就能想到y=kx b,斜切式方程(恭喜你猜对了, 0分,这么简单,你好意思加分么),当k==0时,就是水平方向运动,当k==∞时,就是垂直方向运动(不应该是x=b吗?是啊,x=y/k-b/k,当k==∞时,y就是孙子你知道吗?所以x=-b/k,又由于[0,1)之间的数与[0,∞)之间的数是一样多的,所以对于b来说k=1,所以x=-b,再根据对称性得到x=b,一切就是这么简单,不管你信不信,反正我信了),这样鱼就能够沿着某个方向运动了。
//_step就是鱼游动的方向
Point StraightLineMove::next(float delta) {
_calcPos = _curPos;
_calcPos.x = _step.x*delta;
_calcPos.y = _step.y*delta;
return BaseMove::next(delta);
}
圆滑曲线
看到这些沿着直线游来游去的鱼儿真没劲,曲线!圆滑曲线才是我的最爱。记得在大学里的时候在数值分析中学了各种各样的插值算法,引用某大神的一句话就是:给我几个点,我就能给你一条漂亮的圆滑曲线,对于多项式插值来说,结果都是唯一的,只是精度的问题,这里我们就随便挑一个实现,拉格朗日插值。
先欣赏一下漂亮的算法公式
然后来一段代码压压惊
Point LagrangeCurveMove::next(float delta) {
_calcPos.x = _curPos.x;
_calcPos.x = step*delta;
if ((_points[0].x-_points[_points.size()-1].x)*(_calcPos.x-_points[_points.size()-1].x)<=0) {
_prePos = _curPos;
_curPos = _points[_points.size()-1];
return _curPos;
}
_calcPos.y = 0;
float i_total_numerator = 1;
float k = _points.size();
for (int j=0; j<k; j) {
i_total_numerator *= (_calcPos.x-_points[j].x);
}
float i_total_denominator = 1;
for (int j=0; j<_points.size(); j) {
i_total_denominator = 1;
for (int i=0; i<_points.size(); i) {
if (i == j) continue;
i_total_denominator *= (_points[j].x-_points[i].x);
}
_calcPos.y = _points[j].y*i_total_numerator/(_calcPos.x-_points[j].x)/i_total_denominator;
}
return BaseMove::next(delta);
}
极坐标系方程
全套服务虽然有了,但我们的体验全部都仅仅是笛卡尔坐标系下的x,y而已,这些完全没法让我们达到高潮,让我们深入一点,极坐标系,r与θ的各种姿势,慢慢的我们就会来到高潮,下面让我们一个一个的体验这些套餐
别急,你不想撑过三十秒么,让我们先做做准备
代码是最好的镇静剂
bool PolarMove::setTotalTime(float time) {
if (!BaseMove::setTotalTime(time)) return false;
_step = 2*M_PI/_totalTime;
return true;
}
Point PolarMove::getPosByTheta(float theta) {
float r = getRadius(theta);
return Point(_origin.x r*cosf(theta), _origin.y r*sinf(theta));
}
Point PolarMove::next(float delta, bool fix) {
_theta = _step*delta;
_calcPos = getPosByTheta(_theta);
return BaseMove::next(delta, fix);
}
bool PolarMove::isEnd() {
return _theta >= 2*M_PI;
}
void PolarMove::setOrigin(cocos2d::Vec2 origin) {
_origin = origin;
_curPos = getPosByTheta(_theta);
_prePos = _curPos;
}
套餐一:阿基米德螺线
r = 12*θ (0=<θ<10π)
const float spiral_size = 12;
bool SpiralMove::isEnd() {
return _theta >= 10*M_PI;
}
float SpiralMove::getRadius(float theta) {
return spiral_size*_theta;
}
套餐二:笛卡尔的爱情方程式
r=200*(1-sinθ) (0=<θ<2π)
const float heart_size = 200;
float HeartMove::getRadius(float theta) {
return heart_size*(1-sinf(_theta));
}
套餐三:玫瑰玫瑰我爱你
r = 350*sin2θ (0=<θ<2π)
const float rose_size = 350;
float RoseMove::getRadius(float theta) {
return rose_size*sinf(theta*2);
}
套餐四:伯努利双纽线(扭扭更健康)
r^2 = 2500*cos(2θ) (0=<θ<2π)
const float lemniscate_size = 500;
float LemniscateMove::getRadius(float theta) {
return lemniscate_size*sqrtf(cosf(theta*2));
}
Point LemniscateMove::next(float delta, bool fix) {
_theta = _step*delta;
if (_theta >= 2*M_PI) {
_prePos = _curPos;
_curPos = _origin;
return _curPos;
}
if (_theta>=M_PI/4 && _theta<=M_PI*3/4) {
_calcPos = _origin;
} else {
_calcPos = getPosByTheta(_theta);
}
_curPos = BaseMove::next(delta, fix);
if (!fix) {
if (_theta>=M_PI/4 && _theta<=M_PI*3/4) {
if (_step > 0) {
_theta = M_PI*5/4;
} else {
_theta = M_PI*7/4;
}
_step *= -1;
}
}
return _curPos;
}
傅里叶变换
在这里你的一切需求都可以得到满足,傅里叶变换,神一样的存在,可以将任何图形用方程式叠加出来,很显然,这里已经到了收费环节,而且前面我也说了,不能提供收费服务,请转到掐死他,以及未满十八岁禁止入内。
(8π<t<10π)
鱼儿的方向
好了,我们已经很明确知道鱼儿的位置了,接下来我们得干另一件大事了,还记得学校军训吗?(这辈子都记得,特别是那教官凶神恶煞的表情)你走一个非常整齐的方阵,哇,漂亮极了,但是突然教官来一句:向左转,然后就尴尬了,有人左右不分了呀,有没有!一个漂亮的方阵就这样被毁了,所以方向是很重要的!
方向,那是文科生的叫法,一个字,Low,理科生都叫导数,有了方程式,求个导数就行了嘛,来,我们先科普一下求导,慢慢看,下课(怎么就下课了呀,有些同学已经迷失在了求导的路上,要报警了,某些方程式没有导数或者在某个点不存在导数)这个方式即不通用也不简单,一点都不能满足大众的需求,那让我们返璞归真,导数的本质是通过极限的概念对函数进行局部的线性逼近,简单点说在A1(x1, y1)点的导数,就是找到下一个点A2(x2, y2)与A1无限靠近,这个时候(y2-y1)/(x2-x1)就是A1点处的导数(当然了,非连续的点另行处理)再让我们看看前面得到的成果,我们已经知道了鱼儿有任何时刻的位置了,而每帧(1/60s)鱼儿的位置变化其实是很小的,至少对于我们肉眼来说,连续两帧的位置已经足够我们确定鱼的方向了。另一方面,取下一个点和取上一个点其实结果一样,我们只要算出当前的点,而上一个时刻的点已经为我们准备好了,所以方向是很容易确定的。
float BaseMove::getAngle() {
Point dir = Vec2(_curPos.x-_prePos.x, _curPos.y-_prePos.y);
// Translate to rotation of the fish
if (fabs(dir.x) < 0.0001) {
return dir.y>0 ? 90 : 270;
} else if (fabs(dir.y) < 0.0001) {
return dir.x>0 ? 0 : 180;
} else {
return (float)(atan2f(dir.y, dir.x)*180.0f/M_PI);
}
}
匀速运动
解放军演习的时候,那方阵走得多完美,整齐,方向正确,还有一个重要的点就是,每个士兵之间的间隔都是一模一样的(当然浮点数计算误差不考虑)同样,我们这里也需要处理这件大事,一方面是为了漂亮,另一方面其实这个会影响我们前面算的方向,我们需要鱼儿沿着我们计算好的路线尽量的匀速前进。要解决这个问题,又可以引出一堆理论知识,我们直接抛弃,不然又要下课了,直捣黄龙,什么叫匀速,虽然我高考语文只考了69分,但我知道,匀速就是如果你跑快了,你就缓一缓,你要是跑慢了,你就加把劲。
在这里,我们给定一个匀速的范围(不要死心眼,匀速就一定要追求一模一样,电脑是奴隶,也是爱人,适当的关爱还是需要的)我们先计算1帧的位置,如果与前一帧的距离正好在设定的范围内,恭喜你,如果太大或太小怎么办呢?聪明的你可能很容易就联想到了二分法,如果太小,那我们就加倍跑,如果还小继续加倍,直到超过或在设定的范围内,这个时候我们就确定我们想要的时刻在2^N到2^(N 1)之间了,再在这个区域内进行2分法找到我们想要的时刻就OK了。
const float move_dis_min = 2;
const float move_dis_max = 3;
Point BaseMove::next(float delta, bool fix) {
float dis = (_calcPos.x-_curPos.x)*(_calcPos.x-_curPos.x) (_calcPos.y-_curPos.y)*(_calcPos.y-_curPos.y);
if (fabsf(delta) > 0.0001) {
if (dis < move_dis_min) {
return next((fix?0.5:1)*fabsf(delta), fix);
} else if (dis > move_dis_max) {
return next(-0.5f * fabsf(delta), true);
}
}
_prePos = _curPos;
_curPos = _calcPos;
return _curPos;
}
鱼群
最后到了该说再见的地时候了,顺便分享一下关于鱼群简单的思路,一种最笨的方式就是创造N条路线,让这些鱼儿游动起来像个鱼群;另一种就是以某条鱼作为一个首领,首领给定路线,其它鱼在该鱼的后面一个区域内产生,然后某条鱼的游动路径就是该鱼当前点的位置,首领鱼当前点的位置,N帧(随机取一个范围)后首领鱼的位置做一个贝赛尔曲线,这就是该鱼在N-5帧(比首领鱼延迟一些),然后再算下一个M帧的路径,这样,这些鱼都会跟随首领鱼游动看起来也会很像一个鱼群哟;如果你不满足这些,那就去google鱼群算法吧。
如果需要更多服务,请拨打freeblank.xie@gmail.com