前言

一直很想做游戏很久了,决定开始做一些小项目。本项目基于js编写,需要的函数参考webapi文档,考虑之后改写成其他版本

从读取图片开始

想画图先写一个读图函数,实现最基本的读取图片功能。

1
2
3
4
5
var readImage = function(imgpath){
var img = new Image()
img.src = imgpath
return img
}

如何画图参考官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
##通过选择器选择画布

const image = new Image(60, 45); // 文档设定了图形的size
image.onload = drawImageActualSize; // 图片载入成功后画图

// Load an image of intrinsic size 300x227 in CSS pixels
image.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg';

function drawImageActualSize() {
// Use the intrinsic size of image in CSS pixels for the canvas element
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;

// Will draw the image as 300x227, ignoring the custom size of 60x45
// given in the constructor
ctx.drawImage(this, 0, 0);

// To use the custom size we'll have to specify the scale parameters
// using the element's width and height properties - lets draw one
// on top in the corner:
ctx.drawImage(this, 0, 0, this.width, this.height);
}

载入测试图片,可以在页面显示图片成功后继续。

设置挡板对象

这个挡板是可以移动的挡板,可以基于上下左右移动,因此简单实现挡板的参数有x,y,image,以及上下左右方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Paddle = function(){
var leftDown = false
var rightDown = false
var upDown = false
var downDown = false
##这里的上下左右方向初始认定为false

var image = readImage('cat.png')##读取猫猫图片

var o = {
image: image,##图片参数
x: 100, ##坐标x
y: 80, ##坐标y
speed: 5, ##代表移动速度
}
return o

}
移动逻辑

如果挡板向上移动,代表y方向的坐标减少,向下移动代表y方向的坐标增大。这里通过Setinterval()函数实现,如果按键为制定按键,将按照制定周期调用函数或表达式。

官方示例里1000代表每一秒更新一次时间。我们为了让挡板移动显示的很平滑,可以把时间弄得很小,所以肉眼上看是在移动。

1
2
3
4
5
6
7
var myVar = setInterval(function(){ myTimer() }, 1000);

function myTimer() {
var d = new Date();
var t = d.toLocaleTimeString();
document.getElementById("demo").innerHTML = t;
}

因为挡板默认的按键为false,一按键后,挡板开始按照指定x,y方向增加or减少移动量。移动后再擦除区域画图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setInterval(function(){
if (paddle.leftDown){
paddle.x -= paddle.speed
}else if(paddle.rightDown){
paddle.x += paddle.speed
}
else if(paddle.upDown){
paddle.y -= paddle.speed
}
else if(paddle.downDown){
paddle.y += paddle.speed
}
context.clearRect(0, 0, canvas.width, canvas.height)
context.drawImage(paddle.image, paddle.x, paddle.y)
}, 1000/30)

增加按键功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
window.addEventListener('keydown', function(event){
log('keydown')
var key = event.key
if (key == 'a'){
paddle.leftDown = true
}else if (key == 'd')
{
paddle.rightDown = true
}
else if (key == 'w')
{
paddle.upDown = true
}
else if (key == 's')
{
paddle.downDown = true
}
})
##
addEventListnener

addEventListnener是一个事件监听器,当对象触发指定事件后,回调函数会被执行。
比如说点击就是个最简单的事件监听器

1
2
3
4
5
document.getElementById("myBtn").addEventListener("click", function()
{
document.getElementById("demo").innerHTML = "Hello World";
});
##当被点击时会执行函数,执行的函数就是在html增加一个“hello world”语句
keyup

当键盘手指离开按键时,keyup事件会被触发,因此逻辑就是手在键盘的键上时,坐标变化,图移动,按键离开,停止移动,保持静止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
window.addEventListener('keyup', function(event){
log('keyup')
var key = event.key
if (key == 'a'){
paddle.leftDown = false
}else if (key == 'd')
{
paddle.rightDown = false
}
else if (key == 'w')
{
paddle.upDown = false
}
else if (key == 's')
{
paddle.downDown = false
}
})
keydown
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
window.addEventListener('keydown', function(event){
log('keydown')
var key = event.key
if (key == 'a'){
paddle.leftDown = true
}else if (key == 'd')
{
paddle.rightDown = true
}
else if (key == 'w')
{
paddle.upDown = true
}
else if (key == 's')
{
paddle.downDown = true
}
})

开始优化

使用方向

首先让代码简洁易懂,这里的左右上下的移动可以增加到paddle对象里,使用上下左右,而不是加减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
o.moveLeft = function() {
paddle.x -= paddle.speed
}

o.moveRight = function(){
paddle.x += paddle.speed
}

o.moveDown = function() {
paddle.y += paddle.speed
}

o.moveUp = function(){
paddle.y -= paddle.speed
}

那么setInterval也相应修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setInterval(function(){
//update x and y
if (paddle.leftDown){
paddle.moveLeft()
}else if(paddle.rightDown){
paddle.moveRight()
}
else if(paddle.upDown){
paddle.moveUp()
}
else if(paddle.downDown){
paddle.moveDown()
}
context.clearRect(0, 0, canvas.width, canvas.height)
context.drawImage(paddle.image, paddle.x, paddle.y)
}, 1000/30)
封装游戏

设定一个新的函数来封装游戏的定时器,画布等等,并且简化setinterval函数,只留有更新参数以及画图功能在main,因为setinterval其实不太好完全封装,这里选择了部分打包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var GuobaGame = function() {
var g = {}##设置g对象
var canvas = document.querySelector('#id-canvas')
var context = canvas.getContext('2d')
g.canvas = canvas
g.context = context

setInterval(function(){
//update
g.update()
//clear
context.clearRect(0, 0, canvas.width, canvas.height)
//draw

g.draw()
}, 1000 / 30)

return g
}

在main函数里使用update以及draw函数来更新参数,这样就不需要setinterval了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
game.update = function(){
if (paddle.leftDown){
paddle.moveLeft()
}else if(paddle.rightDown){
paddle.moveRight()
}
else if(paddle.upDown){
paddle.moveUp()
}
else if(paddle.downDown){
paddle.moveDown()
}
}

game.draw = function(){
game.context.drawImage(paddle.image, paddle.x, paddle.y)
}
封装事件

但是目前代码还是很混乱,因为事件的部分没有切割,后续十几个事件的话会更加麻烦。

因此我们可以重构一下,我们使用registerAction这个函数来调用所有事件,它是一个回调函数,可以返回所有事件的key键值,也就是对于所有键盘键获取。

我们可以在guobagame里来获取所有action的参数,设定对象g来保存

1
2
3
4
5
6
7
var g = {
actions: {},
keydowns: {},
}
g.registerAction = function(key, callback) {
g.actions[key] = callback
}

对于事件监听可以修改为

1
2
3
4
5
6
window.addEventListener('keydown', function(event){
g.keydowns[event.key] = true
})
window.addEventListener('keyup', function(event){
g.keydowns[event.key] = false
})
定时器

这里的定时器新增一个action函数保存所有关于actions的值,如果按键被按下时,就会调用注册的action

Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。

例子比如

1
2
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']

因此这个按键actions就被返回了

1
2
3
4
5
6
7
8
var actions = Object.keys(g.actions)
for (var i = 0; i < actions.length; i++) {
var key = actions[i]
if(g.keydowns[key]) {
// 如果按键被按下, 调用注册的 action
g.actions[key]()
}
}
对于限制窗口

现在移动paddle容易穿模,因此我们可以设定paddle的移动不可以超过窗口高度宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
game.registerAction('a', function(){
if(paddle.x > 0){
paddle.moveLeft()
}
})

game.registerAction('d', function(){
if(paddle.x + paddle.width * 2 - 10 < 500){
paddle.moveRight()
}
})

game.registerAction('w', function(){
if(paddle.y > 0){
paddle.moveUp()
}
})

game.registerAction('s', function(){
if(paddle.y + paddle.height - 80 < 500){
paddle.moveDown()
}
})

经过一系列优化后,我们的代码就看起来很清爽了

增加球类

对于ball类,我们可以复制粘贴paddle即可,然后删除我们用不到的参数,比如上下左右的操作,然后新增碰撞功能。对于碰撞的逻辑是这样的,球的x,y坐标要小于paddle的x,y坐标,对于x要加上paddle的宽度,对于Y要加上球的高度

1
2
3
4
5
6
7
o.collide = function(ball){
if(ball.y + ball.image.height > o.y){
if(ball.x > o.x && ball.x < o.x + o.image.width){
log('相撞')
return true
}
}
增加发射函数

如果小球被发射才会碰撞,目前我们先设定发射为真,让小球一直碰撞。

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
image: image,
x: 220,
y: 80,
speedX: 10,
speedY: 10,
fired: false,
}

o.fired = function(){
o.fired = true
}

并且让小球增加两个速度,一个x,一个y方向,当小球碰撞后,speedy会有相反速度。

1
2
o.x += o.speedX
o.y += o.speedY

初步完成

第一天的任务就可以完成了
因为不太明白js,但是发现js内置了很多应用以及窗口调用函数,的确开发起来很方便。

另附上目前实现的打砖块。素材来源于iconfont。以及附上全代码。

paddle

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>game 1</title>
<style media="screen">
canvas{
border: 5px black solid;
}
</style>
</head>

<body>
<canvas id="id-canvas" width="500" height="500"></canvas>
<script>
var readImage = function(imgpath){
var img = new Image()
img.src = imgpath
return img
}


var Paddle = function(){
var image = readImage('paddle.png')

var o = {
image: image,
x: 100,
y: 280,
height: 182,
width: 98,
speed: 8,
}

o.moveLeft = function() {
o.x -= o.speed
}

o.moveRight = function(){
o.x += o.speed
}

o.moveDown = function() {
o.y += o.speed
}

o.moveUp = function(){
o.y -= o.speed
}

o.collide = function(ball){
if(ball.y + ball.image.height > o.y){
if(ball.x > o.x && ball.x < o.x + o.image.width){
log('相撞')
return true
}
}
return false
}


return o

}

var Ball = function(){
var image = readImage('ball.png')

var o = {
image: image,
x: 220,
y: 80,
speedX: 10,
speedY: 10,
fired: false,
}

o.fired = function(){
o.fired = true
}

o.move = function(){
if(o.fired){
//log
log('move')
if(o.x < 0 || o.x + 32 > 500){
o.speedX = -o.speedX
}

if(o.y < 0 || o.y + 32 > 500){
o.speedY = -o.speedY
}

o.x += o.speedX
o.y += o.speedY

}
}


return o

}

var Block = function(){
var image = readImage('brick.png')

var o = {
image: image,
x: 220,
y: 80,
w: 111,
h: 32,
alive: true,
}

o.kill = function(){
o.alive = false
}

o.collide = function(ball){
if(ball.y + ball.image.height > o.y){
if(ball.x > o.x && ball.x < o.x + o.image.width){
log('相撞')
return true
}
}
return false
}

return o

}

var GuobaGame = function() {
var g = {
actions: {},
keydowns: {},
}
var canvas = document.querySelector('#id-canvas')
var context = canvas.getContext('2d')
g.canvas = canvas
g.context = context

g.drawImage = function(Image){
g.context.drawImage(Image.image, Image.x, Image.y)
}
// events
window.addEventListener('keydown', function(event){
g.keydowns[event.key] = true
})
window.addEventListener('keyup', function(event){
g.keydowns[event.key] = false
})

//
g.registerAction = function(key, callback) {
g.actions[key] = callback
}
// timer

setInterval(function(){
//update
var actions = Object.keys(g.actions)
for (var i = 0; i < actions.length; i++) {
var key = actions[i]
if(g.keydowns[key]) {
// 如果按键被按下, 调用注册的 action
g.actions[key]()
}
}
// update
g.update()
// clear
context.clearRect(0, 0, canvas.width, canvas.height)
// draw
g.draw()
}, 1000/30)


return g
}
var log = console.log.bind(console)

var __main = function(){
var game = GuobaGame()


var paddle = Paddle()
var ball = Ball()
var block = Block()

game.registerAction('a', function(){
if(paddle.x > 0){
paddle.moveLeft()
}
})

game.registerAction('d', function(){
if(paddle.x + paddle.width * 2 - 10 < 500){
paddle.moveRight()
}
})

game.registerAction('w', function(){
if(paddle.y > 0){
paddle.moveUp()
}
})

game.registerAction('s', function(){
if(paddle.y + paddle.height - 80 < 500){
paddle.moveDown()
}
})

game.registerAction('f', function(){
ball.fired()
})

game.update = function(){
ball.move()
//判断相撞

if(paddle.collide(ball)){
ball.speedY *= -1
}
}

game.draw = function(){
game.drawImage(paddle)
game.drawImage(ball)
if(block.alive){
game.drawImage(block)
}
}

}
//draw
__main()


</script>
</body>
</html>