用Canvas写消除泡泡游戏!!!

这几天刚接触了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

你可能感兴趣的:(用Canvas写消除泡泡游戏!!!)