一、课程目标
- 随机生成降落模型(L型、凸型、Z型、一型、田型)。
- 程序中的每一个小块,都可以视为一个块元素。
- 活动中的模块都可以左右移动、旋转。
- 模块固定到底部后不可再移动,同时生成新的移动模块。
- 一行被排列完整之后,该行会消失,同时上方的模块会下落。
- 被固定的模块搞到达到界面顶部后,游戏结束。
二、项目开发
- 阶段一 (制作背景)
1. 创建一个宽为200px,高为360px的背景容器1. 在该容器上创建一个20*20的快元素1. 键盘控制该元素进行移动,每次移动20px1. 代码见 -> [阶段一开发代码](https://www.yuque.com/u12581613/sr444s/qi7p8l?view=doc_embed)
- 阶段二 (基础移动)
1. 将容器进行分割,分割为18行,10列。行高列宽均为20px1. 以16宫格(4行4列)为基准,定义L形状模型的4个方块位置1. 控制该模型进行移动 -> 也可以说是控制16宫格进行移动1. 代码见 -> [阶段二开发代码](https://www.yuque.com/u12581613/sr444s/ka31h0?view=doc_embed)
- 阶段三 (块元素控制)
1. 控制模型只能在容器中移动1. 当模型触底时,将块元素变成灰色固定在底部,同时生成一个新的模型1. 判断块元素与块元素之间的碰撞,分为左右碰撞和底部接触1. 代码见 -> [阶段三开发代码](https://www.yuque.com/u12581613/sr444s/pe82gb?view=doc_embed)
- 阶段四 (铺满下坠)
1. 判断一行是否铺满1. 清理铺满的一行1. 并让在其上方的块元素下落1. 代码见 -> [阶段四开发代码](https://www.yuque.com/u12581613/sr444s/yebpk3?view=doc_embed)
- 阶段五 (项目收尾)
1. 创建多种样式模型,根据随机数生成随机样式,创建自动坠落方法1. 判断游戏结束1. 美化样式 -> 下面完整代码
三、课程总结
项目难点
- 容器分割,构建16宫格的概念
- 观察块元素的位置变化,找出其中规律
- 判断块元素与块元素之间的碰撞
- 判断一行被铺满的情况
老师总结:
这些都是在js基础阶段学过的内容,只不过由于项目经验等原因,可能很多新手都无法写出来。本项目中没有根本上的技术难点,项目中也没有使用未学过的知识,我们学习到的知识是实实在在有用的,而不是为了学而学的东西。
个人总结:
老师说的确实有道理,听这堂课的时候感觉都听得懂,自己做就很难写出来,还不能熟练的融汇贯通,希望以后可以做的更好把。
项目完整代码
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><style type="text/css">.container{width: 200px;height: 360px;position: relative;background: url("images/bg.jpg") no-repeat;background-size: 200px 360px;}.activity_model{width: 20px;height: 20px;background: #a4b7da url("images/move.png") center no-repeat;background-size: 15px;border: .3px solid rgb(148, 170, 218);box-sizing: border-box;position: absolute;}.fixed_model{width: 20px;height: 20px;background: rgb(75, 79, 92) url("images/stop.png") center no-repeat;background-size: 15px;border: .3px solid rgb(162, 167, 177);box-sizing: border-box;position: absolute;}</style></head><body onload="init();"><!-- 背景容器 --><div id="container" class="container"><!-- 块元素 --></div><script>//创建一个常量控制步长const STEP = 20;//定义常量分割容器const ROW_COUNT = 18,COL_COUNT = 10;//创建每个模型的数据源const MODELS = [//第一个样式模型(L型){0:{row:2,col:0},1:{row:2,col:1},2:{row:2,col:2},3:{row:1,col:2}},//第二个样式模型(凸型){0:{row:1,col:1},1:{row:0,col:0},2:{row:1,col:0},3:{row:2,col:0}},//第三个样式模型(田型){0:{row:1,col:1},1:{row:2,col:1},2:{row:1,col:2},3:{row:2,col:2}},//第四个样式模型(一型){0:{row:0,col:0},1:{row:0,col:1},2:{row:0,col:2},3:{row:0,col:3}},//第四个样式模型(Z型){0:{row:1,col:1},1:{row:1,col:2},2:{row:2,col:2},3:{row:2,col:3}},];//创建变量,存放当前使用的模型var currentModel = {};//创建变量标记16宫格的位置var currentX = 0,currentY = 0;//记录所有块元素的位置//k=行_列 : v=块元素var fixedBlocks = {};//创建定时器var myInterval = null;//入口方法function init(){createModel();onKeyDown();}//根据模型的数据创建对应的块元素function createModel(){//判断游戏是否接受if (isGameOver()){gameOver();return;}//确定当前使用哪一个模型,随机数生成currentModel = MODELS[Math.round(Math.random()*4)];//重新初始化16宫格的位置currentX = 0;currentY = 0;//生成对应数量的块元素for (var key in currentModel){var divEle = document.createElement("div");divEle.className = "activity_model";document.querySelector("#container").appendChild(divEle);}//定位块元素的位置locationBlock();//让当前模型自动下落autoDown();}//根据数据源定位块元素的位置function locationBlock(){//判断块元素的越界行为checkBound();//1.拿到所有的块元素var eles = document.querySelectorAll(".activity_model");for (var i = 0;i<eles.length;i++){//单个块元素var activityModelEle = eles[i];//2.找到每个块元素对应的数据var blockModel = currentModel[i];//3.根据每个块元素对应的数据来指定块元素的位置//每个块元素的位置由两个条件决定:1、16宫格所在的位置。 2、块元素在16宫格中的位置activityModelEle.style.left = (currentX + blockModel.col) * STEP + "px";activityModelEle.style.top = (currentY + blockModel.row) * STEP + "px";}}//监听用户键盘事件function onKeyDown(){document.onkeydown = function(evnet){switch(event.keyCode){case 38:console.log("上");rotate();break;case 39:console.log("右");move(1,0);break;case 40:console.log("下");move(0,1);break;case 37:console.log("左");move(-1,0);break;}}}//控制块元素移动function move(x,y){if (isMeet(currentX + x,currentY+y,currentModel)){//底部的触碰发生在移动16宫格的时候,并且这次移动是因为Y轴的变化引起的if (y!=0){//模型之间底部发生触碰fixedBottomModel();}return;}//16宫格在动currentX += x;currentY += y;//根据16宫格来重新定位块元素locationBlock();}//控制模型的旋转function rotate(){//克隆一下currentModel,这里利用JSON方法完全复制一个currentModelvar json_obj = JSON.stringify(currentModel);var cloneCurrentModel = JSON.parse(json_obj);//算法//旋转后的行 == 旋转前的列//旋转后的列 == 3-旋转前的行//遍历我们当前的 模型数据源for (var key in cloneCurrentModel){//块元素数据源var blockModel = cloneCurrentModel[key];//实现算法var temp = blockModel.row;blockModel.row = blockModel.col;blockModel.col = 3-temp;}//如果旋转之后会发生触碰,那么就不需要进行旋转了if (isMeet(currentX,currentY,cloneCurrentModel)){return;}//接受这次旋转currentModel = cloneCurrentModel;locationBlock();}//控制模型只能在容器中移动function checkBound(){//定义模型活动的边界var leftBound = 0,rightBound = COL_COUNT,bottomBound = ROW_COUNT;//当块元素超出边界,让16宫格向后退一步for (var key in currentModel){var blockModel = currentModel[key];//判断左侧越界if (blockModel.col + currentX < leftBound) {currentX++;}//判断右侧越界if (blockModel.col + currentX >= rightBound) {currentX--;}//判断底部越界if (blockModel.row + currentY >= bottomBound){currentY--;//把模型固定在底部fixedBottomModel();}}}//把模型固定在底部function fixedBottomModel() {//1.改变模型(中块元素)的样式//2.让模型不可以再进行移动var activityModelEles = document.querySelectorAll(".activity_model");for (var i = activityModelEles.length - 1;i >= 0;i--){//更改每个块元素的类名//由于类名被更改,我们模型移动依靠的就是修改activity_model类的定位,现在不是原类名,自然就无法移动了activityModelEles[i].className = "fixed_model";//记录块元素的位置var blockModel = currentModel[i];fixedBlocks[(currentY + blockModel.row) + "_" + (currentX + blockModel.col)] = activityModelEles[i];}//判断是否要铺满一行需要清理isRemoveLine();//3.创建新的模型createModel();}//判断模型之间的触碰问题//X,Y表示16宫格<将要>移动到的位置//model 表示当前数据源<将要>完成的变化function isMeet(x,y,model){//所谓模型之间的触碰,在一个固定位置已经存在一个被固定的元素块时,那么活动中的模型不可以再占用该位置//判断触碰,就是判断活动中模型<将要移动到的位置>是否已经存在被固定的模型(块元素)了//如果存在返回true,表示将要移动到的位置会发送触碰,反之返回falsefor (var k in model) {var blockModel = model[k];//该位置是否已经存在块元素?if (fixedBlocks[(y + blockModel.row) + "_" + (x + blockModel.col)]){return true;}}return false;}//判断一行是否被铺满function isRemoveLine(){//在一行中,每一列都有块元素,那么该行就需要被清理了//遍历所有行中的所有列//遍历所有行for (var i = 0;i < ROW_COUNT;i++){//标记符,假设当前行已经被铺满了var flag = 1;//遍历当前行中的所有列for (var j = 0; j < COL_COUNT; j++){//如果当前行中有一列没有数据,那就说明没有被铺满if (!fixedBlocks[i + "_" + j]){flag = 0;break;}}if (flag){//该行已经被铺满了,删除改行removeLine(i);}}}//清理被铺满的一行function removeLine(line){//遍历该行中的所有列for (var i = 0; i < COL_COUNT; i++){//1.删除该行中所有的块元素document.querySelector("#container").removeChild(fixedBlocks[line + "_" + i]);//2.删除该行中所有块元素的数据源fixedBlocks[line + "_" + i] = null;}downLine(line);}//让被清理行之上的块元素下落function downLine(line){//遍历被清理行之上的所有行for (var i = line - 1; i >= 0; i--){//该行中的所有列for (var j = 0; j < COL_COUNT; j++){//不存在数据跳过一次循环if (!fixedBlocks[i + "_" + j]) { continue; }//存在数据//1.被清理行之上所在块元素数据源所在的行数 + 1fixedBlocks[(i+1) + "_" + j] = fixedBlocks[i + "_" + j];//2.让容器的位置下落fixedBlocks[(i+1) + "_" + j].style.top = (i+1) * STEP + "px";//3.清理掉之前的块元素fixedBlocks[i + "_" + j] = null;}}}//让模型自动下落function autoDown(){if (myInterval){clearInterval(myInterval);}myInterval = setInterval(function(){move(0,1);},300);}//判断游戏结束function isGameOver(){for (var i = 0;i < COL_COUNT; i++){if (fixedBlocks["0_" + i]) { return true; }}return false;}//结束游戏function gameOver(){//1.停止计时器if(myInterval){clearInterval(myInterval);}//2.弹出对话框alert("恭喜您,游戏结束啦!");}</script></body></html>
