用Canvas寫消除泡泡游戲!B瞪稀倚评!

這幾天剛接觸了canvas,寫完一個(gè)畫板游戲后馏予,頓時(shí)感覺(jué)太這個(gè)項(xiàng)目太簡(jiǎn)單了天梧,畢竟在網(wǎng)上看見了那么多的canvas項(xiàng)目,所以一通亂找吗蚌,就找到了大佬寫的桌球游戲腿倚,地址

看完以后纯出,嗯蚯妇。。暂筝。大部分看懂了箩言,但關(guān)鍵的碰撞檢測(cè)這塊,真心一時(shí)半會(huì)有點(diǎn)沒(méi)明白焕襟。所以陨收,打算做個(gè)簡(jiǎn)單點(diǎn)的,不需要計(jì)算力度和方向鸵赖。

1. 頁(yè)面組成

整個(gè)游戲分成游戲頁(yè)面和結(jié)束頁(yè)面
結(jié)束頁(yè)就非常簡(jiǎn)單了务漩,只顯示了耗時(shí)和積分,還有一個(gè)再玩一次的按鈕它褪,所以這里主要介紹游戲頁(yè)面饵骨。

游戲地址

可以將游戲頁(yè)面分成三個(gè)類,一個(gè)是泡泡球茫打,一個(gè)是輔助線居触,一個(gè)是炮筒。

2.泡泡球

2.1泡泡球?qū)傩?/h3>

泡泡球的屬性老赤,有距離左邊的長(zhǎng)度x轮洋,距離上部的長(zhǎng)度y,以及自身顏色,不過(guò)對(duì)于子彈泡泡球抬旺,另外還有發(fā)射過(guò)程中的水平速度和垂直速度

$.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. 渲染
泡泡球的渲染方法是需要一直調(diào)用的弊予,所以直接把渲染寫在類的原型上,方便繼承开财。

$.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 圖像源荣堰,這里是通過(guò)img標(biāo)簽傳入圖片,需要等待所有泡泡球圖片加載完以后開始繪制竭翠,才不會(huì)出現(xiàn)錯(cuò)誤振坚。

2. 子彈球的run方法
對(duì)于子彈球,發(fā)射后一直都在跑動(dòng)斋扰,直到碰撞到中間的泡泡球渡八,才會(huì)停止。所以子彈球的run方法和渲染方法需要在碰撞之前一直調(diào)用传货,并且子彈球的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輔助線屬性

輔助線有起點(diǎn)和終點(diǎn)问裕,并且它在未觸發(fā)時(shí)是隱藏狀態(tài)

$.Dotline = function(x0, y0, x1, y1){
    this.x0 = x0
    this.y0 = y0
    this.x1 = x1
    this.y1 = y1
    this.display = false
}

3.2輔助線方法

輔助線僅有一個(gè)渲染方法逮壁,非常簡(jiǎn)單

$.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炮筒屬性

炮筒的起點(diǎn)一直是固定的,唯一不固定的是它旋轉(zhuǎn)的角度

$.Muzzle = function(x, y, angle) {
    this.x = x
    this.y = y
    this.angle = angle
}

3.4炮筒方法

炮筒會(huì)隨著位置的不一樣粮宛,旋轉(zhuǎn)不同的角度窥淆,而旋轉(zhuǎn)畫布是以畫布的左上角為中心,進(jìn)行旋轉(zhuǎn)巍杈,所以需要對(duì)旋轉(zhuǎn)參照點(diǎn)進(jìn)行位移忧饭。
這里的xscale是頁(yè)面寬度和背景圖片的比例,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.初始化生成對(duì)象

頁(yè)面初次加載時(shí)词裤,首先聲明泡泡球、炮筒和輔助線對(duì)象鳖宾,對(duì)于子彈球吼砂,只需要生成一個(gè)處于畫布中間,畫布底部的小球
對(duì)于中間的泡泡球鼎文,需要使用遍歷方法渔肩,讓小球生成5行,每行小于一個(gè)已知的球數(shù)漂问。

$.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.鼠標(biāo)/手指動(dòng)作

5.1 按下和移動(dòng)

鼠標(biāo)/手指按下后計(jì)算該位置赖瞒,然后產(chǎn)生輔助虛線和角度,修改輔助線和炮筒位置和方向。
對(duì)于手touch事件蚤假,取值與電腦有所區(qū)別栏饮。
移動(dòng)事件與按下類似

$.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 釋放鼠標(biāo)/手指

這里的touch事件的取值又不一樣,需要注意磷仰。
當(dāng)取得釋放點(diǎn)的坐標(biāo)后袍嬉,計(jì)算出子彈球與坐標(biāo)的角度,然后得到每次更新畫板后的水平速度和垂直速度

$.up = function(evt){
    var e
    if (document.body.ontouchstart !== undefined) {
        e = evt.changedTouches[0]
    }else {
        e = evt || window.event
    }
    $.dotline.display = false
    $.moving = true

    //主球和到達(dá)點(diǎn)形成的三角形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邊上的角度和運(yùn)動(dòng)速率
    $.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.頁(yè)面渲染

通過(guò)requestAnimFrame重復(fù)渲染畫板,并且每次渲染之前伺通,都需要清除之前繪制的圖案箍土,清除后,再重新繪制畫板內(nèi)的內(nèi)容罐监。這樣吴藻,就能繪制出頁(yè)面的動(dòng)態(tài)改變。

$.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)
    }
}

當(dāng)子彈球的數(shù)組小于1時(shí)弓柱,就重新生成一個(gè)子彈球
當(dāng)子彈球開始moving后沟堡,執(zhí)行碰撞方法bumpballs,當(dāng)泡泡球開始清除矢空,執(zhí)行清除方法航罗,清除完成后執(zhí)行增加泡泡球方法。

7.碰撞

通過(guò)for循環(huán)屁药,遍歷所有泡泡球粥血,計(jì)算正在移動(dòng)的子彈球和所有泡泡球的球心距離,如果最小距離小于兩個(gè)半徑之和酿箭,則說(shuō)明發(fā)生了碰撞复亏。
此時(shí),讓畫板停止繪制七问,并將子彈球加入到泡泡球數(shù)組中蜓耻,并在子彈球數(shù)組中刪除該子彈球茫舶,然后讓畫板繼續(xù)繪制并跳出循環(huán)遍歷械巡。

$.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
        }
    }

    //主球停止?jié)L動(dòng)后,擺放正確位置饶氏,并解除清除方法的鎖定狀態(tài)
    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. 清除

開始清除子彈球和附近相鄰球時(shí)古程,對(duì)所有與子彈球相同顏色的小球進(jìn)行遍歷,先計(jì)算子彈球和其中一個(gè)球是否相鄰喊崖,再判斷這個(gè)球與剩余其它同色球是否相鄰挣磨,若都相鄰,就將它們都改為相同屬性荤懂,然后將這個(gè)球的信息存儲(chǔ)茁裙,下個(gè)循環(huán)以它為中心點(diǎn)判斷。
當(dāng)循環(huán)結(jié)束后节仿,將泡泡球數(shù)組內(nèi)相同屬性的球刪除

$.meltballs = function() {
    var arrColor = [], lastball = $.balls[ $.balls.length - 1 ]
    $.meltpoint = lastball

    //判斷相同顏色的球是否與子彈球相鄰個(gè)數(shù)超過(guò)2個(gè)
    $.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
                }
            }
        }
    }

    //得到與子彈球相鄰超過(guò)2個(gè)的同色球并清理
    $.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. 游戲結(jié)束

獲取所有泡泡球距離頂部的坐標(biāo)晤锥,取得最高那個(gè),如果最高值小于一定數(shù)值,那就停止畫板重繪矾瘾,并讓結(jié)束頁(yè)顯示女轿,得到游戲時(shí)間和分?jǐn)?shù)。

$.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
    }
}

其實(shí)這個(gè)小游戲還有一些bug壕翩,實(shí)在能力有限沒(méi)精力實(shí)現(xiàn)了蛉迹,有什么建議還請(qǐng)大家多多指正。
源碼地址:https://github.com/gao182/canvas-test/blob/master/remove-bubble/index.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末放妈,一起剝皮案震驚了整個(gè)濱河市婿禽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌大猛,老刑警劉巖扭倾,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異挽绩,居然都是意外死亡膛壹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門唉堪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)模聋,“玉大人,你說(shuō)我怎么就攤上這事唠亚×捶剑” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵灶搜,是天一觀的道長(zhǎng)祟蚀。 經(jīng)常有香客問(wèn)我,道長(zhǎng)割卖,這世上最難降的妖魔是什么前酿? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鹏溯,結(jié)果婚禮上罢维,老公的妹妹穿的比我還像新娘。我一直安慰自己丙挽,他們只是感情好肺孵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著颜阐,像睡著了一般平窘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞬浓,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天初婆,我揣著相機(jī)與錄音,去河邊找鬼。 笑死磅叛,一個(gè)胖子當(dāng)著我的面吹牛屑咳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弊琴,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼兆龙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了敲董?” 一聲冷哼從身側(cè)響起紫皇,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腋寨,沒(méi)想到半個(gè)月后聪铺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萄窜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年铃剔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片查刻。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡键兜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出穗泵,到底是詐尸還是另有隱情普气,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布佃延,位于F島的核電站现诀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏苇侵。R本人自食惡果不足惜赶盔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一企锌、第九天 我趴在偏房一處隱蔽的房頂上張望榆浓。 院中可真熱鬧,春花似錦撕攒、人聲如沸陡鹃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)萍鲸。三九已至,卻和暖如春擦俐,著一層夾襖步出監(jiān)牢的瞬間脊阴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嘿期,地道東北人品擎。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像备徐,于是被迫代替她去往敵國(guó)和親萄传。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容