• 游戏中的路径动画设计与实现


    路径动画让对象沿着指定路径运动,在游戏中用着广泛的应用,比如塔防类游戏就经常使用路径动画。前几天在cantk里实现了路径动画(源码在github上),路径动画实现起来并不难,实际上写起来挺有意思的,这里和大家分享一下。

    先说下路径动画的基本需求:

    • 1.支持基本的路径类型:直线,弧线,抛物线,二次贝塞尔曲线,三次贝塞尔曲线,正弦(余弦)和其它曲线。
    • 2.对象沿路径运动的速度是可以控制的。
    • 3.对象沿路径运动的加速度是可以控制的。
    • 4.对象沿路径运动的角度(切线方向或不旋转)是可以控制的。
    • 5.可以通过几条基本的路径组合成一条复合的路径。
    • 6.多个对象可以沿同一条路径运动。
    • 7.同一个对象也可以多次沿同一条路径运动。
    • 8.对象到达终点时能触发一个事件通知游戏。

    看起来是不是很复杂呢? 呵呵,其实一点也不难,不过也有点挑战:

    • 1.计算任意时刻对象所在的位置。不是通过x计算y的值,而是通过时间t计算x和y的值。所以需要使用参数方程,时间就是参数,x和y各对应一个方程。

    • 2.计算任意时刻对象的方向。这个确实有点考验我(数学不怎么好:(),开始是打算通过对曲线的方程求导数得到切线方程,但是发现计算量很大,而且atan只能得到0到180度的角度,要得到0到360的角度还要进一步计算。后来一想,导数不是dy/dx的极限吗,只有dx极小就可以得到近似的结果了。所以决定取当前时刻的点和下一个邻近时刻的点来计算角度。

    • 3.控制对象的速度很容易,我们可以指定通过此路径的总时间来控制对象的速度。

    • 4.控制对象的加速度需要点技巧。对于用过缓动作(Tween)动画的朋友来说是很简单的,可以使用不同的Ease来实现。cantk沿用了android里的术语,叫插值算法(Interpolator),常见的有加速,减速,匀速和回弹(Bounce)。cantk里有缺省的实现,你也可以自己实现不同的插值算法。

    • 5.复合路径当然很简单了,用Composite模式就行了,不过这里我并没有严格使用Composite模式。

    • 6.路径的实现并不关联沿着它运动的对象,由更上一次的模块去管理对象吧,好让路径算法本身是独立的。

    现在我们来实现各种路径吧:

    注:duration是通过此路径的时间,interpolator是插值算法。

    • 0.定义一个基类BasePath,实现一些缺省的行为。
    function BasePath() {
        return;
    }
    
    BasePath.prototype.getPosition = function(t) {
        return {x:0, y:0};
    }
    
    BasePath.prototype.getDirection = function(t) {
        var p1 = this.getPosition(t);
        var p2 = this.getPosition(t+0.1);
    
        return BasePath.angleOf(p1, p2);
    }
    
    BasePath.prototype.getStartPoint = function() {
        return this.startPoint ? this.startPoint : this.getPosition(0);
    }
    
    BasePath.prototype.getEndPoint = function() {
        return this.endPoint ? this.endPoint : this.getPosition(this.duration);
    }
    
    BasePath.prototype.getSamples = function() {
        return this.samples;
    }
    
    BasePath.prototype.draw = function(ctx) {
        var n = this.getSamples();
        var p = this.getStartPoint();   
    
        ctx.moveTo(p.x, p.y);
        for(var i = 0; i <= n; i++) {
            var t = this.duration*i/n;
            var p = this.getPosition(t);
            ctx.lineTo(p.x, p.y);
        }
    
        return this;
    }
    
    BasePath.angleOf = function(from, to) {
        var dx = to.x - from.x;
        var dy = to.y - from.y;
        var d = Math.sqrt(dx * dx + dy * dy);
    
        if(dx == 0 && dy == 0) {
            return 0;
        }
    
        if(dx == 0) {
            if(dy < 0) {
                return 1.5 * Math.PI;
            }
            else {
                return 0.5 * Math.PI;
            }
        }
    
        if(dy == 0) {
            if(dx < 0) {
                return Math.PI;
            }
            else {
                return 0;
            }
        }
    
        var angle = Math.asin(Math.abs(dy)/d);
        if(dx > 0) {
            if(dy > 0) {
                return angle;
            }
            else {
                return 2 * Math.PI - angle;
            }
        }
        else {
            if(dy > 0) {
                return Math.PI - angle;
            }
            else {
                return Math.PI + angle;
            }
        }
    }
    • 1.直线。两点决定一条直线,从一个点运动到另外一个点。
    function LinePath(duration, interpolator, x1, y1, x2, y2) {
        this.dx = x2 - x1;
        this.dy = y2 - y1;
        this.x1 = x1;
        this.x2 = x2;
        this.y1 = y1;
        this.y2 = y2;
        this.duration = duration;
        this.interpolator = interpolator;
        this.angle = BasePath.angleOf({x:x1,y:y1}, {x:x2, y:y2});
        this.startPoint = {x:this.x1, y:this.y1};
        this.endPoint = {x:this.x2, y:this.y2};
    
        return;
    }
    
    LinePath.prototype = new BasePath();
    LinePath.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
    
        var x = this.x1 + this.dx * percent;
        var y = this.y1 + this.dy * percent;
    
        return {x:x, y:y};
    }
    
    LinePath.prototype.getDirection = function(t) {
        return this.angle;
    }
    
    LinePath.prototype.draw = function(ctx) {
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
    
        return this;
    }
    
    LinePath.create = function(duration, interpolator, x1, y1, x2, y2) {
        return new LinePath(duration, interpolator, x1, y1, x2, y2);
    }
    • 2.弧线,由圆心,半径,起始幅度和结束幅度决定一条弧线。
    function ArcPath(duration, interpolator, xo, yo, r, sAngle, eAngle) {
        this.xo = xo;
        this.yo = yo;
        this.r = r;
        this.sAngle = sAngle;
        this.eAngle = eAngle;
        this.duration = duration;
        this.interpolator = interpolator;
        this.angleRange = eAngle - sAngle;
    
        this.startPoint = this.getPosition(0);  
        this.endPoint = this.getPosition(duration); 
    
        return;
    }
    
    ArcPath.prototype = new BasePath();
    ArcPath.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
        var angle = this.sAngle + percent * this.angleRange;
    
        var x = this.xo + this.r * Math.cos(angle);
        var y = this.yo + this.r * Math.sin(angle);
    
        return {x:x, y:y};
    }
    
    ArcPath.prototype.getDirection = function(t) {
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
        var angle = this.sAngle + percent * this.angleRange + Math.PI * 0.5;
    
        return angle;
    }
    
    ArcPath.prototype.draw = function(ctx) {
        ctx.arc(this.xo, this.yo, this.r, this.sAngle, this.eAngle, this.sAngle > this.eAngle);
    
        return this;
    }
    
    ArcPath.create = function(duration, interpolator, xo, yo, r, sAngle, eAngle) {
        return new ArcPath(duration, interpolator, xo, yo, r, sAngle, eAngle);
    }
    • 3.抛物线。这里的抛物线不是数学上严格的抛物线,也不是物理上严格的抛物线,而是游戏中的抛物线。游戏中的抛物线允在X/Y方向指定不同的加速度(即重力),它由初始位置,X/Y方向的加速度和初速度决定。
    function ParaPath(duration, interpolator, x1, y1, ax, ay, vx, vy) {
        this.x1 = x1;
        this.y1 = y1;
        this.ax = ax;
        this.ay = ay;
        this.vx = vx;
        this.vy = vy;
        this.duration = duration;
        this.interpolator = interpolator;
    
        this.startPoint = this.getPosition(0);  
        this.endPoint = this.getPosition(duration); 
        var dx = Math.abs(this.endPoint.x-this.startPoint.x);
        var dy = Math.abs(this.endPoint.y-this.startPoint.y);
        this.samples = Math.max(dx, dy);
    
        return;
    }
    
    ParaPath.prototype = new BasePath();
    ParaPath.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
    
        t = (percent * this.duration)/1000;
        var x = 0.5 * this.ax * t * t + this.vx * t + this.x1;
        var y = 0.5 * this.ay * t * t + this.vy * t + this.y1;
    
        return {x:x, y:y};
    }
    
    ParaPath.create = function(duration, interpolator, x1, y1, ax, ay, vx, vy) {
        return new ParaPath(duration, interpolator, x1, y1, ax, ay, vx, vy);
    }
    • 4.正弦和余弦曲线其实一样,正弦偏移90度就是余弦。它由初始位置,波长,波速,振幅和角度偏移决定。
    function SinPath(duration, interpolator, x1, y1, waveLenth, v, amplitude, phaseOffset) {
        this.x1 = x1;
        this.y1 = y1;
        this.v = v;
        this.amplitude = amplitude;
        this.waveLenth = waveLenth;
        this.duration = duration;
        this.phaseOffset = phaseOffset ? phaseOffset : 0;
        this.interpolator = interpolator;
        this.range = 2 * Math.PI * (v * duration * 0.001)/waveLenth;
    
        this.startPoint = this.getPosition(0);  
        this.endPoint = this.getPosition(duration); 
        var dx = Math.abs(this.endPoint.x-this.startPoint.x);
        var dy = Math.abs(this.endPoint.y-this.startPoint.y);
        this.samples = Math.max(dx, dy);
    
        return;
    }
    
    SinPath.prototype = new BasePath();
    SinPath.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
        t = percent * this.duration;
    
        var x = (t * this.v)/1000 + this.x1;
        var y = this.amplitude * Math.sin(percent * this.range + this.phaseOffset) + this.y1;
    
        return {x:x, y:y};
    }
    
    SinPath.create = function(duration, interpolator, x1, y1, waveLenth, v, amplitude, phaseOffset) {
        return new SinPath(duration, interpolator, x1, y1, waveLenth, v, amplitude, phaseOffset);
    }
    • 5.三次贝塞尔曲线。它由4个点决定,公式请参考百度文库。
    
    function Bezier3Path(duration, interpolator, x1, y1, x2, y2, x3, y3, x4, y4) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x3 = x3;
        this.y3 = y3;
        this.x4 = x4;
        this.y4 = y4;
    
        this.duration = duration;
        this.interpolator = interpolator;
        this.startPoint = this.getPosition(0);  
        this.endPoint = this.getPosition(duration); 
    
        return;
    }
    
    Bezier3Path.prototype = new BasePath();
    Bezier3Path.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
    
        t = percent;
        var t2 = t * t;
        var t3 = t2 * t;
    
        var t1 = 1 - percent;
        var t12 = t1 * t1;
        var t13 = t12 * t1;
    
        //http://wenku.baidu.com/link?url=HeH8EMcwvOjp-G8Hc-JIY-RXAvjRMPl_l4ImunXSlje-027d01NP8SkNmXGlbPVBioZdc_aCJ19TU6t3wWXW5jqK95eiTu-rd7LHhTwvATa
        //P = P0*(1-t)^3 + 3*P1*(1-t)^2*t + 3*P2*(1-t)*t^2 + P3*t^3;
    
        var x = (this.x1*t13) + (3*t*this.x2*t12) + (3*this.x3*t1*t2) + this.x4*t3;
        var y = (this.y1*t13) + (3*t*this.y2*t12) + (3*this.y3*t1*t2) + this.y4*t3;
    
        return {x:x, y:y};
    }
    
    Bezier3Path.prototype.draw = function(ctx) {
        ctx.moveTo(this.x1, this.y1);
        ctx.bezierCurveTo(this.x2, this.y2, this.x3, this.y3, this.x4, this.y4);
    }
    
    Bezier3Path.create = function(duration, interpolator, x1, y1, x2, y2, x3, y3, x4, y4) {
        return new Bezier3Path(duration, interpolator, x1, y1, x2, y2, x3, y3, x4, y4);
    }
    
    • 6.二次贝塞尔曲线。它由3个点决定,公式请参考百度文库。
    
    function Bezier2Path(duration, interpolator, x1, y1, x2, y2, x3, y3) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
        this.x3 = x3;
        this.y3 = y3;
    
        this.duration = duration;
        this.interpolator = interpolator;
        this.startPoint = this.getPosition(0);  
        this.endPoint = this.getPosition(duration); 
    
        return;
    }
    
    Bezier2Path.prototype = new BasePath();
    Bezier2Path.prototype.getPosition = function(time) {
        var t = time;
        var timePercent = Math.min(t/this.duration, 1);
        var percent = this.interpolator ? this.interpolator.get(timePercent) : timePercent;
    
        t = percent;
        var t2 = t * t;
    
        var t1 = 1 - percent;
        var t12 = t1 * t1;
    
        //P = (1-t)^2 * P0 + 2 * t * (1-t) * P1 + t^2*P2;
        var x = (this.x1*t12) + 2 * this.x2 * t * t1 + this.x3 * t2;
        var y = (this.y1*t12) + 2 * this.y2 * t * t1 + this.y3 * t2;
    
        return {x:x, y:y};
    }
    
    Bezier2Path.prototype.draw = function(ctx) {
        ctx.moveTo(this.x1, this.y1);
        ctx.quadraticCurveTo(this.x2, this.y2, this.x3, this.y3);
    }
    
    Bezier2Path.create = function(duration, interpolator, x1, y1, x2, y2, x3, y3) {
        return new Bezier2Path(duration, interpolator, x1, y1, x2, y2, x3, y3);
    }
    

    现在我们把它们包装一下:

    function PathAnimation(x, y) {
        this.startPoint = {x:x, y:y};
        this.endPoint = {x:x, y:y};
        this.duration = 0;
        this.paths = [];
    
        return;
    }
    
    PathAnimation.prototype.getStartPoint = function() {
        return this.startPoint;
    }
    
    PathAnimation.prototype.getEndPoint = function() {
        return this.endPoint;
    }
    
    PathAnimation.prototype.addPath = function(path) {
        this.paths.push({path:path, startTime:this.duration});
        this.endPoint = path.getEndPoint();
        this.duration += path.duration;
    
        return this;
    }
    
    PathAnimation.prototype.addLine = function(duration, interpolator, p1, p2) {
        return this.addPath(LinePath.create(duration, interpolator, p1.x, p1.y, p2.x, p2.y));
    }
    
    PathAnimation.prototype.addArc = function(duration, interpolator, origin, r, sAngle, eAngle) {
        return this.addPath(ArcPath.create(duration, interpolator, origin.x, origin.y, r, sAngle, eAngle));
    }
    
    PathAnimation.prototype.addPara = function(duration, interpolator, p, a, v) {
        return this.addPath(ParaPath.create(duration, interpolator, p.x, p.y, a.x, a.y, v.x, v.y));
    }
    
    PathAnimation.prototype.addSin = function(duration, interpolator, p, waveLenth, v, amplitude, phaseOffset) {
        return this.addPath(SinPath.create(duration, interpolator, p.x, p.y, waveLenth, v, amplitude, phaseOffset));
    }
    
    PathAnimation.prototype.addBezier = function(duration, interpolator, p1, p2, p3, p4) {
        return this.addPath(Bezier3Path.create(duration, interpolator, p1.x,p1.y, p2.x,p2.y, p3.x,p3.y, p4.x,p4.y));
    }
    
    PathAnimation.prototype.addQuad = function(duration, interpolator, p1, p2, p3) {
        return this.addPath(Bezier2Path.create(duration, interpolator, p1.x,p1.y, p2.x,p2.y, p3.x,p3.y));
    }
    
    PathAnimation.prototype.getDuration = function() {
        return this.duration;
    }
    
    PathAnimation.prototype.getPathInfoByTime = function(elapsedTime) {
        var t = 0;  
        var paths = this.paths;
        var n = paths.length;
    
        for(var i = 0; i < n; i++) {
            var iter = paths[i];
            var path = iter.path;
            var startTime = iter.startTime;
            if(elapsedTime >= startTime && elapsedTime < (startTime + path.duration)) {
                return iter;
            }
        }
    
        return null;
    }
    
    PathAnimation.prototype.getPosition = function(elapsedTime) {
        var info = this.getPathInfoByTime(elapsedTime);
    
        return info ? info.path.getPosition(elapsedTime - info.startTime) : this.endPoint;
    }
    
    PathAnimation.prototype.getDirection = function(elapsedTime) {
        var info = this.getPathInfoByTime(elapsedTime);
    
        return info ? info.path.getDirection(elapsedTime - info.startTime) : 0;
    }
    
    PathAnimation.prototype.draw = function(ctx) {
        var paths = this.paths;
        var n = paths.length;
    
        for(var i = 0; i < n; i++) {
            var iter = paths[i];
            ctx.beginPath();
            iter.path.draw(ctx);
            ctx.stroke();
        }
    
        return this;
    }
    
    PathAnimation.prototype.forEach = function(visit) {
        var paths = this.paths;
        var n = paths.length;
    
        for(var i = 0; i < n; i++) {
            visit(paths[i]);
        }
    
        return this;
    }

    Cantk里做了进一步包装,使用起来非常简单:先放一个UIPath对象到场景中,然后在onInit事件里增加路径,在任何时间都可以向UIPath增加对象或删除对象。

    参考:
    * 1.PathAnimation源代码: https://github.com/drawapp8/PathAnimation
    * 2.UIPath接口描述https://github.com/drawapp8/cantk/wiki/ui_path_zh
    * 3.Cantk项目: https://github.com/drawapp8/cantk

  • 相关阅读:
    悼念丹尼斯·里奇,那个给乔布斯提供肩膀的巨人(转载)
    c# 做成Windows服务
    Visual Studio 2010 新建完项目编译就出错
    C#调用Win32 的API函数User32.dll
    c# Remoting小例子
    backgroundworker使用 实现进度条ProgressBar
    winform最小化后隐藏到右下角,单击或双击后恢复
    关于Thread的实例
    c# 捕获的异常写到日志里
    C# delegate and event 规范写法
  • 原文地址:https://www.cnblogs.com/zhangyunlin/p/6167334.html
Copyright © 2020-2023  润新知