前言
最近看到了萧井陌的几个游戏的源码,好奇也跟着打了打,真的是很有收获。
最后会实现一个打方块的游戏,完全由原生js编写,当然,用了canvas。接下来会连续发几篇文章一步步建立并完善我的小游戏,并记录萧大比较酷的观点以及我自己的一些想法。
游戏效果
只是一个比较基础的版本,只有球和挡板,障碍物和界面效果记录数据什么的都没有加,以后会逐渐完善。
完整代码
先给出完整的代码吧,因为需要说明的地方不太多,而且代码量也很小。
图片的话自己随手截的,如果想测试的话请自行修改相关的参数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>game 1</title>
</head>
<style>
canvas {
border: 1px solid black;
}
</style>
<body>
<canvas id="id-canvas" width="1000" height="900"></canvas>
<script>
var log = console.log.bind(console)
var imageFromPath = function(path) {
var img = new Image()
img.src = path
return img
}
var Paddle = function() {
var image = imageFromPath('paddle.png')
var o = {
image: image,
x: 400,
y: 750,
speed: 25,
}
o.moveLeft = function() {
this.x -= this.speed
}
o.moveRight = function() {
this.x += this.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) {
return true
}
}
return false
}
return o
}
var Ball = function() {
var image = imageFromPath('ball.png')
var o = {
image: image,
x: 200,
y: 400,
speedX: 15,
speedY: 15,
fired: false,
}
o.move = function () {
if (o.fired) {
if (o.x < 0 || o.x > 1000) {
o.speedX = -o.speedX
}
if (o.y < 0 || o.y > 900) {
o.speedY = -o.speedY
}
// move
o.x += o.speedX
o.y += o.speedY
}
}
o.fire = function() {
o.fired = true
}
return o
}
var GuaGame = function() {
var g = {
actions: {},
keydowns: {},
}
var canvas = document.querySelector('#id-canvas')
var context = canvas.getContext('2d')
g.canvas = canvas
g.context = context
// draw
g.drawImage = function(guaImage) {
this.context.drawImage(guaImage.image, guaImage.x, guaImage.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(){
// events
var actions = Object.keys(g.actions)
for (let i = 0; i < actions.length; i++) {
var key = actions[i]
if (g.keydowns[key]) {
// 如果按键被按下,调用注册的actions
g.actions[key]()
}
}
// update
g.update()
// clear
context.clearRect(0, 0, canvas.width, canvas.height)
// draw
g.draw()
}, 1000/30)
return g
}
var __main = function() {
var game = GuaGame()
var paddle = Paddle()
var ball = Ball()
// events
game.registerAction('ArrowLeft', function(){
paddle.moveLeft()
})
game.registerAction('ArrowRight', function(){
paddle.moveRight()
})
game.registerAction('f', function(){
ball.fire()
})
game.update = function() {
ball.move()
// 判断相撞
if (paddle.collide(ball)) {
ball.speedY *= -1
}
}
game.draw = function() {
// draw
game.drawImage(paddle)
game.drawImage(ball)
}
}
__main()
</script>
</body>
</html>
说明
目前的程序主要是由三部分构成吧。
首先是paddle挡板部分,有横纵坐标以及速度等一些属性,向左向右移动的函数和碰撞检测,那个碰撞检测写的不是很好,经常出bug,后期再完善一下吧。
第二就是ball球那部分,它的速度分为X和Y两个方向,还有一个fired标记,意味着开始游戏时按“f”球才会移动。球的move移动函数包含了对画板四周的检测。
第三部分就是那个GuaGame,也是我眼中整个程序最精妙的地方。
// draw
g.drawImage = function(guaImage) {
this.context.drawImage(guaImage.image, guaImage.x, guaImage.y)
}
它提供了一个接口,可以方便的显示图片。
var g = {
actions: {},
keydowns: {},
}
g.registerAction = function(key, callback) {
g.actions[key] = callback
}
这大概就是最令我感到眼前一亮的地方了,registerAction是一个注册事件的函数,它接受按键的键值和一个回调函数,作用就是把键值和它应该执行的函数对应并储存起来。
// events
window.addEventListener('keydown', function(event){
g.keydowns[event.key] = true
})
window.addEventListener('keyup', function(event){
g.keydowns[event.key] = false
})
然后是对按键事件的监听,通过keydown和keyup改变键值对应事件的状态,如果为true就执行注册过的回调函数,反之则不执行。
// timer
setInterval(function(){
// events
var actions = Object.keys(g.actions)
for (let i = 0; i < actions.length; i++) {
var key = actions[i]
if (g.keydowns[key]) {
// 如果按键被按下,调用注册的actions
g.actions[key]()
}
}
// update
g.update()
// clear
context.clearRect(0, 0, canvas.width, canvas.height)
// draw
g.draw()
}, 1000/30)
这个定时器内部判断按键的状态,再调用按键注册的回调函数。clear()清屏,update()和draw()均在GuaGame外部实现。
var __main = function() {
var game = GuaGame()
var paddle = Paddle()
var ball = Ball()
// events
game.registerAction('ArrowLeft', function(){
paddle.moveLeft()
})
game.registerAction('ArrowRight', function(){
paddle.moveRight()
})
game.registerAction('f', function(){
ball.fire()
})
game.update = function() {
ball.move()
// 判断相撞
if (paddle.collide(ball)) {
ball.speedY *= -1
}
}
game.draw = function() {
// draw
game.drawImage(paddle)
game.drawImage(ball)
}
}
__main()函数作为整个程序的入口,先实例化game\paddle\ball,然后注册回调函数,将键值和函数一一对应起来储存在game的内部,最后实现update和draw函数。
后记
虽然程序看起来简陋界面也不美观,但学到了一种处理事件的很巧妙的方法,以后在这个框架上完善会变得简单,反而是这个框架的建立很费时间,不过这也没什么好奇怪的,毕竟从0到1可比从1到100难多了。