导言
在一个风和日丽的一天,看完了疯狂HTML 5+CSS 3+JavaScript讲义,跟着做了书里最后一章的俄罗斯方块小游戏,并做了一些改进,作为自己前端学习的第一站。
游戏效果:
制作思路
因为书里的俄罗斯方块比较普通,太常规了,不是很好看,所以我在网上找了上面那张图片,打算照着它来做。(请无视成品和原图的差距)
然后便是游戏界面和常规的俄罗斯方块游戏逻辑。
接着便是游戏结束界面了。
原本想做个弹出层,但觉得找图片有点麻烦,所以就在网上找了文字特效,套用了一下。
代码实现:
首先是html文件和css文件,主要涉及了布局方面。作为新手,在上面真的是翻来覆去的踩坑。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">
/*导入外部的字体文件*/
@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;">
当前分数:<span id="cur_points">0</span>
</div>
<div style="float:right;">
最高分数:<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;/*没有这句的话因为子div都设置了float,所以是浮在网页上的,所以父div就没有高度,这句清除了浮动,让父div有了子div的高度*/
}
然后是重头戏,teris.js
游戏变量
//游戏设定
var TETRIS_ROWS = 20;
var TETRIS_COLS = 14;
var CELL_SIZE = 24;
var NO_BLOCK=0;
var HAVE_BLOCK=1;
// 定义几种可能出现的方块组合
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}
]
];
// 记录当前积分
var curScore=0;
// 记录曾经的最高积分
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;//地图数据
var currentFall;//当前下落的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();
//第一行,最后一行,第一列,最后一列粗一点。
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();
//设置绘制block时的style
canvasCtx.fillStyle="#201a14";
}
draw canvas
1 function changeWidthAndHeight(w, h){
2 //通过jquery设置css
3 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");
4 $(".bg").css({
5 "width":w,
6 "height":h,
7 "top":0, "bottom":0, "right":0, "left":0,
8 "margin":"auto"
9 });
10 }
change width and height
1 //draw blocks
2 function drawBlocks(){
3 //清空地图
4 for(let i=0; i<TETRIS_ROWS;i++){
5 for(let j=0;j<TETRIS_COLS;j++)
6 canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
7 }
8 //绘制地图
9 for(let i=0; i<TETRIS_ROWS;i++){
10 for(let j=0;j<TETRIS_COLS;j++){
11 if(tetris_status[i][j]!=NO_BLOCK)
12 canvasCtx.fillRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);//中间留点缝隙
13 }
14 }
15 //绘制currentFall
16 for(let i=0;i<currentFall.length;i++)
17 canvasCtx.fillRect(currentFall[i].x*CELL_SIZE+1, currentFall[i].y*CELL_SIZE+1, CELL_SIZE-2,CELL_SIZE-2);
18 }
draw block
游戏逻辑
1 function rotate(){
2 // 定义记录能否旋转的旗标
3 var canRotate = true;
4 for (var i = 0 ; i < currentFall.length ; i++)
5 {
6 var preX = currentFall[i].x;
7 var preY = currentFall[i].y;
8 // 始终以第三个方块作为旋转的中心,
9 // i == 2时,说明是旋转的中心
10 if(i != 2)
11 {
12 // 计算方块旋转后的x、y坐标
13 var afterRotateX = currentFall[2].x + preY - currentFall[2].y;
14 var afterRotateY = currentFall[2].y + currentFall[2].x - preX;
15 // 如果旋转后所在位置已有方块,表明不能旋转
16 if(tetris_status[afterRotateY][afterRotateX + 1] != NO_BLOCK)
17 {
18 canRotate = false;
19 break;
20 }
21 // 如果旋转后的坐标已经超出了最左边边界
22 if(afterRotateX < 0 || tetris_status[afterRotateY - 1][afterRotateX] != NO_BLOCK)
23 {
24 moveRight();
25 afterRotateX = currentFall[2].x + preY - currentFall[2].y;
26 afterRotateY = currentFall[2].y + currentFall[2].x - preX;
27 break;
28 }
29 if(afterRotateX < 0 || tetris_status[afterRotateY-1][afterRotateX] != NO_BLOCK)
30 {
31 moveRight();
32 break;
33 }
34 // 如果旋转后的坐标已经超出了最右边边界
35 if(afterRotateX >= TETRIS_COLS - 1 ||
36 tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK)
37 {
38 moveLeft();
39 afterRotateX = currentFall[2].x + preY - currentFall[2].y;
40 afterRotateY = currentFall[2].y + currentFall[2].x - preX;
41 break;
42 }
43 if(afterRotateX >= TETRIS_COLS - 1 ||
44 tetris_status[afterRotateY][afterRotateX+1] != NO_BLOCK)
45 {
46 moveLeft();
47 break;
48 }
49 }
50 }
51 if(canRotate){
52 for (var i = 0 ; i < currentFall.length ; i++){
53 var preX = currentFall[i].x;
54 var preY = currentFall[i].y;
55 if(i != 2){
56 currentFall[i].x = currentFall[2].x +
57 preY - currentFall[2].y;
58 currentFall[i].y = currentFall[2].y +
59 currentFall[2].x - preX;
60 }
61 }
62 localStorage.setItem("currentFall", JSON.stringify(currentFall));
63 }
64 }
旋转
1 //按下 下 或 interval到了
2 function next(){
3 if(moveDown()){
4 //记录block
5 for(let i=0;i<currentFall.length;i++)
6 tetris_status[currentFall[i].y][currentFall[i].x]=HAVE_BLOCK;
7 //判断有没有满行的
8 for(let j=0;j<currentFall.length;j++){
9 for(let i=0;i<TETRIS_COLS; i++){
10 if(tetris_status[currentFall[j].y][i]==NO_BLOCK)
11 break;
12 //最后一行满了
13 if(i==TETRIS_COLS-1){
14 //消除最后一行
15 for(let i=currentFall[j].y; i>0;i--){
16 for(let j=0;j<TETRIS_COLS;j++)
17 tetris_status[i][j]=tetris_status[i-1][j];
18 }
19 //分数增加
20 curScore+=5;
21 localStorage.setItem("curScore", curScore);
22 if(curScore>maxScore){
23 //超越最高分
24 maxScore=curScore;
25 localStorage.setItem("maxScore", maxScore);
26 }
27 //加速
28 curSpeed+=0.1;
29 localStorage.setItem("curSpeed", curSpeed);
30 //ui输出
31 curScoreEle.innerHTML=""+curScore;
32 maxScoreEle.innerHTML=""+maxScore;
33 curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留两位小数
34 clearInterval(timer);
35 timer=setInterval(function(){
36 next();
37 }, 500/curSpeed);
38 }
39 }
40 }
41 //判断是否触顶
42 for(let i=0;i<currentFall.length;i++){
43 if(currentFall[i].y==0){
44 gameEnd();
45 return;
46 }
47 }
48 localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
49 //新的block
50 createBlock();
51 localStorage.setItem("currentFall", JSON.stringify(currentFall));
52 }
53 drawBlocks();
54 }
55
56 //右移
57 function moveRight(){
58 for(let i=0;i<currentFall.length;i++){
59 if(currentFall[i].x+1>=TETRIS_ROWS || tetris_status[currentFall[i].y][currentFall[i].x+1]!=NO_BLOCK)
60 return;
61 }
62 for(let i=0;i<currentFall.length;i++)
63 currentFall[i].x++;
64 localStorage.setItem("currentFall", JSON.stringify(currentFall));
65 return;
66 }
67 //左移
68 function moveLeft(){
69 for(let i=0;i<currentFall.length;i++){
70 if(currentFall[i].x-1<0 || tetris_status[currentFall[i].y][currentFall[i].x-1]!=NO_BLOCK)
71 return;
72 }
73 for(let i=0;i<currentFall.length;i++)
74 currentFall[i].x--;
75 localStorage.setItem("currentFall", JSON.stringify(currentFall));
76 return;
77 }
78 //judge can move down and if arrive at end return 1, if touch other blocks return 2, else, return 0
79 function moveDown(){
80 for(let i=0;i<currentFall.length;i++){
81 if(currentFall[i].y>=TETRIS_ROWS-1 || tetris_status[currentFall[i].y+1][currentFall[i].x]!=NO_BLOCK)
82 return true;
83 }
84
85 for(let i=0;i<currentFall.length;i++)
86 currentFall[i].y+=1;
87 return false;
88 }
上下左右移动
1 function gameKeyEvent(evt){
2 switch(evt.keyCode){
3 //向下
4 case 40://↓
5 case 83://S
6 next();
7 drawBlocks();
8 break;
9 //向左
10 case 37://←
11 case 65://A
12 moveLeft();
13 drawBlocks();
14 break;
15 //向右
16 case 39://→
17 case 68://D
18 moveRight();
19 drawBlocks();
20 break;
21 //旋转
22 case 38://↑
23 case 87://W
24 rotate();
25 drawBlocks();
26 break;
27 }
28 }
keydown事件监听
其他的详细情况可以看源代码,我就不整理了。
接下来我们看游戏结束时的特效。因为我也不是很懂,所以在这里整理的会比较详细。当做学习。
1 //game end
2 function gameEnd(){
3 clearInterval(timer);
4 //键盘输入监听结束
5 window.onkeydown=function(){
6 //按任意键重新开始游戏
7 window.onkeydown=gameKeyEvent;
8 //初始化游戏数据
9 initData();
10 createBlock();
11 localStorage.setItem("currentFall", JSON.stringify(currentFall));
12 localStorage.setItem("tetris_status", JSON.stringify(tetris_status));
13 localStorage.setItem("curScore", curScore);
14 localStorage.setItem("curSpeed", curSpeed);
15 //绘制
16 curScoreEle.innerHTML=""+curScore;
17 curSpeedEle.innerHTML=curSpeed.toFixed(1);//保留两位小数
18 drawBlocks();
19 timer=setInterval(function(){
20 next();
21 }, 500/curSpeed);
22 //清除特效
23 this.stage.removeAllChildren();
24 this.textStage.removeAllChildren();
25 };
26 //特效,游戏结束
27 setTimeout(function(){
28 initAnim();
29 //擦除黑色方块
30 for(let i=0; i<TETRIS_ROWS;i++){
31 for(let j=0;j<TETRIS_COLS;j++)
32 canvasCtx.clearRect(j*CELL_SIZE+1, i*CELL_SIZE+1, CELL_SIZE-2, CELL_SIZE-2);
33 }
34 }, 200);
35 //推迟显示Failed
36 setTimeout(function(){
37 if(textFormed) {
38 explode();
39 setTimeout(function() {
40 createText("FAILED");
41 }, 810);
42 } else {
43 createText("FAILED");
44 }
45 }, 800);
46 }
上面代码里的localstorage是html5的本地数据存储。因为不是运用很难,所以具体看代码。
整个特效是运用了createjs插件。要引入几个文件。
easeljs-0.7.1.min.js, EasePacj.min.js, requestAnimationFrame.js和TweenLite.min.js
游戏重新开始就要清除特效。我看api里我第一眼望过去最明显的就是removeAllChildren(),所以就选了这个。其他的改进日后再说。
//清除特效
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
上面初始化了一个stage,用于存放特效,一个textstage,用于形成“FAILED”的像素图片。还有一个按任意键重新游戏的提示。同时开始每隔一段时间就刷新stage。
根据block的位置来初始化小圆点。
1 function initCircles() {
2 circles = [];
3 var p=[];
4 var count=0;
5 for(let i=0; i<TETRIS_ROWS;i++)
6 for(let j=0;j<TETRIS_COLS;j++)
7 if(tetris_status[i][j]!=NO_BLOCK)
8 p.push({'x':j*CELL_SIZE+2, 'y':i*CELL_SIZE+2, 'w':CELL_SIZE-3, 'h':CELL_SIZE-4});
9 for(var i=0; i<250; i++) {
10 var circle = new createjs.Shape();
11 var r = 7;
12 //x和y范围限定在黑色block内
13 var x = p[count]['x']+p[count]['w']*Math.random();
14 var y = p[count]['y']+p[count]['h']*Math.random();
15 count++;
16 if(count>=p.length)
17 count=0;
18 var color = colors[Math.floor(i%colors.length)];
19 var alpha = 0.2 + Math.random()*0.5;
20 circle.alpha = alpha;
21 circle.radius = r;
22 circle.graphics.beginFill(color).drawCircle(0, 0, r);
23 circle.x = x;
24 circle.y = y;
25 circles.push(circle);
26 stage.addChild(circle);
27 circle.movement = 'float';
28 tweenCircle(circle);
29 }
30 }
initCircles
然后再讲显示特效Failed的createText()。先将FAILED的text显示在textstage里,然后ctx.getImageData.data获取像素数据,并以此来为每个小圆点定义位置。
1 function createText(t) {
2 curText=t;
3 var fontSize = 500/(t.length);
4 if (fontSize > 80) fontSize = 80;
5 text.text = t;
6 text.font = "900 "+fontSize+"px 'Source Sans Pro'";
7 text.textAlign = 'center';
8 text.x = TETRIS_COLS*CELL_SIZE/2;
9 text.y = 0;
10 textStage.addChild(text);
11 textStage.update();
12
13 var ctx = document.getElementById('text').getContext('2d');
14 var pix = ctx.getImageData(0,0,600,200).data;
15 textPixels = [];
16 for (var i = pix.length; i >= 0; i -= 4) {
17 if (pix[i] != 0) {
18 var x = (i / 4) % 600;
19 var y = Math.floor(Math.floor(i/600)/4);
20 if((x && x%8 == 0) && (y && y%8 == 0)) textPixels.push({x: x, y: y});
21 }
22 }
23
24 formText();
25 textStage.clear();//清楚text的显示
26 }
CreateText
跟着代码的节奏走,我们现在来到了formtext.
1 function formText() {
2 for(var i= 0, l=textPixels.length; i<l; i++) {
3 circles[i].originX = offsetX + textPixels[i].x;
4 circles[i].originY = offsetY + textPixels[i].y;
5 tweenCircle(circles[i], 'in');
6 }
7 textFormed = true;
8 if(textPixels.length < circles.length) {
9 for(var j = textPixels.length; j<circles.length; j++) {
10 circles[j].tween = TweenLite.to(circles[j], 0.4, {alpha: 0.1});
11 }
12 }
13 }
formtext
explode()就是讲已组成字的小圆点给重新遣散。
动画实现是使用了tweenlite.
1 function tweenCircle(c, dir) {
2 if(c.tween) c.tween.kill();
3 if(dir == 'in') {
4 /*TweenLite.to 改变c实例的x坐标,y坐标,使用easeInOut弹性函数,透明度提到1,改变大小,radius,总用时0.4s*/
5 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() {
6 c.movement = 'jiggle';/*轻摇*/
7 tweenCircle(c);
8 }});
9 } else if(dir == 'out') {
10 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() {
11 c.movement = 'float';
12 tweenCircle(c);
13 }});
14 } else {
15 if(c.movement == 'float') {
16 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,
17 onComplete: function() {
18 tweenCircle(c);
19 }});
20 } else {
21 c.tween = TweenLite.to(c, 0.05, {x: c.originX + Math.random()*3, y: c.originY + Math.random()*3, ease:Quad.easeInOut,
22 onComplete: function() {
23 tweenCircle(c);
24 }});
25 }
26 }
27 }
TweenLite.to函数第一个参数,要做动画的实例,第二个参数,事件,第三个参数,动画改变参数。
Quad.easeInOut()意思是在动画开始和结束时缓动。
onComplete动画完成时调用的函数。易得,在我们的应用中,我们将开始下一次动画。
个人感言
其实刚开始没想做这么复杂,所以文件排的比较随意,然后就导致了后期项目完成时那副杂乱无章的样子。^_^,以后改。等我等看懂动画效果时在说,现在用的有点半懵半懂。
这篇博客写得有点乱。新手之作,就先这样吧。同上,以后改。因为不知道这个项目会不会拿来直接当我们计算机职业实践的作业。要是的话,我就彻改,连同博客。
以下是源代码地址。(我还以为csdn的下载要的积分是自己定的。等我下次彻改的时候我传到github上。现在将就一下)