簡(jiǎn)在之前完成有關(guān)于canvas這方面的相關(guān)內(nèi)容整理和項(xiàng)目的時(shí)候,我就發(fā)現(xiàn)原來(lái)canvas可以實(shí)現(xiàn)的內(nèi)容還有很多,那么今天我們就來(lái)說(shuō)說(shuō)有關(guān)于:“簡(jiǎn)單的俄羅斯方塊小游戲案例分享!”這方面的內(nèi)容的分享!
導(dǎo)言
在一個(gè)風(fēng)和日麗的一天,看完了瘋狂HTML 5+CSS 3+JavaScript講義,跟著做了書里最后一章的俄羅斯方塊小游戲,并做了一些改進(jìn),作為自己前端學(xué)習(xí)的第一站。
游戲效果:
制作思路
因?yàn)闀锏亩砹_斯方塊比較普通,太常規(guī)了,不是很好看,所以我在網(wǎng)上找了上面那張圖片,打算照著它來(lái)做。(請(qǐng)無(wú)視成品和原圖的差距)
然后便是游戲界面和常規(guī)的俄羅斯方塊游戲邏輯。
接著便是游戲結(jié)束界面了。
原本想做個(gè)彈出層,但覺(jué)得找圖片有點(diǎn)麻煩,所以就在網(wǎng)上找了文字特效,套用了一下。
代碼實(shí)現(xiàn):
首先是html文件和css文件,主要涉及了布局方面。作為新手,在上面真的是翻來(lái)覆去的踩坑。o(╥﹏╥)o
index.html:
<!DOCTYPE html>
<html>
<head>
<title>俄羅斯方塊</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<link rel=stylesheet type="text/css" href="teris.css">
<style type="text/css">
/*導(dǎo)入外部的字體文件*/
@font-face{
font-family:tmb;/*為字體命名為tmb*/
src:url("DS-DIGIB.TTF") format("TrueType");/*format為字體文件格式,TrueType為ttf*/
}
div>span{
font-family:tmb;
font-size:18pt;
color:green;
}
</style>
</head>
<body>
<div id="container" class="bg">
<!--ui-->
<div class="ui_bg">
<div style="float:left;margin-right:4px;">
速度:<span id="cur_speed">1</span>
</div>
<div style="float:left;">
當(dāng)前分?jǐn)?shù):<span id="cur_points">0</span>
</div>
<div style="float:right;">
最高分?jǐn)?shù):<span id="max_points">0</span>
</div>
</div>
<canvas id="text" width="500" height="100" style="position:absolute;"></canvas>
<canvas id="stage" width="500" height="100" style="position:absolute;"></canvas>
</div>
<script src='EasePack.min.js'></script>
<script src='TweenLite.min.js'></script>
<script src='easeljs-0.7.1.min.js'></script>
<script src='requestAnimationFrame.js'></script>
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
<script type="text/javascript" src="teris.js"></script>
</body>
</html>
teris.css
*{
margin:0;
padding:0;
}
html, body{
width:100%;
height:100%;
}
.bg{
font-size:13pt;
background-color:rgb(239, 239, 227);
/*好看的漸變色*/
background-image:radial-gradient(rgb(239, 239, 227), rgb(230, 220, 212));
/*陰影*/
box-shadow:#cdc8c1 -1px -1px 7px 0px;
padding-bottom:4px;
}
.ui_bg{
border-bottom:1px #a69e9ea3 solid;
padding-bottom:2px;
overflow:hidden;/*沒(méi)有這句的話因?yàn)樽觗iv都設(shè)置了float,所以是浮在網(wǎng)頁(yè)上的,所以父div就沒(méi)有高度,這句清除了浮動(dòng),讓父div有了子div的高度*/
}
然后是重頭戲,teris.js
游戲變量:
//游戲設(shè)定
var TETRIS_ROWS = 20;
var TETRIS_COLS = 14;
var CELL_SIZE = 24;
var NO_BLOCK=0;
var HAVE_BLOCK=1;
// 定義幾種可能出現(xiàn)的方塊組合
var blockArr = [
// Z
[
{x: TETRIS_COLS / 2 - 1 , y:0},
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 , y:1},
{x: TETRIS_COLS / 2 + 1 , y:1}
],
// 反Z
[
{x: TETRIS_COLS / 2 + 1 , y:0},
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 , y:1},
{x: TETRIS_COLS / 2 - 1 , y:1}
],
// 田
[
{x: TETRIS_COLS / 2 - 1 , y:0},
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 - 1 , y:1},
{x: TETRIS_COLS / 2 , y:1}
],
// L
[
{x: TETRIS_COLS / 2 - 1 , y:0},
{x: TETRIS_COLS / 2 - 1, y:1},
{x: TETRIS_COLS / 2 - 1 , y:2},
{x: TETRIS_COLS / 2 , y:2}
],
// J
[
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 , y:1},
{x: TETRIS_COLS / 2 , y:2},
{x: TETRIS_COLS / 2 - 1, y:2}
],
// □□□□
[
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 , y:1},
{x: TETRIS_COLS / 2 , y:2},
{x: TETRIS_COLS / 2 , y:3}
],
// ┴
[
{x: TETRIS_COLS / 2 , y:0},
{x: TETRIS_COLS / 2 - 1 , y:1},
{x: TETRIS_COLS / 2 , y:1},
{x: TETRIS_COLS / 2 + 1, y:1}
]
];
// 記錄當(dāng)前積分
var curScore=0;
// 記錄曾經(jīng)的最高積分
var maxScore=1;
var curSpeed=1;
//ui元素
var curSpeedEle=document.getElementById("cur_speed");
var curScoreEle=document.getElementById("cur_points");
var maxScoreEle=document.getElementById("max_points");
var timer;//方塊下落控制
var myCanvas;
var canvasCtx;
var tetris_status;//地圖數(shù)據(jù)
var currentFall;//當(dāng)前下落的block
游戲界面的完善
//create canvas
function createCanvas(){
myCanvas=document.createElement("canvas");
myCanvas.width=TETRIS_COLS*CELL_SIZE;
myCanvas.height=TETRIS_ROWS*CELL_SIZE;
//繪制背景
canvasCtx=myCanvas.getContext("2d");
canvasCtx.beginPath();
//TETRIS_COS
for(let i=1; i<TETRIS_COLS; i++){
canvasCtx.moveTo(i*CELL_SIZE, 0);
canvasCtx.lineTo(i*CELL_SIZE, myCanvas.height);
}
for(let i=1; i<TETRIS_ROWS; i++){
canvasCtx.moveTo(0, i*CELL_SIZE);
canvasCtx.lineTo(myCanvas.width, i*CELL_SIZE);
}
canvasCtx.closePath();
canvasCtx.strokeStyle="#b4a79d";
canvasCtx.lineWidth=0.6;
canvasCtx.stroke();
//第一行,最后一行,第一列,最后一列粗一點(diǎn)。
canvasCtx.beginPath();
canvasCtx.moveTo(0, 0);
canvasCtx.lineTo(myCanvas.width, 0);
canvasCtx.moveTo(0, myCanvas.height);
canvasCtx.lineTo(myCanvas.width, myCanvas.height);
canvasCtx.moveTo(0, 0);
canvasCtx.lineTo(0, myCanvas.height);
canvasCtx.moveTo(myCanvas.width, 0);
canvasCtx.lineTo(myCanvas.width, myCanvas.height);
canvasCtx.closePath();
canvasCtx.strokeStyle="#b4a79d";
canvasCtx.lineWidth=4;
canvasCtx.stroke();
//設(shè)置繪制block時(shí)的style
canvasCtx.fillStyle="#201a14";
}
draw canvas
function changeWidthAndHeight(w, h){
//通過(guò)jquery設(shè)置css
h+=$("ui_bg").css("height")+$("ui_bg").css("margin-rop")+$("ui_bg").css("margin-bottom")+$("ui_bg").css("padding-top")+$("ui_bg").css("padding-bottom");
$(".bg").css({
"width":w,
"height":h,
"top":0, "bottom":0, "right":0, "left":0,
"margin":"auto"
});
}
change width and height
//draw blocks
function drawBlocks(){
//清空地圖
for(let i=0; i<TETRIS_ROWS;i++){
for(let j=0;j<TETRIS_COLS;j++)
canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
}
//繪制地圖
for(let i=0; i<TETRIS_ROWS;i++){
for(let j=0;j<TETRIS_COLS;j++){
if(tetris_status[i][j]!=NO_BLOCK)
canvasCtx.fillRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);//中間留點(diǎn)縫隙
}
}
//繪制currentFall
for(let i=0;i<currentFall.length;i++)
canvasCtx.fillRect(currentFall[i].x*CELL_SIZE+1, currentFall[i].y*CELL_SIZE+1, CELL_SIZE-2,CELL_SIZE-2);
}
draw block
游戲邏輯
function rotate(){
// 定義記錄能否旋轉(zhuǎn)的旗標(biāo)
var canRotate = true;
for (var i = 0 ; i < currentFall.length ; i++)
{
var preX = currentFall[i].x;
var preY = currentFall[i].y;
// 始終以第三個(gè)方塊作為旋轉(zhuǎn)的中心,
// i == 2時(shí),說(shuō)明是旋轉(zhuǎn)的中心
if(i != 2)
{
// 計(jì)算方塊旋轉(zhuǎn)后的x、y坐標(biāo)
var afterRotateX = currentFall[2].x + preY - currentFall[2].y;
var afterRotateY = currentFall[2].y + currentFall[2].x - preX;
// 如果旋轉(zhuǎn)后所在位置已有方塊,表明不能旋轉(zhuǎn)
if(tetris_status[afterRotateY][afterRotateX + 1] != NO_BLOCK)
{
canRotate = false;
break;
}
// 如果旋轉(zhuǎn)后的坐標(biāo)已經(jīng)超出了最左邊邊界
if(afterRotateX < 0 || tetris_status[afterRotateY - 1][afterRotateX] != NO_BLOCK)
{
moveRight();
afterRotateX = currentFall[2].x + preY - currentFall[2].y;
afterRotateY = currentFall[2].y + currentFall[2].x - preX;
break;
}
if(afterRotateX < 0 || tetris_status[afterRotateY-1][afterRotateX] != NO_BLOCK)
{
moveRight();
break;
}
// 如果旋轉(zhuǎn)后的坐標(biāo)已經(jīng)超出了最右邊邊界
if(afterRotateX >= TETRIS_COLS - 1 ||
tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK)
{
moveLeft();
afterRotateX = currentFall[2].x + preY - currentFall[2].y;
afterRotateY = currentFall[2].y + currentFall[2].x - preX;
break;
}
if(afterRotateX >= TETRIS_COLS - 1 ||
tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK)
{
moveLeft();
break;
}
}
}
if(canRotate){
for (var i = 0 ; i < currentFall.length ; i++){
var preX = currentFall[i].x;
var preY = currentFall[i].y;
if(i != 2){
currentFall[i].x = currentFall[2].x +
preY - currentFall[2].y;
currentFall[i].y = currentFall[2].y +
currentFall[2].x - preX;
}
}
localStorage.setItem("currentFall", JSON.stringify(currentFall));
}
}
旋轉(zhuǎn)
//按下 下 或 interval到了
function next(){
if(moveDown()){
//記錄block
for(let i=0;i<currentFall.length;i++)
tetris_status[currentFall[i].y][currentFall[i].x]=HAVE_BLOCK;
//判斷有沒(méi)有滿行的
for(let j=0;j<currentFall.length;j++){
for(let i=0;i<TETRIS_COLS; i++){
if(tetris_status[currentFall[j].y][i]==NO_BLOCK)
break;
//最后一行滿了
if(i==TETRIS_COLS-1){
//消除最后一行
for(let i=currentFall[j].y; i>0;i--){
for(let j=0;j<TETRIS_COLS;j++)
tetris_status[i][j]=tetris_status[i-1][j];
}
//分?jǐn)?shù)增加
curScore+=5;
localStorage.setItem("curScore", curScore);
if(curScore>maxScore){
//超越最高分
maxScore=curScore;
localStorage.setItem("maxScore", maxScore);
}
//加速
curSpeed+=0.1;
localStorage.setItem("curSpeed", curSpeed);
//ui輸出
curScoreEle.innerHTML=""+curScore;
maxScoreEle.innerHTML=""+maxScore;
curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留兩位小數(shù)
clearInterval(timer);
timer=setInterval(function(){
next();
}, 500/curSpeed);
}
}
}
//判斷是否觸頂
for(let i=0;i<currentFall.length;i++){
if(currentFall[i].y==0){
gameEnd();
return;
}
}
localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
//新的block
createBlock();
localStorage.setItem("currentFall", JSON.stringify(currentFall));
}
drawBlocks();
}
//右移
function moveRight(){
for(let i=0;i<currentFall.length;i++){
if(currentFall[i].x+1>=TETRIS_ROWS || tetris_status[currentFall[i].y][currentFall[i].x+1]!=NO_BLOCK)
return;
}
for(let i=0;i<currentFall.length;i++)
currentFall[i].x++;
localStorage.setItem("currentFall", JSON.stringify(currentFall));
return;
}
//左移
function moveLeft(){
for(let i=0;i<currentFall.length;i++){
if(currentFall[i].x-1<0 || tetris_status[currentFall[i].y][currentFall[i].x-1]!=NO_BLOCK)
return;
}
for(let i=0;i<currentFall.length;i++)
currentFall[i].x--;
localStorage.setItem("currentFall", JSON.stringify(currentFall));
return;
}
//judge can move down and if arrive at end return 1, if touch other blocks return 2, else, return 0
function moveDown(){
for(let i=0;i<currentFall.length;i++){
if(currentFall[i].y>=TETRIS_ROWS-1 || tetris_status[currentFall[i].y+1][currentFall[i].x]!=NO_BLOCK)
return true;
}
for(let i=0;i<currentFall.length;i++)
currentFall[i].y+=1;
return false;
}
上下左右移動(dòng)
function gameKeyEvent(evt){
switch(evt.keyCode){
//向下
case 40://↓
case 83://S
next();
drawBlocks();
break;
//向左
case 37://←
case 65://A
moveLeft();
drawBlocks();
break;
//向右
case 39://→
case 68://D
moveRight();
drawBlocks();
break;
//旋轉(zhuǎn)
case 38://↑
case 87://W
rotate();
drawBlocks();
break;
}
}
keydown事件監(jiān)聽
keydown事件監(jiān)聽
其他的詳細(xì)情況可以看源代碼,我就不整理了。
接下來(lái)我們看游戲結(jié)束時(shí)的特效。因?yàn)槲乙膊皇呛芏?,所以在這里整理的會(huì)比較詳細(xì)。當(dāng)做學(xué)習(xí)。
//game end
function gameEnd(){
clearInterval(timer);
//鍵盤輸入監(jiān)聽結(jié)束
window.onkeydown=function(){
//按任意鍵重新開始游戲
window.onkeydown=gameKeyEvent;
//初始化游戲數(shù)據(jù)
initData();
createBlock();
localStorage.setItem("currentFall", JSON.stringify(currentFall));
localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
localStorage.setItem("curScore", curScore);
localStorage.setItem("curSpeed", curSpeed);
//繪制
curScoreEle.innerHTML=""+curScore;
curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留兩位小數(shù)
drawBlocks();
timer=setInterval(function(){
next();
}, 500/curSpeed);
//清除特效
this.stage.removeAllChildren();
this.textStage.removeAllChildren();
};
//特效,游戲結(jié)束
setTimeout(function(){
initAnim();
//擦除黑色方塊
for(let i=0; i<TETRIS_ROWS;i++){
for(let j=0;j<TETRIS_COLS;j++)
canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
}
}, 200);
//推遲顯示Failed
setTimeout(function(){
if(textFormed) {
explode();
setTimeout(function() {
createText("FAILED");
}, 810);
} else {
createText("FAILED");
}
}, 800);
}
上面代碼里的localstorage是html5的本地?cái)?shù)據(jù)存儲(chǔ)。因?yàn)椴皇沁\(yùn)用很難,所以具體看代碼。
整個(gè)特效是運(yùn)用了createjs插件。要引入幾個(gè)文件。
easeljs-0.7.1.min.js,EasePacj.min.js,requestAnimationFrame.js和TweenLite.min.js 游戲重新開始就要清除特效。我看api里我第一眼望過(guò)去最明顯的就是removeAllChildren(),所以就選了這個(gè)。其他的改進(jìn)日后再說(shuō)。
//清除特效
this.stage.removeAllChildren();
this.textStage.removeAllChildren();
function initAnim() {
initStages();
initText();
initCircles();
//在stage下方添加文字——按任意鍵重新開始游戲.
tmp = new createjs.Text("t", "12px 'Source Sans Pro'", "#54555C");
tmp.textAlign = 'center';
tmp.x = 180;
tmp.y=350;
tmp.text = "按任意鍵重新開始游戲";
stage.addChild(tmp);
animate();
}
initAnim
上面初始化了一個(gè)stage,用于存放特效,一個(gè)textstage,用于形成“FAILED”的像素圖片。還有一個(gè)按任意鍵重新游戲的提示。同時(shí)開始每隔一段時(shí)間就刷新stage。
根據(jù)block的位置來(lái)初始化小圓點(diǎn)。
function initCircles() {
circles = [];
var p=[];
var count=0;
for(let i=0; i<TETRIS_ROWS;i++)
for(let j=0;j<TETRIS_COLS;j++)
if(tetris_status[i][j]!=NO_BLOCK)
p.push({'x':j*CELL_SIZE+2, 'y':i*CELL_SIZE+2, 'w':CELL_SIZE-3, 'h':CELL_SIZE-4});
for(var i=0; i<250; i++) {
var circle = new createjs.Shape();
var r = 7;
//x和y范圍限定在黑色block內(nèi)
var x = p[count]['x']+p[count]['w']*Math.random();
var y = p[count]['y']+p[count]['h']*Math.random();
count++;
if(count>=p.length)
count=0;
var color = colors[Math.floor(i%colors.length)];
var alpha = 0.2 + Math.random()*0.5;
circle.alpha = alpha;
circle.radius = r;
circle.graphics.beginFill(color).drawCircle(0, 0, r);
circle.x = x;
circle.y = y;
circles.push(circle);
stage.addChild(circle);
circle.movement = 'float';
tweenCircle(circle);
}
}
initCircles
然后再講顯示特效Failed的createText()。先將FAILED的text顯示在textstage里,然后ctx.getImageData.data獲取像素?cái)?shù)據(jù),并以此來(lái)為每個(gè)小圓點(diǎn)定義位置。
function createText(t) {
curText=t;
var fontSize = 500/(t.length);
if (fontSize > 80) fontSize = 80;
text.text = t;
text.font = "900 "+fontSize+"px 'Source Sans Pro'";
text.textAlign = 'center';
text.x = TETRIS_COLS*CELL_SIZE/2;
text.y = 0;
textStage.addChild(text);
textStage.update();
var ctx = document.getElementById('text').getContext('2d');
var pix = ctx.getImageData(0,0,600,200).data;
textPixels = [];
for (var i = pix.length; i >= 0; i -= 4) {
if (pix[i] != 0) {
var x = (i / 4) % 600;
var y = Math.floor(Math.floor(i/600)/4);
if((x && x%8 == 0) && (y && y%8 == 0)) textPixels.push({x: x, y: y});
}
}
formText();
textStage.clear();//清楚text的顯示
}
CreateText
跟著代碼的節(jié)奏走,我們現(xiàn)在來(lái)到了formtext.
function formText() {
for(var i= 0, l=textPixels.length; i<l; i++) {
circles[i].originX = offsetX + textPixels[i].x;
circles[i].originY = offsetY + textPixels[i].y;
tweenCircle(circles[i], 'in');
}
textFormed = true;
if(textPixels.length < circles.length) {
for(var j = textPixels.length; j<circles.length; j++) {
circles[j].tween = TweenLite.to(circles[j], 0.4, {alpha: 0.1});
}
}
}
formtext
explode()就是講已組成字的小圓點(diǎn)給重新遣散。
動(dòng)畫實(shí)現(xiàn)是使用了tweenlite.
function tweenCircle(c, dir) {
if(c.tween) c.tween.kill();
if(dir == 'in') {
/*TweenLite.to 改變c實(shí)例的x坐標(biāo),y坐標(biāo),使用easeInOut彈性函數(shù),透明度提到1,改變大小,radius,總用時(shí)0.4s*/
c.tween = TweenLite.to(c, 0.4, {x: c.originX, y: c.originY, ease:Quad.easeInOut, alpha: 1, radius: 5, scaleX: 0.4, scaleY: 0.4, onComplete: function() {
c.movement = 'jiggle';/*輕搖*/
tweenCircle(c);
}});
} else if(dir == 'out') {
c.tween = TweenLite.to(c, 0.8, {x: window.innerWidth*Math.random(), y: window.innerHeight*Math.random(), ease:Quad.easeInOut, alpha: 0.2 + Math.random()*0.5, scaleX: 1, scaleY: 1, onComplete: function() {
c.movement = 'float';
tweenCircle(c);
}});
} else {
if(c.movement == 'float') {
c.tween = TweenLite.to(c, 5 + Math.random()*3.5, {x: c.x + -100+Math.random()*200, y: c.y + -100+Math.random()*200, ease:Quad.easeInOut, alpha: 0.2 + Math.random()*0.5,
onComplete: function() {
tweenCircle(c);
}});
} else {
c.tween = TweenLite.to(c, 0.05, {x: c.originX + Math.random()*3, y: c.originY + Math.random()*3, ease:Quad.easeInOut,
onComplete: function() {
tweenCircle(c);
}});
}
}
}
TweenLite.to函數(shù)第一個(gè)參數(shù),要做動(dòng)畫的實(shí)例,第二個(gè)參數(shù),事件,第三個(gè)參數(shù),動(dòng)畫改變參數(shù)。
Quad.easeInOut()意思是在動(dòng)畫開始和結(jié)束時(shí)緩動(dòng)。onComplete動(dòng)畫完成時(shí)調(diào)用的函數(shù)。易得,在我們的應(yīng)用中,我們將開始下一次動(dòng)畫。
個(gè)人感言
其實(shí)剛開始沒(méi)想做這么復(fù)雜,所以文件排的比較隨意,然后就導(dǎo)致了后期項(xiàng)目完成時(shí)那副雜亂無(wú)章的樣子。^_^,以后改。等我等看懂動(dòng)畫效果時(shí)在說(shuō),現(xiàn)在用的有點(diǎn)半懵半懂。
這篇博客寫得有點(diǎn)亂。新手之作,就先這樣吧。同上,以后改。因?yàn)椴恢肋@個(gè)項(xiàng)目會(huì)不會(huì)拿來(lái)直接當(dāng)我們計(jì)算機(jī)職業(yè)實(shí)踐的作業(yè)。要是的話,我就徹改,連同博客。
以下是源代碼地址。
還在審核。明天再說(shuō)。相信我,我在小番茄里做了記錄。
總結(jié)
以上所述是小編給大家介紹的“簡(jiǎn)單的俄羅斯方塊小游戲案例分享!”這方面的相關(guān)內(nèi)容,希望對(duì)大家有所幫助,有喜歡的前端的小伙伴們都可以在W3Cschool網(wǎng)站中學(xué)習(xí)和了解!