游戏中的路径动画设计与实现
2015-09-12 11:39
387 查看
路径动画让对象沿着指定路径运动,在游戏中用着广泛的应用,比如塔防类游戏就经常使用路径动画。前几天在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,实现一些缺省的行为。
1.直线。两点决定一条直线,从一个点运动到另外一个点。
2.弧线,由圆心,半径,起始幅度和结束幅度决定一条弧线。
3.抛物线。这里的抛物线不是数学上严格的抛物线,也不是物理上严格的抛物线,而是游戏中的抛物线。游戏中的抛物线允在X/Y方向指定不同的加速度(即重力),它由初始位置,X/Y方向的加速度和初速度决定。
4.正弦和余弦曲线其实一样,正弦偏移90度就是余弦。它由初始位置,波长,波速,振幅和角度偏移决定。
5.三次贝塞尔曲线。它由4个点决定,公式请参考百度文库。
6.二次贝塞尔曲线。它由3个点决定,公式请参考百度文库。
现在我们把它们包装一下:
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
先说下路径动画的基本需求:
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
相关文章推荐
- raspberry pi镜像压缩备份 linux
- 使用Intent调用系统其它程序打开本地各种类型的文件
- 游戏中的路径动画设计与实现
- iOS中键值监听KVO的学习
- HDU 5003 Osu!
- Leetcode: Maximal Square
- RMQ问题
- Android:Resources资源文件
- Git 常用命令整理
- HDU 1242 【搜索+记忆化。。?】
- iOS 8 新特性autoLayout
- [JWFD开源工作流]JWFD开源工作流官方下载内容更新
- 在一个字符串中找到第一个只出现一次的字符。
- getevent/sendevent 使用说明
- js实现右键管理
- ccf练习---节日
- 由单例模式探讨JVM的内存管理机制
- 微信公众账号 Senparc.Weixin.MP SDK 开发教程
- Android:布局实例之常见用户设置界面
- 泊松分布和幂律分布 转