这几天刚接触了canvas,写完一个画板游戏后,顿时感觉太这个项目太简单了,毕竟在网上看见了那么多的canvas项目,所以一通乱找,就找到了大佬写的桌球游戏,地址
看完以后,嗯。。。大部分看懂了,但关键的碰撞检测这块,真心一时半会有点没明白。所以,打算做个简单点的,不需要计算力度和方向。
1. 页面组成
整个游戏分成游戏页面和结束页面
结束页就非常简单了,只显示了耗时和积分,还有一个再玩一次的按钮,所以这里主要介绍游戏页面。
游戏地址
可以将游戏页面分成三个类,一个是泡泡球,一个是辅助线,一个是炮筒。
2.泡泡球
2.1泡泡球属性
泡泡球的属性,有距离左边的长度x,距离上部的长度y,以及自身颜色,不过对于子弹泡泡球,另外还有发射过程中的水平速度和垂直速度
$.Ball = function(x, y) {
this.sx = 0
this.sy = 0
this.x = x
this.y = y
this.color = Math.floor(Math.random()*5) || 5
}
2.2泡泡球方法
1. 渲染
泡泡球的渲染方法是需要一直调用的,所以直接把渲染写在类的原型上,方便继承。
$.Ball.prototype.render = function() {
var b
switch(this.color) {
case 0 :
b = document.getElementById("bs0")
break;
case 1 :
b = document.getElementById("bs1")
break;
case 2 :
b = document.getElementById("bs2")
break;
case 3 :
b = document.getElementById("bs3")
break;
case 4 :
b = document.getElementById("bs4")
break;
default:
b = document.getElementById("bs5")
}
if(b.complete) {
$.ctx.drawImage(b , this.x-$.radius , this.y-$.radius , 2*$.radius , 2*$.radius);
}
else {
b.onload = function(){
$.ctx.drawImage(b , this.x-$.radius , this.y-$.radius , 2*$.radius , 2*$.radius);
}
}
}
这里要注意到的是,canvas的drawImage方法允许任何的 canvas 图像源,这里是通过img标签传入图片,需要等待所有泡泡球图片加载完以后开始绘制,才不会出现错误。
2. 子弹球的run方法
对于子弹球,发射后一直都在跑动,直到碰撞到中间的泡泡球,才会停止。所以子弹球的run方法和渲染方法需要在碰撞之前一直调用,并且子弹球的x,y是一直改变的。
$.Ball.prototype.run = function() {
this.x += this.sx
this.y += this.sy
this.render()
if (this.x < $.radius || this.x > $.cas.width - $.radius) {
this.sx = - this.sx
}
else if(this.y < $.radius || this.y > $.cas.height - $.radius){
$.bullets.pop()
$.moving = false
}
}
3.辅助线和炮筒
3.1辅助线属性
辅助线有起点和终点,并且它在未触发时是隐藏状态
$.Dotline = function(x0, y0, x1, y1){
this.x0 = x0
this.y0 = y0
this.x1 = x1
this.y1 = y1
this.display = false
}
3.2辅助线方法
辅助线仅有一个渲染方法,非常简单
$.Dotline.prototype.render = function () {
$.ctx.save()
$.ctx.beginPath()
$.ctx.setLineDash([3, 10])
$.ctx.moveTo(this.x0, this.y0)
$.ctx.lineTo(this.x1, this.y1)
$.ctx.lineWidth = 3;
$.ctx.strokeStyle = "white"
$.ctx.lineCap = "round";
$.ctx.stroke()
$.ctx.closePath()
$.ctx.restore()
}
3.3炮筒属性
炮筒的起点一直是固定的,唯一不固定的是它旋转的角度
$.Muzzle = function(x, y, angle) {
this.x = x
this.y = y
this.angle = angle
}
3.4炮筒方法
炮筒会随着位置的不一样,旋转不同的角度,而旋转画布是以画布的左上角为中心,进行旋转,所以需要对旋转参照点进行位移。
这里的xscale是页面宽度和背景图片的比例,128是炮筒的宽度,
$.Muzzle.prototype.render = function() {
var b = document.getElementById("muzzle"),
xscale = maxWidth/720
$.ctx.save()
$.ctx.translate(this.x, this.y)
$.ctx.rotate(this.angle)
if(b.complete) {
$.ctx.drawImage(b , -xscale*128/2 , -xscale*128 * 0.74*1.2, xscale*128, xscale*128 * 0.74)
}
else {
b.onload = function(){
$.ctx.drawImage(b , -xscale*128/2 , -xscale*128 * 0.74*1.2, xscale*128, xscale*128 * 0.74)
}
}
$.ctx.restore()
}
4.初始化生成对象
页面初次加载时,首先声明泡泡球、炮筒和辅助线对象,对于子弹球,只需要生成一个处于画布中间,画布底部的小球
对于中间的泡泡球,需要使用遍历方法,让小球生成5行,每行小于一个已知的球数。
$.bullets.push(new $.Ball( $.cas.width/2, $.cas.height - (maxWidth*0.44/2)))
$.muzzle = new $.Muzzle($.cas.width/2, $.cas.height - (maxWidth*0.44/2), 0)
$.dotline = new $.Dotline($.cas.width/2, $.cas.height - 166, $.cas.width/2, $.cas.height - 166)
for (var i = 0; i < 5; i++) {
for (var j = 0; j < $.rownum ; j++) {
$.balls.push(new $.Ball( (j*$.radius*2) + (i%2*$.radius) + $.radius, (i*2*$.radius) - (i*5) +$.radius ))
}
}
5.鼠标/手指动作
5.1 按下和移动
鼠标/手指按下后计算该位置,然后产生辅助虚线和角度,修改辅助线和炮筒位置和方向。
对于手touch事件,取值与电脑有所区别。
移动事件与按下类似
$.down = function(evt){
var e
if (document.body.ontouchstart !== undefined) {
e = evt.touches[0]
}else {
e = evt || window.event
}
$.dotline.display = true
$.dotline.x0 = $.bullets[0].x
$.dotline.y0 = $.bullets[0].y
$.dotline.x1 = e.clientX - $.view.offsetLeft
$.dotline.y1 = e.clientY - $.view.offsetTop - maxWidth/10
$.muzzle.angle = -Math.atan(($.dotline.x1 - $.bullets[0].x)/($.dotline.y1 - $.bullets[0].y))
window.addEventListener('mousemove', $.move)
window.addEventListener( 'mouseup', $.up )
}
$.move = function(evt) {
var e
if (document.body.ontouchstart !== undefined) {
event.preventDefault()
e = evt.touches[0]
}else {
e = evt || window.event
}
$.dotline.x1 = e.clientX - $.view.offsetLeft
$.dotline.y1 = e.clientY - $.view.offsetTop - maxWidth/10
$.muzzle.angle = -Math.atan(($.dotline.x1 - $.bullets[0].x)/($.dotline.y1 - $.bullets[0].y))
}
5.2 释放鼠标/手指
这里的touch事件的取值又不一样,需要注意。
当取得释放点的坐标后,计算出子弹球与坐标的角度,然后得到每次更新画板后的水平速度和垂直速度
$.up = function(evt){
var e
if (document.body.ontouchstart !== undefined) {
e = evt.changedTouches[0]
}else {
e = evt || window.event
}
$.dotline.display = false
$.moving = true
//主球和到达点形成的三角形a,b边和角度
var a = e.clientX - $.view.offsetLeft - $.bullets[0].x
b = e.clientY - $.view.offsetTop - maxWidth/10 - $.bullets[0].y
angle = Math.atan(a/b)
$.muzzle.angle = -angle
//c边上的角度和运动速率
$.bullets[0].sx = a > 0 ? 10 * Math.abs(Math.sin(angle)) : -10 * Math.abs(Math.sin(angle))
$.bullets[0].sy = b > 0 ? 10 * Math.abs(Math.cos(angle)) : -10 * Math.abs(Math.cos(angle))
window.removeEventListener('mousemove', $.move)
window.removeEventListener('mouseup', $.up)
}
6.页面渲染
通过requestAnimFrame重复渲染画板,并且每次渲染之前,都需要清除之前绘制的图案,清除后,再重新绘制画板内的内容。这样,就能绘制出页面的动态改变。
$.redraw = function() {
$.ctx.clearRect(0, 0, $.cas.width, $.cas.height)
var t = Date.now()
$.scroe = $.scoreballs.length * 50
if ($.dotline.display) { $.dotline.render()}
$.muzzle.render()
for (var i = 0; i < $.balls.length; i++) {
$.balls[i].render()
}
if ($.bullets.length < 1) {
$.bullets.push(new $.Ball( $.cas.width/2, $.cas.height - (maxWidth*0.44/2) ))
}
$.bullets[0].run()
if ($.moving) { $.bumpballs() }
if ($.melting) {$.meltballs();$.addbulls()}
if ($.scoreballs.length > 2 && t - $.clearBull < 500 ) {
for (var i = 0; i < $.scoreballs.length; i++) {
$.scoreballs[i].renderscore($.scoreballs[i].x,$.scoreballs[i].y)
}
}else {
$.scoreballs = []
}
if (!$.Stop) {
requestAnimFrame($.redraw)
}
}
当子弹球的数组小于1时,就重新生成一个子弹球
当子弹球开始moving后,执行碰撞方法bumpballs,当泡泡球开始清除,执行清除方法,清除完成后执行增加泡泡球方法。
7.碰撞
通过for循环,遍历所有泡泡球,计算正在移动的子弹球和所有泡泡球的球心距离,如果最小距离小于两个半径之和,则说明发生了碰撞。
此时,让画板停止绘制,并将子弹球加入到泡泡球数组中,并在子弹球数组中删除该子弹球,然后让画板继续绘制并跳出循环遍历。
$.bumpballs = function() {
for (var i = 0; i < $.balls.length; i++) {
var b1 = $.balls[i], bt = $.bullets[0]
var rc = Math.sqrt(Math.pow(b1.x - bt.x , 2) + Math.pow(b1.y - bt.y , 2))
if (Math.floor(rc) <= $.radius*2) {
$.Stop = true
$.balls.push(bt)
$.direction = b1
$.moving = false
$.bullets.pop()
$.Stop = false
break
}
}
//主球停止滚动后,摆放正确位置,并解除清除方法的锁定状态
if (!$.moving) {
var lastball = $.balls[ $.balls.length - 1 ]
var y = Math.round((lastball.y-$.radius)/(2*$.radius - 5))
//判断子弹球摆放的地方并摆放
if (lastball.x - $.direction.x > 20 ) {
if (lastball.y - $.direction.y <= 20 && lastball.y - $.direction.y >= -20) {
lastball.y = $.direction.y
lastball.x = $.direction.x + 2*$.radius
}
else if ( lastball.y - $.direction.y < -20) {
lastball.y = (y*2*$.radius) - (y*5) +$.radius
lastball.x = $.direction.x + $.radius
}
else if (lastball.y - $.direction.y > 20) {
lastball.y = (y*2*$.radius) - (y*5) + $.radius
lastball.x = $.direction.x + $.radius
}
}
else if (lastball.x - $.direction.x < -20) {
if(lastball.y - $.direction.y <= 20 && lastball.y - $.direction.y >= -20) {
lastball.y = $.direction.y
lastball.x = $.direction.x - 2*$.radius
}
else if (lastball.y - $.direction.y > 20) {
lastball.y = (y*2*$.radius) - (y*5) + $.radius
lastball.x = $.direction.x - $.radius
}
else if (lastball.y - $.direction.y < -20) {
lastball.y = (y*2*$.radius) - (y*5) +$.radius
lastball.x = $.direction.x - $.radius
}
}
else if (lastball.x - $.direction.x <= 20 && lastball.x - $.direction.x >= -20) {
if (lastball.x - $.direction.x > 0 && lastball.y - $.direction.y > 20 ) {
lastball.y = (y*2*$.radius) - (y*5) + $.radius
lastball.x = $.direction.x + $.radius
}
else if (lastball.x - $.direction.x > 0 && lastball.y - $.direction.y <= 20 ) {
lastball.y = (y*2*$.radius) - (y*5) +$.radius
lastball.x = $.direction.x + $.radius
}
else if (lastball.x - $.direction.x <= 0 && lastball.y - $.direction.y >= -20 ) {
lastball.y = (y*2*$.radius) - (y*5) +$.radius
lastball.x = $.direction.x - $.radius
}
else if (lastball.x - $.direction.x <= 0 && lastball.y - $.direction.y <= 20 ) {
lastball.y = (y*2*$.radius) - (y*5) +$.radius
lastball.x = $.direction.x - $.radius
}
}
$.melting = true
}
}
在子弹球碰撞后,开始判断它的位置,并将它摆放到正确位置。
8. 清除
开始清除子弹球和附近相邻球时,对所有与子弹球相同颜色的小球进行遍历,先计算子弹球和其中一个球是否相邻,再判断这个球与剩余其它同色球是否相邻,若都相邻,就将它们都改为相同属性,然后将这个球的信息存储,下个循环以它为中心点判断。
当循环结束后,将泡泡球数组内相同属性的球删除
$.meltballs = function() {
var arrColor = [], lastball = $.balls[ $.balls.length - 1 ]
$.meltpoint = lastball
//判断相同颜色的球是否与子弹球相邻个数超过2个
$.balls._foreach(function(){
if (this.color === lastball.color) {
arrColor.push(this)
}
})
for (var i = arrColor.length - 2; i >= 0; i--) {
for (var j = arrColor.length - 2; j >= 0; j--) {
var b1 = arrColor[i], b2 = arrColor[j]
if (b1 !== b2) {
var rc1 = Math.sqrt(Math.pow(b1.x - $.meltpoint.x , 2) + Math.pow(b1.y - $.meltpoint.y , 2))
var rc3 = Math.sqrt(Math.pow(b1.x - b2.x , 2) + Math.pow(b1.y - b2.y , 2))
if (Math.floor(rc1) <= $.radius*2 && Math.floor(rc3) <= $.radius*2) {
$.balls[$.balls._index(b1)].color = "black"
$.balls[$.balls._index(b2)].color = "black"
lastball.color = "black"
$.score +=1
$.meltpoint = b1
}
}
}
}
//得到与子弹球相邻超过2个的同色球并清理
$.balls._foreach(function(){
if (this.color === "black"){
$.scoreballs.push(this)
}
})
var num = 0
while(num < 3) {
$.balls._foreach(function(){
if (this.color === "black"){
$.balls.splice($.balls._index(this),1)
}
})
num ++
}
$.melting = false
$.clearBull = Date.now()
}
9. 游戏结束
获取所有泡泡球距离顶部的坐标,取得最高那个,如果最高值小于一定数值,那就停止画板重绘,并让结束页显示,得到游戏时间和分数。
$.gameover = function() {
var heightObj = [], maxHeight
$.balls._foreach(function(){
heightObj.push(this.y)
})
maxHeight = heightObj.sort(function(a,b){
return b - a
})[0]
if ($.cas.height - maxHeight < (maxWidth*0.4+$.radius)) {
$.cover.classList.remove('active')
document.getElementsByClassName("score_num")[1].innerText = document.getElementsByClassName("score_num")[0].innerText
document.getElementsByClassName("time")[1].innerText = document.getElementsByClassName("time")[0].innerText
$.Stop = true
}
}
其实这个小游戏还有一些bug,实在能力有限没精力实现了,有什么建议还请大家多多指正。
源码地址:https://github.com/gao182/canvas-test/blob/master/remove-bubble/index.html