神奇数学之鱼的游动

发表于2017-10-02
评论4 5.8k浏览

有趣的事

不知道你有没有玩过捕鱼达人?在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 &gt;= 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 &gt;= 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 &gt;= 2*M_PI) {
        _prePos = _curPos;
        _curPos = _origin;

        return _curPos;
    }


    if (_theta&gt;=M_PI/4 &amp;&amp; _theta&lt;=M_PI*3/4) {
        _calcPos = _origin;
    } else {
        _calcPos = getPosByTheta(_theta);
    }

    _curPos = BaseMove::next(delta, fix);

    if (!fix) {
        if (_theta&gt;=M_PI/4 &amp;&amp; _theta&lt;=M_PI*3/4) {
            if (_step &gt; 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) &lt; 0.0001) {
        return dir.y&gt;0 ? 90 : 270;
    } else if (fabs(dir.y) &lt; 0.0001) {
        return dir.x&gt;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) &gt; 0.0001) {
        if (dis &lt; move_dis_min) {
            return next((fix?0.5:1)*fabsf(delta), fix);
        } else if (dis &gt; 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

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: