VUE+Canvas 實(shí)現(xiàn)桌面彈球消磚塊小游戲的示例代碼
大家都玩過彈球消磚塊游戲,左右鍵控制最底端的一個(gè)小木板平移,接住掉落的小球,將球彈起后消除畫面上方的一堆磚塊。
那么用VUE+Canvas如何來(lái)實(shí)現(xiàn)呢?實(shí)現(xiàn)思路很簡(jiǎn)單,首先來(lái)拆分一下要畫在畫布上的內(nèi)容:
(1)用鍵盤左右按鍵控制平移的木板;
(2)在畫布內(nèi)四處彈跳的小球;
(3)固定在畫面上方,并且被球碰撞后就消失的一堆磚塊。
將上述三種對(duì)象,用requestAnimationFrame()函數(shù)平移運(yùn)動(dòng)起來(lái),再結(jié)合各種碰撞檢查,就可以得到最終的結(jié)果。
先看看最終的效果:

一、左右平移的木板
最底部的木板是最簡(jiǎn)單的一部分,因?yàn)槟景宓膟坐標(biāo)是固定的,我們?cè)O(shè)置木板的初始參數(shù),包括其寬度,高度,平移速度等,然后實(shí)現(xiàn)畫木板的函數(shù):
pannel: {
x: 0,
y: 0,
height: 8,
width: 100,
speed: 8,
dx: 0
},
....
drawPannel() {
this.drawRoundRect(
this.pannel.x,
this.pannel.y,
this.pannel.width,
this.pannel.height,
5
);
},
drawRoundRect(x, y, width, height, radius) { // 畫圓角矩形
this.ctx.beginPath();
this.ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);
this.ctx.lineTo(width - radius + x, y);
this.ctx.arc(
width - radius + x,
radius + y,
radius,
(Math.PI * 3) / 2,
Math.PI * 2
);
this.ctx.lineTo(width + x, height + y - radius);
this.ctx.arc(
width - radius + x,
height - radius + y,
radius,
0,
(Math.PI * 1) / 2
);
this.ctx.lineTo(radius + x, height + y);
this.ctx.arc(
radius + x,
height - radius + y,
radius,
(Math.PI * 1) / 2,
Math.PI
);
this.ctx.fillStyle = "#008b8b";
this.ctx.fill();
this.ctx.closePath();
}
程序初始化的時(shí)候,監(jiān)聽鍵盤的左右方向鍵,來(lái)移動(dòng)木板,通過長(zhǎng)度判斷是否移動(dòng)到了左右邊界使其不能繼續(xù)移出畫面:
document.onkeydown = function(e) {
let key = window.event.keyCode;
if (key === 37) {
// 左鍵
_this.pannel.dx = -_this.pannel.speed;
} else if (key === 39) {
// 右鍵
_this.pannel.dx = _this.pannel.speed;
}
};
document.onkeyup = function(e) {
_this.pannel.dx = 0;
};
....
movePannel() {
this.pannel.x += this.pannel.dx;
if (this.pannel.x > this.clientWidth - this.pannel.width) {
this.pannel.x = this.clientWidth - this.pannel.width;
} else if (this.pannel.x < 0) {
this.pannel.x = 0;
}
},
二、彈跳的小球和碰撞檢測(cè)
小球的運(yùn)動(dòng)和木板類似,只是不僅有dx的偏移,還有dy的偏移。
而且還要有碰撞檢測(cè):
(1)當(dāng)碰撞的是上、右、左墻壁以及木板上的時(shí)候則反彈;
(2)當(dāng)碰撞到是木板以外的下邊界的時(shí)候,則輸?shù)粲螒颍?/p>
(3)當(dāng)碰撞的是磚塊的時(shí)候,被碰的磚塊消失,分?jǐn)?shù)+1,小球反彈。
于是和木板一樣,將小球部分分為畫小球函數(shù)drawBall()和小球運(yùn)動(dòng)函數(shù)moveBall():
drawBall() {
this.ctx.beginPath();
this.ctx.arc(this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
this.ctx.fillStyle = "#008b8b";
this.ctx.fill();
this.ctx.closePath();
},
moveBall() {
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
this.breaksHandle();
this.edgeHandle();
},
breaksHandle() {
// 觸碰磚塊檢測(cè)
this.breaks.forEach(item => {
if (item.show) {
if (
this.ball.x + this.ball.r > item.x &&
this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
this.ball.y + this.ball.r > item.y &&
this.ball.y - this.ball.r < item.y + this.breaksConfig.height
) {
item.show = false;
this.ball.dy *= -1;
this.score ++ ;
if(this.showBreaksCount === 0){
this.gameOver = true;
}
}
}
});
},
edgeHandle() {
// 邊緣檢測(cè)
// 碰到頂部反彈
if (this.ball.y - this.ball.r < 0) {
this.ball.dy = -this.ball.dy;
}
if (
// 碰到左右墻壁
this.ball.x - this.ball.r < 0 ||
this.ball.x + this.ball.r > this.clientWidth
) {
this.ball.dx = -this.ball.dx;
}
if (
this.ball.x >= this.pannel.x &&
this.ball.x <= this.pannel.x + this.pannel.width &&
this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
) {
// 球的x在板子范圍內(nèi)并觸碰到了板子
this.ball.dy *= -1;
} else if (
(this.ball.x < this.pannel.x ||
this.ball.x > this.pannel.x + this.pannel.width) &&
this.ball.y + this.ball.r >= this.clientHeight
) {
// 球碰到了底邊緣了
this.gameOver = true;
this.getCurshBreaks();
}
}
三、磚塊的生成
磚塊的生成也比較簡(jiǎn)單,這里我們初始了一些數(shù)據(jù):
breaksConfig: {
row: 6, // 排數(shù)
height: 25, // 磚塊高度
width: 130, // 磚塊寬度
radius: 5, // 矩形圓角
space: 0, // 間距
colunm: 6 // 列數(shù)
}
根據(jù)這些配置項(xiàng)以及畫布寬度,我們可以計(jì)算出每個(gè)磚塊的橫向間隙是多少:
// 計(jì)算得出磚塊縫隙寬度
this.breaksConfig.space = Math.floor(
(this.clientWidth -
this.breaksConfig.width * this.breaksConfig.colunm) /
(this.breaksConfig.colunm + 1)
);
于是我們可以得到每個(gè)磚塊在畫布中的x,y坐標(biāo)(指的磚塊左上角的坐標(biāo))
for (let i = 0; i < _this.breaksConfig.row; i++) {
for (let j = 0; j < _this.breaksConfig.colunm; j++) {
_this.breaks.push({
x: this.breaksConfig.space * (j + 1) + this.breaksConfig.width * j,
y: 10 * (i + 1) + this.breaksConfig.height * i,
show: true
});
}
}
再加上繪制磚塊的函數(shù):
drawBreaks() {
let _this = this;
_this.breaks.forEach(item => {
if (item.show) {
_this.drawRoundRect(
item.x,
item.y,
_this.breaksConfig.width,
_this.breaksConfig.height,
_this.breaksConfig.radius
);
}
});
}
四、讓上面三個(gè)部分動(dòng)起來(lái)
(function animloop() {
if (!_this.gameOver) {
_this.movePannel();
_this.moveBall();
_this.drawAll();
} else {
_this.drawCrushBreaks();
}
window.requestAnimationFrame(animloop);
})();
....
drawAll() {
this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
this.drawPannel();
this.drawBall();
this.drawScore();
this.drawBreaks();
}
五、游戲結(jié)束后的效果
在最開始的動(dòng)圖里可以看到,游戲結(jié)束后,磚塊粉碎成了若干的小球掉落,這個(gè)其實(shí)和畫單獨(dú)的小球類似,思路就是把剩余的磚塊中心坐標(biāo)處生產(chǎn)若干大小不等,運(yùn)動(dòng)軌跡不等,顏色不等的小球,然后繼續(xù)動(dòng)畫。
getCurshBreaks() {
let _this = this;
this.breaks.forEach(item => {
if (item.show) {
item.show = false;
for (let i = 0; i < 8; i++) { // 每個(gè)磚塊粉碎為8個(gè)小球
this.crushBalls.push({
x: item.x + this.breaksConfig.width / 2,
y: item.y + this.breaksConfig.height / 2,
dx: _this.getRandomArbitrary(-6, 6),
dy: _this.getRandomArbitrary(-6, 6),
r: _this.getRandomArbitrary(1, 4),
color: _this.getRandomColor()
});
}
}
});
},
drawCrushBreaks() {
this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
this.crushBalls.forEach(item => {
this.ctx.beginPath();
this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
this.ctx.fillStyle = item.color;
this.ctx.fill();
this.ctx.closePath();
item.x += item.dx;
item.y += item.dy;
if (
// 碰到左右墻壁
item.x - item.r < 0 ||
item.x + item.r > this.clientWidth
) {
item.dx = -item.dx;
}
if (
// 碰到上下墻壁
item.y - item.r < 0 ||
item.y + item.r > this.clientHeight
) {
item.dy = -item.dy;
}
});
},
以上就是桌面彈球消磚塊小游戲的實(shí)現(xiàn)思路和部分代碼,實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,兩三百行代碼就可以實(shí)現(xiàn)這個(gè)小游戲。在小球的運(yùn)動(dòng)上可以進(jìn)行持續(xù)優(yōu)化,并且也可以增加難度選項(xiàng)操作。
最后附上全部的vue文件代碼,供大家參考學(xué)習(xí):
<template>
<div class="break-ball">
<canvas id="breakBall" width="900" height="600"></canvas>
<div class="container" v-if="gameOver">
<div class="dialog">
<p class="once-again">本輪分?jǐn)?shù):{{score}}分</p>
<p class="once-again">真好玩!</p>
<p class="once-again">再來(lái)一次~~</p>
<el-button class="once-again-btn" @click="init">開始</el-button>
</div>
</div>
</div>
</template>
<script>
const randomColor = [
"#FF95CA",
"#00E3E3",
"#00E3E3",
"#6F00D2",
"#6F00D2",
"#C2C287",
"#ECFFFF",
"#FFDC35",
"#93FF93",
"#d0d0d0"
];
export default {
name: "BreakBall",
data() {
return {
clientWidth: 0,
clientHeight: 0,
ctx: null,
crushBalls: [],
pannel: {
x: 0,
y: 0,
height: 8,
width: 100,
speed: 8,
dx: 0
},
ball: {
x: 0,
y: 0,
r: 8,
dx: -4,
dy: -4
},
score: 0,
gameOver: false,
breaks: [],
breaksConfig: {
row: 6, // 排數(shù)
height: 25, // 磚塊高度
width: 130, // 磚塊寬度
radius: 5, // 矩形圓角
space: 0, // 間距
colunm: 6 // 列數(shù)
}
};
},
mounted() {
let _this = this;
let container = document.getElementById("breakBall");
this.ctx = container.getContext("2d");
this.clientHeight = container.height;
this.clientWidth = container.width;
_this.init();
document.onkeydown = function(e) {
let key = window.event.keyCode;
if (key === 37) {
// 左鍵
_this.pannel.dx = -_this.pannel.speed;
} else if (key === 39) {
// 右鍵
_this.pannel.dx = _this.pannel.speed;
}
};
document.onkeyup = function(e) {
_this.pannel.dx = 0;
};
(function animloop() {
if (!_this.gameOver) {
_this.movePannel();
_this.moveBall();
_this.drawAll();
} else {
_this.drawCrushBreaks();
}
window.requestAnimationFrame(animloop);
})();
},
computed:{
showBreaksCount(){
return this.breaks.filter(item=>{
return item.show;
}).length;
}
},
methods: {
init() {
let _this = this;
_this.gameOver = false;
this.pannel.y = this.clientHeight - this.pannel.height;
this.pannel.x = this.clientWidth / 2 - this.pannel.width / 2;
this.ball.y = this.clientHeight / 2;
this.ball.x = this.clientWidth / 2;
this.score = 0;
this.ball.dx = [-1,1][Math.floor(Math.random() * 2)]*4;
this.ball.dy = [-1,1][Math.floor(Math.random() * 2)]*4;
this.crushBalls = [];
this.breaks = [];
// 計(jì)算得出磚塊縫隙寬度
this.breaksConfig.space = Math.floor(
(this.clientWidth -
this.breaksConfig.width * this.breaksConfig.colunm) /
(this.breaksConfig.colunm + 1)
);
for (let i = 0; i < _this.breaksConfig.row; i++) {
for (let j = 0; j < _this.breaksConfig.colunm; j++) {
_this.breaks.push({
x: this.breaksConfig.space * (j + 1) + this.breaksConfig.width * j,
y: 10 * (i + 1) + this.breaksConfig.height * i,
show: true
});
}
}
},
drawAll() {
this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
this.drawPannel();
this.drawBall();
this.drawScore();
this.drawBreaks();
},
movePannel() {
this.pannel.x += this.pannel.dx;
if (this.pannel.x > this.clientWidth - this.pannel.width) {
this.pannel.x = this.clientWidth - this.pannel.width;
} else if (this.pannel.x < 0) {
this.pannel.x = 0;
}
},
moveBall() {
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
this.breaksHandle();
this.edgeHandle();
},
breaksHandle() {
// 觸碰磚塊檢測(cè)
this.breaks.forEach(item => {
if (item.show) {
if (
this.ball.x + this.ball.r > item.x &&
this.ball.x - this.ball.r < item.x + this.breaksConfig.width &&
this.ball.y + this.ball.r > item.y &&
this.ball.y - this.ball.r < item.y + this.breaksConfig.height
) {
item.show = false;
this.ball.dy *= -1;
this.score ++ ;
if(this.showBreaksCount === 0){
this.gameOver = true;
}
}
}
});
},
edgeHandle() {
// 邊緣檢測(cè)
// 碰到頂部反彈
if (this.ball.y - this.ball.r < 0) {
this.ball.dy = -this.ball.dy;
}
if (
// 碰到左右墻壁
this.ball.x - this.ball.r < 0 ||
this.ball.x + this.ball.r > this.clientWidth
) {
this.ball.dx = -this.ball.dx;
}
if (
this.ball.x >= this.pannel.x &&
this.ball.x <= this.pannel.x + this.pannel.width &&
this.ball.y + this.ball.r >= this.clientHeight - this.pannel.height
) {
// 球的x在板子范圍內(nèi)并觸碰到了板子
this.ball.dy *= -1;
} else if (
(this.ball.x < this.pannel.x ||
this.ball.x > this.pannel.x + this.pannel.width) &&
this.ball.y + this.ball.r >= this.clientHeight
) {
// 球碰到了底邊緣了
this.gameOver = true;
this.getCurshBreaks();
}
},
drawScore(){
this.ctx.beginPath();
this.ctx.font="14px Arial";
this.ctx.fillStyle = "#FFF";
this.ctx.fillText("分?jǐn)?shù):"+this.score,10,this.clientHeight-14);
this.ctx.closePath();
},
drawCrushBreaks() {
this.ctx.clearRect(0, 0, this.clientWidth, this.clientHeight);
this.crushBalls.forEach(item => {
this.ctx.beginPath();
this.ctx.arc(item.x, item.y, item.r, 0, 2 * Math.PI);
this.ctx.fillStyle = item.color;
this.ctx.fill();
this.ctx.closePath();
item.x += item.dx;
item.y += item.dy;
if (
// 碰到左右墻壁
item.x - item.r < 0 ||
item.x + item.r > this.clientWidth
) {
item.dx = -item.dx;
}
if (
// 碰到上下墻壁
item.y - item.r < 0 ||
item.y + item.r > this.clientHeight
) {
item.dy = -item.dy;
}
});
},
getRandomColor() {
return randomColor[Math.floor(Math.random() * randomColor.length)];
},
getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
},
getCurshBreaks() {
let _this = this;
this.breaks.forEach(item => {
if (item.show) {
item.show = false;
for (let i = 0; i < 8; i++) {
this.crushBalls.push({
x: item.x + this.breaksConfig.width / 2,
y: item.y + this.breaksConfig.height / 2,
dx: _this.getRandomArbitrary(-6, 6),
dy: _this.getRandomArbitrary(-6, 6),
r: _this.getRandomArbitrary(1, 4),
color: _this.getRandomColor()
});
}
}
});
},
drawBall() {
this.ctx.beginPath();
this.ctx.arc(this.ball.x, this.ball.y, this.ball.r, 0, 2 * Math.PI);
this.ctx.fillStyle = "#008b8b";
this.ctx.fill();
this.ctx.closePath();
},
drawPannel() {
this.drawRoundRect(
this.pannel.x,
this.pannel.y,
this.pannel.width,
this.pannel.height,
5
);
},
drawRoundRect(x, y, width, height, radius) {
this.ctx.beginPath();
this.ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);
this.ctx.lineTo(width - radius + x, y);
this.ctx.arc(
width - radius + x,
radius + y,
radius,
(Math.PI * 3) / 2,
Math.PI * 2
);
this.ctx.lineTo(width + x, height + y - radius);
this.ctx.arc(
width - radius + x,
height - radius + y,
radius,
0,
(Math.PI * 1) / 2
);
this.ctx.lineTo(radius + x, height + y);
this.ctx.arc(
radius + x,
height - radius + y,
radius,
(Math.PI * 1) / 2,
Math.PI
);
this.ctx.fillStyle = "#008b8b";
this.ctx.fill();
this.ctx.closePath();
},
drawBreaks() {
let _this = this;
_this.breaks.forEach(item => {
if (item.show) {
_this.drawRoundRect(
item.x,
item.y,
_this.breaksConfig.width,
_this.breaksConfig.height,
_this.breaksConfig.radius
);
}
});
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.break-ball {
width: 900px;
height: 600px;
position: relative;
#breakBall {
background: #2a4546;
}
.container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.3);
text-align: center;
font-size: 0;
white-space: nowrap;
overflow: auto;
}
.container:after {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
}
.dialog {
width: 400px;
height: 300px;
background: rgba(255, 255, 255, 0.5);
box-shadow: 3px 3px 6px 3px rgba(0, 0, 0, 0.3);
display: inline-block;
vertical-align: middle;
text-align: left;
font-size: 28px;
color: #fff;
font-weight: 600;
border-radius: 10px;
white-space: normal;
text-align: center;
.once-again-btn {
background: #1f9a9a;
border: none;
color: #fff;
}
}
}
</style>
到此這篇關(guān)于VUE+Canvas 實(shí)現(xiàn)桌面彈球消磚塊小游戲的文章就介紹到這了,更多相關(guān)vue彈球消磚塊小游戲內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Vue使用Canvas生成隨機(jī)大小且不重疊圓
- Vue使用canvas實(shí)現(xiàn)圖片壓縮上傳
- vue+canvas繪制時(shí)間軸的方法
- VUE+Canvas實(shí)現(xiàn)簡(jiǎn)單五子棋游戲的全過程
- VUE+Canvas實(shí)現(xiàn)財(cái)神爺接元寶小游戲
- 如何用VUE和Canvas實(shí)現(xiàn)雷霆戰(zhàn)機(jī)打字類小游戲
- Vue使用鼠標(biāo)在Canvas上繪制矩形
- vue使用canvas實(shí)現(xiàn)移動(dòng)端手寫簽名
- vue+canvas實(shí)現(xiàn)拼圖小游戲
- vue使用canvas手寫輸入識(shí)別中文
相關(guān)文章
vuex?mutations的兩種調(diào)用方法小結(jié)
這篇文章主要介紹了vuex?mutations的兩種調(diào)用方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
vue mint-ui學(xué)習(xí)筆記之picker的使用
本篇文章主要介紹了vue mint-ui學(xué)習(xí)筆記之picker的使用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-10-10
Vue實(shí)現(xiàn)virtual-dom的原理簡(jiǎn)析
這篇文章主要介紹了Vue實(shí)現(xiàn)virtual-dom的原理簡(jiǎn)析,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-07-07
vue登錄注冊(cè)及token驗(yàn)證實(shí)現(xiàn)代碼
在vue單頁(yè)中,我們可以通過監(jiān)控route對(duì)象,從中匹配信息去決定是否驗(yàn)證token,然后定義后續(xù)行為。下面通過實(shí)例代碼給大家分享vue登錄注冊(cè)及token驗(yàn)證功能,需要的朋友參考下吧2017-12-12
Vue3中Composition?API和Options?API的區(qū)別
Vue3的Composition API和Options API是Vue.js框架中的兩種不同的API,本文主要介紹了Vue3中Composition?API和Options?API的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Vue3使用Echarts導(dǎo)致tooltip失效問題及解決方法
Vue3 使用 proxy 對(duì)象代理,而 echarts 則使用了大量的全等(===), 對(duì)比失敗從而導(dǎo)致了bug,這篇文章主要介紹了Vue3使用Echarts導(dǎo)致tooltip失效問題及解決方法,需要的朋友可以參考下2023-08-08
vue-cli 3.0 引入mint-ui報(bào)錯(cuò)問題及解決
這篇文章主要介紹了vue-cli 3.0 引入mint-ui報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
解決vue-cli3創(chuàng)建使用iview定制主題javascriptEnabled找不到該配置項(xiàng)
這篇文章主要介紹了解決vue-cli3創(chuàng)建使用iview定制主題javascriptEnabled找不到該配置項(xiàng)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
vue中前進(jìn)刷新、后退緩存用戶瀏覽數(shù)據(jù)和瀏覽位置的實(shí)例講解
今天小編就為大家分享一篇vue中前進(jìn)刷新、后退緩存用戶瀏覽數(shù)據(jù)和瀏覽位置的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2018-09-09

