目录
一、效果展示
二、代码分享
三、原理分析
3.1、界面搭建
3.2、方块创建
3.3、方块旋转
3.4、方块移动
3.5、移动判断
3.6、下落判断与清除
3.7、得分计算
一、效果展示
二、代码分享
<template><div class="game"><div class="game-div"><div class="game-min"><div class="row" v-for="(row, i) in frame" :key="i"><pclass="element"v-for="(col, j) in row":key="j":style="{ background: col.bg }"></p></div></div><!-- 小屏幕 --><div class="right-div"><div class="ass"><div class="row" v-for="(row, i) in ass" :key="i"><pclass="element"v-for="(col, j) in row":key="j":style="{ background: col.bg }"></p></div></div><!-- 分数计算 --><div class="score-div"><div><p><span>得分:</span> <span style="color: red">{{score}}</span></p></div><div><p><span>等级:</span> <span style="color: red">{{level}}</span></p></div><div><p><span>消除:</span> <span style="color: red">{{times}}</span></p></div><div class="ztks" @click="stopGame">暂停/开始</div></div></div></div><!-- 控制台 --><div class="control"><p @click="change1">变换</p><div class="control-center"><div @click="moveLeft">向左</div><div @click="moveRight">向右</div></div><p @click="moveDown">向下</p></div></div>
</template><script>
import { color, blockMod, transition } from "@/utils/ykdata.js";
export default {data() {return {row: 20, //行col: 10, //列frame: [], //界面ass: [], //副屏幕bg: "#eee",block: [], //基本方块集合now: { b: 0, c: 0 }, //当前方块以及其旋转角度next: { b: 0, c: 0 }, //下一个方块以及其旋转角度nowBlock: [], //当前形状数据nextBlock: [], //下一个形状数据xz: 0, //当前旋转角度timer: "", //自动下落speed: 800, //速度score: 0, //得分level: 1, //等级times: 0, //消除次数stop: true, //是否停止removeRow: [], //消除的行记录};},mounted() {this.gameFrame();this.getBlock(0);this.getNext();this.init();},methods: {// 游戏框架gameFrame() {//主屏幕for (let i = 0; i < this.row; i++) {let a = [];for (let j = 0; j < this.col; j++) {let b = {data: 0,bg: this.bg,};a.push(b);}this.frame.push(a);}//副屏幕for (let i = 0; i < 4; i++) {let a = [];for (let j = 0; j < 4; j++) {let b = {data: 0,bg: this.bg,};a.push(b);}this.ass.push(a);}// 模拟格子被占用// this.frame[4][4].bg = "#00aaee";// this.frame[4][4].data = 1;},// 渲染方块getBlock(e) {this.block = blockMod(color[e]);},// 下一个形状async getNext() {// 随机获取形状this.next.b = Math.floor(Math.random() * this.block.length);this.next.c = Math.floor(Math.random() * 4);},// 渲染当前形状init() {// 获取到下一个形状数据this.now = JSON.parse(JSON.stringify(this.next));this.xz = this.now.c;// 当前形状数据this.nowBlock = JSON.parse(JSON.stringify(this.block[this.now.b]));// 渲染形状数据// let c = this.nowBlock.site;// for (let i = 0; i < c.length; i += 2) {// this.frame[c[i]][c[i + 1]].bg = this.nowBlock.color;// }this.renderBlock(this.nowBlock, this.frame, 1);// 旋转if (this.now.c > 0) {for (let i = 0; i < this.now.c; i++) {this.change(this.nowBlock, this.frame, this.now, i);}}this.getNext().then(() => {if (this.nextBlock.site) {this.renderBlock(this.nextBlock, this.ass, 0);}// 下一个形状this.nextBlock = JSON.parse(JSON.stringify(this.block[this.next.b]));this.renderBlock(this.nextBlock, this.ass, 1);// 旋转if (this.next.c > 0) {for (let i = 0; i < this.next.c; i++) {this.change(this.nextBlock, this.ass, this.next, i);}}});},// 渲染形状// b:方块,d:位置,n:0擦除,1生成,2确定落到最下层renderBlock(b, d, n) {let c = b.site;if (n == 0) {for (let i = 0; i < c.length; i += 2) {d[c[i]][c[i + 1]].bg = this.bg; //0擦除}} else if (n == 1) {for (let i = 0; i < c.length; i += 2) {d[c[i]][c[i + 1]].bg = b.color; //1生成}} else if (n == 2) {for (let i = 0; i < c.length; i += 2) {d[c[i]][c[i + 1]].data = 1; //2确定落到最下层}}},// 旋转 b:当前方块 d:渲染的位置 z:渲染的对象现在还是下一个 xz:当前旋转角度change(b, d, z, xz) {this.renderBlock(b, d, 0); //先清空再旋转// 记录第一块位置const x = b.site[0];const y = b.site[1];for (let i = 0; i < b.site.length; i += 2) {let a = b.site[i];b.site[i] = b.site[i + 1] - y + x + transition[z.b][xz].x;b.site[i + 1] = -(a - x) + y + transition[z.b][xz].y;}xz++;if (xz == 4) {xz = 0;}this.renderBlock(b, d, 1);},// 向下移动moveDown() {if (this.canMove(3)) {// 先清空this.renderBlock(this.nowBlock, this.frame, 0);for (let i = 0; i < this.nowBlock.site.length; i += 2) {// 向下移动一位this.nowBlock.site[i]++;}this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据} else {// 已经不能下落了this.renderBlock(this.nowBlock, this.frame, 2);// 判断是否可以消除this.removeBlock();}},// 向左移动moveLeft() {if (this.canMove(2)) {// 先清空this.renderBlock(this.nowBlock, this.frame, 0);for (let i = 0; i < this.nowBlock.site.length; i += 2) {// 向左移动一位this.nowBlock.site[i + 1]--;}this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据}},// 向右移动moveRight() {if (this.canMove(1)) {this.renderBlock(this.nowBlock, this.frame, 0);for (let i = 0; i < this.nowBlock.site.length; i += 2) {this.nowBlock.site[i + 1]++;}this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据}},// 是否可移动判断// 预判能否移动或变化 e:1右移 2左移 3下移 4变化canMove(e) {let c = this.nowBlock.site;let d = 0;switch (e) {case 1:for (let i = 0; i < c.length; i += 2) {if (c[i + 1] >= this.col - 1) {return false;}// 判断下一个位置是否被占用d += this.frame[c[i]][c[i + 1] + 1].data;}if (d > 0) {return false;}break;case 2:for (let i = 0; i < c.length; i += 2) {if (c[i + 1] <= 0) {return false;}// 判断下一个位置是否被占用d += this.frame[c[i]][c[i + 1] - 1].data;}if (d > 0) {return false;}break;case 3:for (let i = 0; i < c.length; i += 2) {if (c[i] >= this.row - 1) {return false;}// 判断下一个位置是否被占用(防穿透)d += this.frame[c[i] + 1][c[i + 1]].data;}if (d > 0) {return false;}break;// case 4:// for (let i = 0; i < c.length; i += 2) {// if (c[i + 1] >= this.col - 1) {// return false;// }// }// break;}return true;},// 下落时旋转// 旋转 b:当前方块 xz:当前旋转角度change1() {const b = JSON.parse(JSON.stringify(this.nowBlock));// 记录第一块位置const x = b.site[0];const y = b.site[1];let n = true;for (let i = 0; i < b.site.length; i += 2) {let a = b.site[i];b.site[i] = b.site[i + 1] - y + x + transition[this.now.b][this.xz].x;b.site[i + 1] = -(a - x) + y + transition[this.now.b][this.xz].y;// 判断旋转后该点是否合理if (b.site[i + 1] < 0 ||b.site[i + 1] >= this.col ||b.site[i] >= this.row ||this.frame[b.site[i]][b.site[i + 1]].data > 0) {n = false;}}// 符合条件if (n) {this.renderBlock(this.nowBlock, this.frame, 0); //先清空this.xz++;if (this.xz == 4) {this.xz = 0;}this.nowBlock = b;this.renderBlock(this.nowBlock, this.frame, 1); //再旋转}},// 到底部:确定位置不能再动,保证上面其他方块下落时不会将它穿透// 自动下落autoMoveDown() {this.timer = setInterval(() => {this.moveDown();}, this.speed);},// 开始与暂停stopGame() {this.stop = !this.stop;if (this.stop) {clearInterval(this.timer);} else {this.autoMoveDown();}},// 判断是否可以消除removeBlock() {// 遍历整个界面for (let i = 0; i < this.row; i++) {let a = 0;for (let j = 0; j < this.col; j++) {if (this.frame[i][j].data == 1) {a++;}}if (a == this.col) {// 说明该行已经占满可以消除this.removeRow.push(i);}}// 获取是否可以消除行if (this.removeRow.length > 0) {let l = this.removeRow;for (let i = 0; i < l.length; i++) {let j = 0;let timer = setInterval(() => {this.frame[l[i]][j] = { bg: this.bg, data: 0 };j++;if (j == this.col) {clearInterval(timer);}}, 20);}setTimeout(() => {// 上面方块下移,从下往上判断for (let i = this.row - 1; i >= 0; i--) {let a = 0;for (let j = 0; j < l.length; j++) {if (l[j] > i) {a++;}}if (a > 0) {for (let k = 0; k < this.col; k++) {if (this.frame[i][k].data == 1) {// 先向下移动this.frame[i + a][k] = this.frame[i][k];// 再清除当前this.frame[i][k] = { bg: this.bg, data: 0 };}}}}this.removeRow = []; //清除行记录// 生成下一个this.init();}, 20 * this.col);// 数据处理// 消除次数+1this.times++;// 等级向下取整+1let lev = Math.floor(this.times / 10) + 1;if (lev > this.level) {this.level = lev;// 速度if (this.level < 21) {// 20级以内做减法this.speed = 800 - (this.level - 1) * 40;} else {this.speed = 30;}// 清除当前下落clearInterval(this.timer);// 加速this.autoMoveDown();}this.level = this.times;// 分数消除一行100,两行300,三行600,四行1000this.score += ((l.length * (l.length + 1)) / 2) * 100 * this.level;} else {this.init();}},},
};
</script><style lang='less' scoped>
.game {.game-div {display: flex;.game-min {.row {display: flex;padding-top: 2px;.element {width: 20px;height: 20px;padding: 0;margin: 0 2px 0 0;}}}.right-div {padding-left: 20px;.ass {.row {display: flex;padding-top: 2px;.element {width: 20px;height: 20px;padding: 0;margin: 0 2px 0 0;}}}.score-div {div {height: 20px;line-height: 20px;}.ztks {width: 100px;height: 40px;margin-bottom: 10px;background-color: palevioletred;text-align: center;line-height: 40px;}}}}.control {width: 220px;p {width: 220px;height: 40px;text-align: center;line-height: 40px;background-color: #B940EF;margin-bottom: 20px;}.control-center {align-items: center;display: flex;justify-content: space-between;margin-bottom: 20px;div {width: 90px;height: 40px;text-align: center;line-height: 40px;background-color: #B940EF;}}}
}
</style>
工具函数:
//渐变色
export const color = [['linear-gradient(180deg, #FFA7EB 0%, #F026A8 100%)','linear-gradient(180deg, #DFA1FF 0%, #9A36F0 100%)','linear-gradient(180deg, #9EAAFF 0%, #3846F4 100%)','linear-gradient(180deg, #7BE7FF 2%, #1E85E2 100%)','linear-gradient(180deg, #89FED8 0%, #18C997 100%)','linear-gradient(180deg, #FFED48 0%, #FD9E16 100%)','linear-gradient(180deg, #FFBA8D 1%, #EB6423 100%)',],['#2B7AF5','#2B9DF5','#79CFFF','#1B67DD','#4F94FF','#2180F2','#3FD0FF',],
];//7种方块元素
export const blockMod = (color) => {let a = {site: [0, 1, 0, 2, 1, 2, 2, 2],color: color[0],};let b = {site: [0, 1, 1, 1, 1, 2, 2, 2],color: color[1],};let c = {site: [1, 1, 1, 2, 2, 1, 2, 2],color: color[2],};let d = {site: [1, 0, 1, 1, 1, 2, 1, 3],color: color[3],};let e = {site: [0, 2, 1, 1, 1, 2, 2, 1],color: color[4],};let f = {site: [0, 1, 0, 2, 1, 1, 2, 1],color: color[5],};let g = {site: [1, 1, 2, 0, 2, 1, 2, 2],color: color[6],};return ([a, b, c, d, e, f, g]);
};//旋转规则
export const transition = [[{x: 1, y: 1,}, {x: 1, y: 0,}, {x: 0, y: -2,}, {x: -2, y: 1,}],[{x: 1, y: 1,}, {x: 1, y: 0,}, {x: 0, y: -2,}, {x: -2, y: 1,}],[{x: 0, y: 1,}, {x: 1, y: 0,}, {x: 0, y: -1,}, {x: -1, y: 0,}],[{x: -1, y: 2,}, {x: 1, y: 1,}, {x: 2, y: -1,}, {x: -2, y: -2,}],[{x: 2, y: 0,}, {x: 0, y: -1,}, {x: -1, y: -1,}, {x: -1, y: 2,}],[{x: 1, y: 1,}, {x: 1, y: 0,}, {x: 0, y: -2,}, {x: -2, y: 1,}],[{x: 0, y: 0,}, {x: 1, y: 0,}, {x: -1, y: 0,}, {x: 0, y: 0,}],
]
三、原理分析
3.1、界面搭建
主界面的20X10,类似贪吃蛇,副界面的随机方块,则是4x4,都是双重for循环。初始化的时候调用gameFrame()即可。
3.2、方块创建
主要说明一下随机生成的方块,每个都是有4个小方格组成,组成了7种基本样式,在自身基础上进行四个方向的旋转,就是下落的所有可能性。参考坐标如右图所示:
3.3、方块旋转
旋转 b:当前方块 d:渲染的位置 z:渲染的对象现在还是下一个 xz:当前旋转角度,在change(b, d, z, xz)里进行旋转,这里用到了工具函数里的transition,旋转核心就是找到一个固定的点,看出x和Y坐标的变化,即(x=y,y=-x),加上工具函数里的blockMod,就可以依次生成对应的下落方块。
3.4、方块移动
在生成一个的同时,要考虑的下一个方块的生成与随机旋转,所以 b:方块,d:位置,n:0擦除,1生成,2确定落到最下层,在renderBlock(b, d, n)方法里进行形状的渲染。
我们有三个方向:moveDown()、moveLeft()、moveRight(),这里的原理和贪吃蛇基本类似,不过在向下移动时考虑因素较多,下落时旋转要考虑 b:当前方块 xz:当前旋转角度change1()方法,为了保持原来的形状,所以多处用到了深拷贝。
3.5、移动判断
预判能否移动或变化 e:1右移 2左移 3下移 4变化,在 canMove(e)方法里实现。主要是判断下一个位置是否被占用,到了底部:确定位置不能再动,保证上面其他方块下落时不会将它穿透,是要考虑的问题。
3.6、下落判断与清除
判断是否可以消除:遍历整个界面,当一行被占满,将该行的方块依次删去,并保留上面附着的方块位置不变,形成“被吞噬”的现象。在消掉满了的一行时,要让上面方块下移,从下往上判断,再清除当前方块,避免冲突。
3.7、得分计算
分数消除一行100,两行300,三行600,四行1000,消掉一行,带分数增加,以此同时,随着等级的增加,速度也会越来越快,难度增加。