小火车是怎么跑起来的
作者: 发布于:

前言

淘宝玩法平台 由淘宝 FED 团队互动小组开发和维护,是一款为运营/视觉同学提供自主搭建/定制游戏玩法服务的平台,平台提供丰富的游戏模板,和超级可定制能力,分分钟就可自己搭建出个性十足的游戏玩法。

淘宝玩法平台的版头动画,是由我们的视觉设计师 @鲍龙 设计,由我实现的。

点击 Demo 观看实际效果。

版头动画中,跑着一列小火车。这销魂的路线,确实能给人「眼前一亮」的感觉,也有一些同学询问我实现的原理。其实,真的很简单,就是一点小技巧而已。

采集数据

首先,我沿着轨道采集了一些点的坐标。平滑的地方可以稀疏一些,曲率较大的地方需要采集得密一些。采集好的点,画出来大概是这样的:

确定位置

动画的基本原理,就是在每一帧的时候进行重绘。我们需要知道,某一帧 时,小火车的位置(坐标)是多少?

假设小火车是匀速的,速度为 speed,那么我们可以知道:在某一帧时,小火车已经行进的距离 L,是上一帧行进的距离 L’ 加上两帧的时间差 inter 乘以 speed。那么,我们只需为小火车保存当前的 L,在每一帧,就可以获得最新的 L,进而决定那一帧火车的位置。代码如下(这里是为了理解方便,实际情况要复杂一些,我们不会用 setInterval,时间间隔也不会那么武断地取常量)。

train.speed = 10;
train.L = 0;
setInterval(function(){
    var inter = 50;
    train.L += inter * trans.speed;
    // 根据 train.L 算出 train 的位置
    // 在那个位置上画出 train
}, 50)

有了小火车当前帧已经行进的距离 L,又有了轨道的踩点数据,如何确定小火车的状态呢?勾股定理帮助我们计算轨道中任意两个相邻踩点之间的距离,进而我们也可以得到整个轨道的长度。这样,就能够通过 L 求取火车位于轨道上的哪两个踩点之间,以及火车在它们之间的哪个位置。

假设小火车在第 i 个点 p[i] 和第 i+1 个点 p[i+1] 之间,而且在比例为 q 的位置上,如何确定小火车的位置呢。

简单的线性内插,求得:

train.x = (1-q) * p[i].x + q * p[i+1].x
train.y = (1-q) * p[i].y + q * p[i+1].y

这样就能够算出小火车每一帧的位置坐标,我们只需要在 (train.x ,  train.y) 位置绘制出火车即可。

确定角度

现在,小火车已经能开动了。但遗憾的是,它永远只保持着一个方向,但显然的是,沿着轨道方向的小火车更加真实。我们可以通过轨道上任意相邻两点的坐标(比如下图的 A B 两点),求出这两点间线段的角度。然后在对小火车平移之后,先不急着绘制,再旋转这个角度绘制,小火车就能沿着轨道了。

看上去不错,但是还不够,因为采样点不够密集,所以在每个采样点处,小火车的旋转角度都会「骤变」一下,动画看起来就会不流畅。所以,对于每一帧的旋转角度,我们再进行一次内插。

首先,为每个点确定一个「切线」法向量,如 A 点的 theta_A,B 点的 theta_B,B 点的 theta_C。确定的过程,可以取前后两点连线的法线,比如 theta_B 就是 AC 两点的法线。然后对于任意一帧,根据小火车的位置,内插出火车的角度。

train.theta = (1-q) * p[i].theta + q * p[i+1].theta

这样,小火车跑动就不会再一卡一卡的了。

速度问题

之前,我们把小火车的速度设置为了常量,但是如果它的速度是合理变化的,那就会更加逼真了。对于过山车而言,简单的能量守恒定律告诉我们,速度的平方与小火车的高度存在某种线性关系。稍加推理,不难写出:

// 在每一帧
train.speed = Math.sqrt(C1 - C2 * train.y)

适当地调节两个常量 C 1C 2 的值,使动画看上去比较合理。这样,动画就完成了。

总结

动画的原理很简单,最关键的就是「内插」而已。每一帧中,不管是小火车的位置还是角度,都是通过内插得来的。这样,位置和角度的值就会是连续变化的。其实,这种情况,也许把曲线拟合成若干条贝塞尔曲线互相连接,会更加「完美」,但是经验告诉我,拟合贝塞尔曲线也许没那么轻松,而适当的采样点也蕴含了足够的信息,足以使我完成这个动画。