上一小节,我们成功添加了 NPC 和跑道效果,但目前为止,主角还是静止的,NPC 一过来,主角就只有 GG 了,游戏当然不能这么玩 😂。因此本小节我们实现的目标:控制主角躲避 NPC,而动画方式有:
上面的动画涉及到元素的位移、旋转等,我们逐帧计算不太现实,我们需要借助一个第三方的类库Tween.js,这个库主要是帮助我们快速处理补间动画。
动画
跳跃
我们需要分解一下跳跃动画,主要分成了 3 个动画:
Tweenjs 提供了补间动画分组功能,主要方便我们在动画完成后,可以快速清除动画的计算。首先我们创建一个动画分组:
1
| const jumpTween = new TWEEN.Group();
|
然后确定跳跃的高度:主角高度 x 2,跳跃时间为 500ms:
1 2
| const jumpHeight = this.size.height * 3; const durations = 500;
|
确定了数值后,就可以写补间动画了:
1 2 3 4
| new TWEEN.Tween(meshPosition, jumpTween) .to({ y: jumpHeight }, durations / 2) .easing(TWEEN.Easing.Quadratic.Out) .start();
|
接下来我们要实现第二个动画:物体重力下落动画,Tweenjs 提供了链式调用的方法chain,该方法接受一个回调函数,并且会在向上跳起动画完成后执行:
1 2 3 4 5 6 7
| new TWEEN.Tween(meshPosition, jumpTween) .to({ y: 0 }, durations / 2) .easing(TWEEN.Easing.Quadratic.In) .onComplete(() => { jumpTween.removeAll(); }),
|
依葫芦画瓢,用到补间动画修改 threejs 网格的rotation去实现翻转:
1 2 3 4 5
| new TWEEN.Tween(meshRotation, jumpTween) .to({ x: -Math.PI * 2 }, durations) .easing(TWEEN.Easing.Quadratic.Out) .start();
|
大体的跳跃动画就实现了:

位移移动
在这个游戏中,我们的位移动画分为两个方向:向左与向右,我们新建一个方法: move,同时接收一个参数direction,LEFT表示向左,RIGHT表示向右,移动距离大小为单条赛道的的宽度。因此实现起来比跳跃更加简单:
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
| public move(params: { direction: IDirection; }) { const { direction } = params; const meshPosition = this.mesh.position; const moveTween = this.moveTween; const durations = 200; let moveDistance = 0; if (direction === 'LEFT') { moveDistance = - RACETRACK.segmentWidth; } else if (direction === 'RIGHT') { moveDistance = RACETRACK.segmentWidth;
} new TWEEN.Tween(meshPosition, moveTween) .to({ x: meshPosition.x + moveDistance }, durations) .easing(TWEEN.Easing.Back.Out) .onComplete(() => { moveTween.removeAll(); }) .start(); }
|
效果如下:

控制
目前为止,主角的动画都是代码自执行的,因此我们还需实现一个功能:控制器,主要的操作是点击和滑屏操作。
微信小游戏的环境没有内置滑动的 API,需要我们用onTouchStart,onTouchEnd去模拟。原理是,我们在onTouchStart时记录点击坐标,再用onTouchEnd的x, y坐标减去 onTouchStart 的x, y坐标,如果x坐标差值小于 10,表示点击操作,否则 ,我们再用坐标差算出滑屏的角度,公式是:
angle = Math.atan2(angY, angX) * 180 / Math.PI;
当 angle >= -45 && angle <= 45时,表示向右滑屏;
当 angle >= 135 && angle <= 180 或 angle >= -180 && angle < -135时表示向左滑屏。
否则表示向上或向下滑屏,我们暂时不处理。
知道了大概的实现原理后,就可以撸码了:
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
| const getTouchResult = ( startPoint: IPoint, endPoint, ): { type: 'SLIDE' | 'TAP' | 'UNKNOW', value: IDirection | IPoint | '', } => { const { x: startX, y: startY } = startPoint; const { x: endX, y: endY } = endPoint; const angX = endX - startX; const angY = endY - startY; if (Math.abs(angX) >= 10) { const angle = (Math.atan2(angY, angX) * 180) / Math.PI; if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) { return { type: 'SLIDE', value: 'LEFT', }; } if (angle >= -45 && angle <= 45) { return { type: 'SLIDE', value: 'RIGHT', }; } return { type: 'UNKNOW', value: '', }; } return { type: 'TAP', value: endPoint, }; };
let startPoint: IPoint = { x: 0, y: 0 }; const touchStart = ({ touches }) => { startPoint = { x: touches[0].clientX, y: touches[0].clientY, }; };
const touchEnd = ({ changedTouches }) => { const { type, value } = getTouchResult(startPoint, { x: changedTouches[0].clientX, y: changedTouches[0].clientY, }); if (type === 'SLIDE') { } if (type === 'TAP') { } }; wx.onTouchStart(touchStart); wx.onTouchEnd(touchEnd);
|
上面的代码其实不只用于控制主角,后续我们还需要用于点击菜单按钮等,所以这里需要考虑把这部分代码抽离成通用模块Gamepad(游戏手柄),再用发布订阅的方式(用到了 facebook 的emitter),游戏或按钮元素去订阅这些事件,从而实现控制元素的功能。下面是实现控制主角后的效果,可以看出我已经可以控制主角,轻松自如地闪躲 NPC 了 😁:

总结
当然还有一些细节需要考虑,比如每次左右移动需要判断是否到了边界,在跳跃过程中,不允许二次跳跃等问题,另外添加了主角的跳动效果。详细的项目结构如下:
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
| ./src ├── Game │ ├── Gamepad // 游戏手柄 │ │ └── index.ts │ ├── NPC // NPC角色 │ │ ├── box.ts // 正方形 │ │ ├── cone.ts // 锥形 │ │ └── index.ts │ ├── Player // 游戏主角 │ │ └── index.ts │ ├── Pool // 对象池 │ │ └── index.ts │ ├── Racetrack // 跑道 │ │ └── index.ts │ ├── camera // 摄影机 │ │ └── index.ts │ ├── constant.ts // 常量 │ ├── helper │ │ ├── axes.ts // 辅助坐标系 │ │ └── orbitControls.ts // 摄影机轨道控制器 │ ├── index.ts │ ├── renderer // WebGL渲染器 │ │ └── index.ts │ ├── scene // 场景 │ │ └── index.ts │ └── util // 工具 │ └── index.ts ├── index.ts // 入口 └── lib ├── weapp-adapter-extend // weapp-adapter的扩展,新增window的方法 │ ├── index.js │ └── window.js └── weapp-adapter.js // 模拟BOM,DOM
|
代码::https://github.com/inarol/rungame/tree/section3
[本文谢绝转载,谢谢]