前言
游戏世界里面充斥着大量的数学与物理知识,就连一个普通的大转盘亦是如此。下文解释的是转盘在运动过程中,时间,转动距离的换算关系,这和选择了具体什么框架没有任何关系,你可以使用dom,或者canvas,或游戏引擎实现角度到视觉的表达呈现。
可以在这个代码里面查看到交互效果,代码示例
- demo - html dom 版本 -codesandbox.io/s/youthful-…
- dome - canvas 版本 - codesandbox.io/s/nice-nigh…
大转盘的交互流程
首先我们设想一个大转盘的奖的动作,它具有如下的流程
- 当用户开始抽奖时,转盘必须立刻开始转动起来,并且是匀加速启动,最后匀速
- 此刻并不知道抽奖结果,在业务压力的高峰期,抽奖可能会比较耗时,需要等待一会儿才返回
- 因此我们在等待转盘结果的同时,转盘应该保持持续的运动
- 在获得到抽奖结果时,转盘最终应该缓慢的停在抽奖结果的位置上面
如上面的描述,转盘的速度曲线在整个过程中应该如下所示
-
在阶段一 (t0 - tt1 阶段),转盘做匀加速运动,此时转过的距离应该为 S1
-
加速到一定阶段后,装盘会沿着当前速度持续转动,在这个阶段,这个转盘速度不变,即 t1 - t2 阶段,此时的位移是 S2
-
在 t2 时刻,收到停止信号后,这个时候转盘会缓慢停下来,做匀减速运动,此时的位移内容是 S
vt曲线图,面积即是位移内容,由于这里的曲线都是直线,因此很好的计算面积大小
我们不妨假定,转盘的最大速度是 0.48deg/ms , 转盘的加速阶段的时间为 2000ms, 停止信号(即t2)发生时间是不确定的,最终的位移总和目前也是不确定的
WheelSprite 类编写
根据上面的描述,我们设计的一个这样的 WheelSprite, 他拥有
class WheelSprite{
// 启动旋转
async function start(): void;
// 另转盘停到指定的角度上面
async function stop(target: number): Promise<void>;
// 当前转动的位置
public dist: number;
// 转盘的三个阶段,分别是暂停状态,加速状态,还有减速状态,加速状态包括匀加速阶段和匀速阶段
public status: 'paused' | 'acceleration' | 'decelerate';
// 当前的角速度
public v: number;
// 各个时间
public t0: number;
public t1: number;
public t2: number;
public t3: number;
}
补充一些常亮=量的定义
const MAXIMUM_SPEED = 0.48; // 每一毫秒的速度
const ACCELERATION_TIME = 2000; // 加速的时间
const ACCELERATION_PER_MS = MAXIMUM_SPEED / ACCELERATION_TIME; // 每毫秒的加速度
编写方法 function run(): void; 启动大转盘
启动方法应该包含 t0 到 t2 阶段的所有工作,先做匀加速运动,在做匀速运动,我们记开始执行代码的时候为 t0, 那么在 t0 到 t1 阶段,这是一个三角形,则
vn / 0.48 = △t / 2000
即 vn = △t / 2000 * / 0.48
S = vn * △t / 2
当 △t > t1(2000ms), 后,则应该再加上匀速的距离,则
S1 = 0.48 * 2000 / 2
S = S1 + (△t - 2000) * 0.48
async function run(){
this.status = "acceleration";
this.t0 = new Date();
// 除非收到 decelerate 的状态,否则这个将会一直旋转
while (this.status != "decelerate") {
await new Promise((resolve) => requestAnimationFrame(resolve));
// 执行的时间间隔,用来计算当前的位置
const dif_t = new Date() - this.t0;
// 如果过了加速的时间段,则位移应该是加速的位移加上匀速的位移
if (dif_t > ACCELERATION_TIME) {
this.v = MAXIMUM_SPEED;
let s1 = MAXIMUM_SPEED * ACCELERATION_TIME / 2;
this.dist = s1 + MAXIMUM_SPEED * (dif_t - ACCELERATION_TIME);
} else {
this.v = dif_t / ACCELERATION_TIME * MAXIMUM_SPEED;
this.dist = this.v * dif_t / 2;
}
}
}
核心方法 - async function stop(stopAngle: number): Promise<void>; 将大转盘停到制定的位置
接下来我们编写 stop 方法,为了防止过快返回让整个动画过程不完善(比如从开始转动到收到结果,可能只需要500ms,此时还处于加速阶段),我们通过增加延时,来保证 stop 的执行至少要在 t0 3秒之后
t = new Date() - this.t0 表示当前的过去的时间, 当 3000 - t > 0 时,说明至少还要等待 3000 - t 毫秒才能继续逻辑,反之则直接继续
async function stop(target){
if(this.status != 'acceleration'){
throw new Error('转盘状态错误');
}
// 保证从t0到当前时刻至少执行了3000
// 如果 new Date() - this.t0 > 3000 则说明从启动到当前已经过去3000ms
// settimeout 在处理负数的数值时会立刻放行
await new Promise(resolve=> setTimeout(resolve, 3000 - (new Date() - this.t0)))
// 记录这个时间
this.t2 = new Date();
while(1){
await new Promise(resolve=> setTimeout(requestAnimationFrame));
// 此处应该处理匀减速流程
// todo
}
}
写到这里,基本的雏形写完了,我们接下来处理最核心的匀减速问题, 在匀减速场景,我们知道如下内容
- 当前的速度 v
- 当前的转动过得距离 dist
- 以及外部传进来的希望停下来的角度 target
- 开始进行匀减速的时间 t3
如图所示,target 是绿色的三角形部分 S3 , 这部分的面积即是即将产生的额外距离,那么这部分的面积大小,则应该满足
target + 360 * n = S1 + S2 + S3
此时,S1, S2 是确定的, 记为S,n可以是任意的正整数, 由于一圈的赌术是360度,我们对 S 360取余并不会影响视觉角度,我们假定 n = 2, 则
target + 360 * 2 - S % 360 = S3
在位移公示,则 t3 - t2 = △t 为,
target + 360 * 2 - S % 360 = MAXIMUM_SPEED / 2 * △t
任意时刻,其实这是一个梯形,则 tn 时刻的位移为
// t2Tot3 表示 t2 到 t3所需要的时间
// t 表示当前时刻
vn = MAXIMUM_SPEED - MAXIMUM_SPEED / t2Tot3 * (t - t2)
s = S + (vn + MAXIMUM_SPEED) * (t - t2) / 2 // 梯形公式
根据如上结果,目前未知的变量只有两个,一个是 n 圈数(决定停下的距离), 一个是 △t 剩余转动的时间,由于 n 有整数限制,我们不妨假定 n 为一个固定值,则我们 stop 代码新增
this.t2 = new Date();
this.status = "decelerate";
// 标记匀减速前所有的路径总和
let S = this.dist;
// target + 360 * 2 - S % 360 = MAXIMUM_SPEED / 2 * △t
let t2Tot3 = (target + 360 * 2 - S % 360) / ( MAXIMUM_SPEED / 2);
this.t3 = this.t2 + t2Tot3;
while(1){
await new Promise((resolve) => requestAnimationFrame(resolve));
const dif_t = new Date() - this.t2;
// 考虑到当时间超出t3后,vn会变成负数,此处做一下限制
let vn = Math.max(0, MAXIMUM_SPEED - MAXIMUM_SPEED / t2Tot3 * dif_t);
this.dist = S + (vn + MAXIMUM_SPEED) * dif_t / 2; //梯形公式
// 当到达终点的时候, vn = 0
if (vn == 0) {
this.dist = target;
break;
}
}
// 转盘结束
那么为什么我们要做设计 n = 2 呢,下面这张图可能会有答案
- 对于转盘来说,多一个或者多个整数倍的360并不会影响视觉的结果
- 由于距离等同于坐标图的面积,当
n越大,剩余转动的距离越远,则所需要的面积越大,则减速曲线就会越平滑,减速时间就会越长,反之亦然 - 当
n->无穷的时候,减速就会越不明显,我们令n = 2,只是一种特例解,用来反向推出减速曲线,如果n = 2时,转盘减速痕迹太过明显,则我们应该加大2,保证更充足的减速时间,如果减速痕迹过于缓慢,则我们可能要考虑一下减少n了
除了 n 会影响减速的效果, 其实最大速度也会影响,这些参数都是可以调的,找到合适的即可