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

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                       

路径动画让对象沿着指定路径运动,在游戏中用着广泛的应用,比如塔防类游戏就经常使用路径动画。前几天在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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 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);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

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

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;}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106

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

           

给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

这里写图片描述

猜你喜欢

转载自blog.csdn.net/jggyff/article/details/84194561
今日推荐