用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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容