利用Canvas实现H5动画游戏

H5游戏主要是基于HTMl实现的一种在webview上面进行游戏的网页游戏。它的优势是能够跨平台、敏捷实现、便于营销与推广,当然其劣势也很明显,虽然现在前端浏览器性能与其支持性越来越突出与卓越,但还是无法完全达到端游的流畅与性能体验。但是这并不影响它的越来越受欢迎,未来随着浏览器性能的逐渐提高,H5游戏将会变得更加广泛。

实现思路

.
无论是2d游戏还是3d游戏,通常我们使用Canvas来实现这类H5游戏,当然3d游戏主要是使用WebGL,这里我们主要是来说说关于一些基本的2d类H5游戏的实现思路。

核心

.
在Web应用中,实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout或者setInterval 来实现,css3 可以使用 transition 和 animation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。

但是传统的通过setTimeout或者setInterval实现的动画,存在两个问题,第一个就是动画的循时间环间隔不好确定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz, 1000ms / 60),第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行,为了解决这个问题,所以H5中才有了requestAnimationFrame。

1
2
3
4
5
6
7
8
9
10
11
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return window.setTimeout(callback, 1000 / 60);
}
);
}

触摸

.
既然是游戏,那么我们就必须要有触摸事件,不同的端触摸的方式不一样所以这里我们需要拿到触摸坐标。

PC上捕获坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var captureMouse = function(element) {
var mouse = { x: 0, y: 0 };
element.addEventListener('mousemove', function(event) {
var x,y;
if (event.pageX || event.pageY) {
x = event.pageX;
y = event.pageY;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
x -= element.offsetLeft;
y -= element.offsetTop;
mouse.x = x;
mouse.y = y;
}, false);
return mouse;
}

Mobile上捕获坐标:

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
var captureTouch = function(element) {
var touch = { x: null, y: null, isPressed: false, event: null },
body_scrollLeft = document.body.scrollLeft,
element_scrollLeft = document.documentElement.scrollLeft,
body_scrollTop = document.body.scrollTop,
element_scrollTop = document.documentElement.scrollTop,
offsetLeft = element.offsetLeft,
offsetTop = element.offsetTop;
element.addEventListener('touchstart', function(event) {
touch.isPressed = true;
touch.event = event;
}, false);
element.addEventListener('touchend', function(event) {
touch.isPressed = false;
touch.x = null;
touch.y = null;
touch.event = event;
}, false);
element.addEventListener('touchmove', function(event) {
var x, y,
touch_event = event.touches[0]; // 首次触摸
if (touch_event.pageX || touch_event.pageY) {
x = touch_event.pageX;
y = touch_event.pageY;
} else {
x = touch_event.clientX + body_scrollLeft + element_scrollLeft;
y = touch_event.clientY + body_scrollTop + element_scrollTop;
}
x -= offsetLeft;
y -= offsetTop;
touch.x = x;
touch.y = y;
touch.event = event;
}, false);
return touch;
};

Canvas

.
说到Canvas相信很多人都不陌生,我们经常使用Canvas来绘制一些图形,像绘制矩形,我们使用fillRect,绘制圆形,我们使用arc,绘制线条我们使用path等等。接下来我们开始使用Canvas做一个船移动路径动画游戏。

1、获取画布

1
const context = canvas.getContext('2d');

2、添加背景

1
2
context.draw(bgImage, 0, 0, canvas.width, canvas.height);
context.draw(boatImage, 0, 0, canvas.width, canvas.height);

有了背景之后,我们再用同样的方式将船也绘制到背景上。绘制的过程,我们需要将其放在requestAnimationFrame中执行

1
2
3
4
5
6
(function drawFrame(){
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
context.draw(bgImage, 0, 0, canvas.width, canvas.height);
}())

当然船的绘制我们需要注意船的起始坐标,我们需要先给船一个起始坐标,这里主要是船相对于背景来说所在的位置

1
2
3
4
5
const wratio = xx;
const hratio = xx;
// 注意:这是个需要适配机型的系数, 不同宽高的尺寸,船相对于背景所在的位置是不一样的,所以我们需要用这个系数来做微调。
const x = screenWidth / wratio - boatWidth / 2;
const y = screenHeight / hratio - boatHeight / 2;

这样船相对于背景的起始坐标,我们就拿到了,我们还需要拿到船的终点坐标,终点坐标同样是相对于背景来说的,所以同样需要进行微调。

1
2
const ex = scrrenWidth / 2 - boatWidth;
const ey = bgh * hratio;

有了起始坐标和终点坐标,那么我们就可以绘制船到背景上

1
2
3
4
5
6
7
(function drawFrame(){
window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
context.draw(bgImage, 0, 0, canvas.width, canvas.height);
context.draw(boatImage, x, y, boat.width, boat.height);
}())

这样船与背景都被绘制出来了,但是仅仅是这样还不够的,这只是一个静态图,同时我们还需要处理绘制出的背景的清晰度问题,同时我们还需要控制canvas宽高仅仅只为一个屏幕的宽高,所以我们需要在这里处理一下。

设备像素比显示高清背景

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const devicePixelRatio = function(context) {
const backingStore =
context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
conext.msBackingStorePixelRatio ||
conext.oBackingStorePixelRatio ||
conext.backingStorePixelRatio ||
1;
return (window.devicePixelRatio || 1) / backingStore;
}
const pxratio = devicePixelRatio(offCanvas.getContext('2d')); // 设备像素比
canvas.width * pxratio
canvas.height * pxratio

注:offCanvas,离屏Canvas。

非离屏渲染是不建立渲染缓冲区的,直接在屏幕上逐个进行绘制,需要重复利用canvas的api。当粒子数量到达一定等级时,性能上会受到较大影响。离屏canvas在屏幕渲染的时候开辟一个缓冲区,将当前需要加载的动画事先在缓冲区渲染完成之后,再显示到屏幕上已到达渲染性能的优化。

生成离屏canvas:

1
'offscreenCanvas' in window && canvas.transferControlToOffscreen();

离屏Canvas是不具备canvas元素的样式属性的,它仅仅继承了canvas的 Width 和 Height属性,你可以将它看成是一个Canvas的缓冲镜像。

接下来,按照设备像素比扩大了canvas的宽高之后,我们还需要对画布进行扩大!

1
context.scale(pxratio, pxratio);

这样一来整个canvas都按照设备像素比进行了等比放大,那么接下来要保证canvas能在一屏幕中展示,并不失真就需要再进行一次矩阵转换。这里我们是用的设备像素比放大,那么同理就用设备像素比进行矩阵缩放。

1
canvas.style.transform = `matrix(${1 / pxratixo}, 0, 0, ${1 / pxratio}, 0, 0)`;

matrix 定义 2D 转换,使用六个值的矩阵。

1
2
3
4
5
「 「
1, 0, 0 也就是 a, c, e
0, 1, 0 ======> b, d, f ==== (ax + cy + e, bx + dy + f, 1)
0, 0, 1 0, 0, 1
」 」

到这一步内容的绘制就基本完成了,接下来就是考虑如何将内容以及操作联动起来!但是在这之前,我们得先计算好整个移动轨迹,如下图。

矢量计算

.

1
2
3
4
5
6
t0 = t1 + t2
s1 = (dragY1 / tanθ2 | dragY1 * cotθ2) // 临边
s2 = (dragY1 * sinθ1 | dragY1 * cosθ2) // 斜边
中间点:(x - s1, dragy)

这里我们计算出了中间点,那么就可以用二次贝塞尔曲线来进行坐标定位,如图:

这里需要注意的是贝塞尔在移动过程中是不会触及顶点的,所以我们需要再对中间点进行扩充,也就是误差调试。在已有的中间点基础上将x轴往左偏移得到x’。接下来就是套用公式:

1
2
3
const quadraticBezier = function(p0, p1, p2, t) {
return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
}

那么问题来了,我们这里涉及到t,那么t我们该如何计算呢?答案当然是用拖动距离与总距的比值来计算,这里精确到ms级。如下图:

1
2
3
4
5
6
7
8
percent1 = dragy / ty1; // 第一段移动过程
percent2 = dragy / ty2; // 第二段移动过程
for(let t = 0, t < percent, t += 0.001) {
const x = quadraticBezier(startP0X, middleP1X, endP2X, t);
const y = quadraticBezier(startP0Y, middleP1Y, endP2Y, t);
console.log(x, y);
}

好了移动计算搞定,那么我们就需要着手拖动了,上面介绍了拖动的坐标捕获,但是仅仅是这样还不够的,我们还需要对坐标进行处理。

首先要获取拖动距离即dragY,拖动分为向上和向下,拖动也不能超出边界,拖动完成后还需要考虑拖动速度,涉及到速度就又需要考虑到拖动缓冲移动所以就需要配置拖动的摩擦系数,防止无限加速。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// touchstart中定义
const h = offsetY - dragY;
// touchmove中定义
dragY = offsetY - h; // 拖动距离
// 定义当前拖动距离大于0为加速,小于0为减速
speed = dragY - lastDragY;
// 最终拖动导致的移动距离为
dragY += speed;
// 加入摩擦系数
speed *= fl; // fl值越小缓冲距离越短,即摩擦越大

以上基本就是实现拖动移动路径动画游戏的基本思路,这里是使用的二次贝塞尔曲线,如果要想达到移动路径更完美受控,使用一次贝塞尔曲线更好,但是这样一来需要计算内容就更多了。。。