写在前面
先玩一把再看(额声明:并没有通关的说)
(注:本文写在游戏写完之后,所以代码并不是从 0 开始的)
小时候在小霸王上玩过一款游戏,忘记名字了,应该是叫火箭车吧,看起来蛮简单的(这 flag 立的太明显了(T^T)),正好这几天比较闲,就想拿 canvas 实现一下,因为之前一直不能理解 canvas 动画,正好也熟悉熟悉锻炼一下(其实还想练练写文章)。
只会写干货
怎么写游戏呢,没经验,首先想到的是我的游戏里要有什么,回忆火箭车简单的分析一下,
- 地图上有公路,公路两旁是草地
- 有一辆自己的车,可以左右上下移动
- 有 n 个对手,对手的速度不一致
- 有障碍物
- 地图的路会拐弯
- 有碰撞,碰撞后车会旋转速度减为 0
- 有进度条
嗯其实功能点还是不少的,挨个儿来。
Before the first, 要明白 canvas 的动画原理,就像电影一样,是按桢来的,所以要有一个函数(就叫她 draw吧) 来绘制每一帧的画面,然后 requestAnimationFrame 来将每一帧连起来, requestAnimationFrame 是大约每 16 毫秒执行一次,所以画面非常连贯,看起来就是动画了。
所以 canvas 动画是这么运作之后,先找素材吧,images.google.com 直接搜 car top 就是满屏的车辆俯视图,挑一些好看的合适的,拿 PS 切成 png 图,裁剪掉透明像素就好了。
接下来我们先定义一些变量,比如我们的小车车、canvas 对象等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var canvas = document.getElementById('canvas') var context = canvas.getContext('2d') var contextWidth = canvas.width var contextHeight = canvas.height var car = { img: 'car.png', x: 0, y: 0, width: 0, height: 0 }
var move = { x: 0, y: 0, speed: 2, friction: 0.9 }
|
好的然后我们需要定个流程骨架,先什么再什么,嗯我们需要个 init 函数来初始化,初始化里来启动第一桢,由于 canvas 动画的原理,即意味着每一帧都要把所有内容都要绘制一遍,所以 draw 里边我们先
- clearContext清掉上一桢
- drawBackground画背景草地
- drawRoad 画公路
- drawEnemys 画对手的小车车
- drawCar 画自己的小车车
- requestAnimationFrame(draw) 画下一桢
这么定义。另外还需要个 listener 函数来监听方向键按键。
1. clearContext清掉上一桢
这个简单,
1 2 3
| function clearContext() { context.clearRect(0, 0, contextWidth, contextHeight) }
|
即可。
2. drawBackground画背景草地
我觉得还是先找素材吧,Google 了一通也没找到合适的就随便选了张拿 PS 剪了剪,然后定义变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var road = { img: new Image(), direction: 1, path: [], meadowI: 0, meadows: [], zebraCrossing: [], zebraCrossingI: 0, meters: 0, width: 0 } road.img.src = 'roads.png' road.img.onload = function() { var x = Math.ceil(contextWidth / this.width) var y = Math.ceil(contextHeight / this.height) for (var i = -1; i < y; i++) { for (var j = 0; j < x; j++) { road.meadows.push([j*this.width, i*this.height, this.width, this.height]) } } init() listener() }
|
road.meadows 为二维数组,我们是从上到下绘制,草地还需要向下移动,所以 x 轴第一列的 y 坐标应该是负数,所以从 i = -1 开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function drawBackground() { var x = Math.ceil(contextWidth / road.img.width) var y = Math.ceil(contextHeight / road.img.height) road.meadowI++ if (road.meadowI > 10) { road.meadowI = 0 } if (road.meadows.length) { road.meadows.forEach(function(i) { context.drawImage(road.img, i[0], i[1] + road.meadowI * 6, i[2], i[3]) }) } }
|
3. drawRoad 画公路
公路也很简单,画个矩形
1 2
| context.fillStyle = '#888' context.fillRect(road.x, 0, road.width, contextHeight)
|
嗯还没说 road.x 和 road.width 怎么来的呢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var carImg = new Image() carImg.onload = function() { car.width = this.width car.height = this.height car.x = contextWidth / 2 - car.width / 2 car.y = contextHeight - car.height - move.y road.width = car.width * 4 + 10 road.x = contextWidth / 2 - road.width / 2 var f41 = road.width / 4 for (var j = -10; j < contextHeight / 50; j++) { for (var i = 1; i <= 3; i++) { road.zebraCrossing.push([road.x + f41 * i, j * 60, 2, 40]) } } } carImg.src = car.img
|
加载上自己的小车车后,把小车车放到画布的正中下,公路的宽度定为车的宽度的 4 倍外加 10 个像素(没啥原因,就是想加 10 个像素),分割线怎么画,定义每个分割线的矩形为 宽 2 像素,高 40 像素,j = -10 的原因也是因为分割线向下移动,给人一种画面在向上前进的感觉。
OK,再在空空的矩形公路上画上分割线,移动规律理同草地
1 2 3 4 5 6 7 8 9
| road.zebraCrossingI++ if (road.zebraCrossingI > 30) { road.zebraCrossingI = 0 } if (road.zebraCrossing.length) { road.zebraCrossing.forEach(function(i) { context.fillRect(i[0], i[1] + road.zebraCrossingI * 6, i[2], i[3]) }) }
|
再画两条路两边的白色不可跨域实线
1 2 3
| context.fillStyle = '#fff' context.fillRect(road.x - 3, 0, 3, contextHeight) context.fillRect(road.x + road.width, 0, 3, contextHeight)
|
嗯公路就这样了,我还没想出来怎么让公路能拐弯…
4. drawEnemys 画对手的小车车
想了想对手貌似不太好实现,还是先将对手作为障碍物的存在吧,嗯还是先定义些变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var enemySrc = (function() { var r = [] for (var i = 1; i <= 12; i++) { r.push({ width: 0, height: 0, img: 'enemy'+i+'.png' }) } return r }()) var enemys = [] for (var i = enemySrc.length - 1; i >= 0; i--) { enemySrc[i].imgObj = new Image() enemySrc[i].imgObj.onload = (function(obj) { return function() { obj.width = this.width obj.height = this.height } })(enemySrc[i]) enemySrc[i].imgObj.src = enemySrc[i].img }
|
障碍物需要放到公路上,所以生成的时候根据 road.x road.width 公路的 x 坐标和宽度来,在初始化的时候生成第一个障碍物
1 2 3 4 5 6 7 8 9 10 11 12
| var _random = Math.ceil(randomWithRange(-1, enemySrc.length + 1)) var _img = enemySrc[((_random+0) >= 0 ? _random : 1)]
var _index = Math.floor(randomWithRange(0, enemys.length + 1)) enemys = [{ img: _img, index: _index, x: Math.floor(road.width / 4 * _index + road.x), y: 0, inc: Math.ceil(randomWithRange(2, 5)) }]
|
我们需要生成障碍物的数据,所以需要有个 geneEnemy 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function geneEnemy() { var _img = enemySrc[Math.ceil(randomWithRange(-1, enemySrc.length + 1))] if (Math.random() * 10 > 5 && _img && enemys.length < 3) { var _index = Math.floor(randomWithRange(0, enemys.length + 1)) if (!enemys[enemys.length - 1] || _index != enemys[enemys.length - 1].index) { enemys.push({ img: _img, index: _index, x: Math.floor(road.width / 4 * _index + road.x), y: 0, inc: Math.ceil(randomWithRange(2, 5)) }) } } }
|
好了,接下来我们可以画障碍物了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function drawEnemy() { if (enemys.length) { for (var i = 0; i < enemys.length; i++) { if (enemys[i + 1]) { enemys[i + 1].y + enemys[i + 1].height } } for (var i = 0; i <= enemys.length - 1; i++) { if (enemys[i].y > contextHeight) { enemys = enemys.slice(i) } else { enemys[i].y += enemys[i].inc context.drawImage(enemys[i].img.imgObj, 0, 0, enemys[i].img.width, enemys[i].img.height, enemys[i].x, enemys[i].y, enemys[i].img.width, enemys[i].img.height) } } if (enemys[enemys.length - 1].y > (contextHeight / 3)) { geneEnemy() } } }
|
5. drawCar 画自己的小车车
好的接下来改画我们自己的小车车了,初版的实现里我并未实现自己的车移动时有动画效果,后来搜了下才实现缓动效果
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
| var keys = []; function drawCar(e) { if (keys[38]) { if (move.y > -move.speed) { move.y--; } } if (keys[40]) { if (move.y < move.speed) { move.y++; } } if (keys[39]) { if (move.x < move.speed) { move.x++; } } if (keys[37]) { if (move.x > -move.speed) { move.x--; } } move.y *= move.friction; car.y += move.y; move.x *= move.friction; car.x += move.x; context.drawImage(carImg, 0, 0, car.width, car.height, car.x, car.y, car.width, car.height) } function listener() { document.onkeydown = function(e) { keys[e.keyCode] = true }; document.onkeyup = function(e) { keys[e.keyCode] = false } }
|
这个不解释了…
6. 然后呢
嗯接下来其实可以玩了,但是还缺了点什么,对碰撞检测呢,写了一大串
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 63 64 65 66 67 68 69 70
| function detectAccident() { var carCoordinate = { topLeft: { x: car.x + carCorner, y: car.y + carCorner }, topRight: { x: car.x + car.width - carCorner, y: car.y + carCorner }, bottomLeft: { x: car.x + carCorner, y: car.y + car.height - carCorner }, bottomRight: { x: car.x + car.width - carCorner, y: car.y + car.height - carCorner } } if (car.x < (road.x - carCorner) || (car.x + car.width) > (road.x + road.width + carCorner) ) { throw 'AccidentException'; } for (var i = 0; i < enemys.length; i++) { var enemysCarCoordinate = { topLeft: { x: enemys[i].x + carCorner, y: enemys[i].y + carCorner }, topRight: { x: enemys[i].x + enemys[i].img.width - carCorner, y: enemys[i].y + carCorner }, bottomLeft: { x: enemys[i].x + carCorner, y: enemys[i].y + enemys[i].img.height - carCorner }, bottomRight: { x: enemys[i].x + enemys[i].img.width - carCorner, y: enemys[i].y + enemys[i].img.height - carCorner } } if ( isAccident(carCoordinate, enemysCarCoordinate) ) { throw 'AccidentException'; } } } function isAccident(car, enemy) { return (car.topLeft.x >= enemy.topLeft.x && car.topLeft.x <= enemy.topRight.x && car.topLeft.y >= enemy.topLeft.y && car.topLeft.y <= enemy.bottomLeft.y) || (car.topRight.x >= enemy.topLeft.x && car.topRight.x <= enemy.topRight.x && car.topRight.y >= enemy.topRight.y && car.topRight.y <= enemy.bottomRight.y) || (car.bottomLeft.x >= enemy.topLeft.x && car.bottomLeft.x <= enemy.topRight.x && car.bottomLeft.y >= enemy.topLeft.y && car.bottomLeft.y <= enemy.bottomLeft.y) || (car.bottomRight.x >= enemy.topLeft.x && car.bottomRight.x <= enemy.topRight.x && car.bottomRight.y >= enemy.topRight.y && car.bottomRight.y <= enemy.bottomRight.y) }
|
总结
嗯,大概就是酱紫啦,再加上开始和结束,就算暂时完工了,有时间再继续研究怎么让路拐弯…,还是很遗憾并没有做的跟火箭车一样…( p_q)
代码见 resource 吧…( ̄∀ ̄)。
第一次写小游戏,欢迎大神指教(o^^o)♪。