這幾天剛接觸了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