前言
喜欢刷视频应该都见过这样的广告吧
是不是很多这种广告里面的人,游戏玩的跟*一样。
没错,俺也一样(我也这么觉得)。
所以今天就解决这个广告小游戏。
一、游戏分析
- 从上面游戏截图中可以看出来,游戏是由多个块组成,并且每个块分成九个小块。小块中的元素,可以是路、水渠、水、水源头、小树、大树。
- 并且玩家可以根据点击块来进行旋转,让块与块之间的水渠能形成一条水路,来进行树木的灌溉。
- 当所有树木全部都长大,即可通关。
- 游戏要点:让块旋转,旋转后能自动找到一条水路,并且水路必须连接源头才能有水,不然会变得干涸。
二、游戏布局
游戏虽然看起来是个三维数组,但是我是硬用了二维数组来解决的
首先是布局方面
<div class="app">
<div class="game">
<div class="game-tr" :style="{'width': (col * 150 + 8) + 'px'}">
<div class="game-td" v-for="(item1, index1) in map" @click="onClick(index1)">
<div
class="game-block"
:class="{ 'bgWater': item2 == 3 || item2 == 5, 'bgSoil': item2 == 4}"
v-for="(item2, index2) in item1"
>
<img src="img/road.png" alt="" v-if="item2 == 0">
<img src="img/soil.png" alt="" v-if="item2 == 1">
<img src="img/water.gif" alt="" v-if="item2 == 2">
<img src="img/waterHead.png" alt="" v-if="item2 == 3" class="waterHead">
<img src="img/tree.png" alt="" v-if="item2 == 4 || item2 == 5" class="tree" :class="{'treeSmall': item2==4, 'treeBig': item2==5}">
</div>
</div>
<!-- 胜利弹框 -->
<div class="win" v-if="win">You Win!</div>
</div>
</div>
</div>
样式方面,这里我就不贴样式了,可在文章末尾获取完整代码。
三、游戏基本参数配置
(1)游戏数据
// 0=路 1=水渠 2=水 3=水源头 4=小树 5=大树
const data = [
[0,0,0, 0,3,1, 0,0,0],
[0,1,0, 0,1,1, 0,1,0],
[0,1,0, 0,1,0, 0,1,0],
[0,1,0, 1,1,0, 0,0,0],
[0,0,0, 0,4,1, 0,1,0],
[0,0,0, 0,1,1, 0,1,0],
[0,0,0, 0,4,0, 0,1,0],
[0,1,0, 0,1,0, 0,1,0],
[0,1,0, 0,1,1, 0,1,0],
[0,1,0, 0,1,0, 0,1,0],
[0,1,0, 1,1,0, 0,1,0],
[0,1,0, 0,1,1, 0,0,0],
[0,0,0, 0,1,1, 0,1,0],
[0,0,0, 1,4,0, 0,0,0],
[0,1,0, 1,1,0, 0,0,0],
[0,1,0, 0,4,0, 0,0,0],
];
(2)游戏参数
map: data, // 主数据
domList: [], // 每个大块地的dom信息
col: 4, // 游戏一行存放多少个大块地
tree: 0, // 游戏中有多少颗树
win: false, // 是否赢了
四、游戏实现
(1)游戏初始化
获取每个大块地的dom信息、获取所有树苗
getInfo() {
this.domList = document.querySelectorAll('.game-td');
for(let i = 0; i < this.map.length; i++) {
if(this.map[i].includes(4)) {
this.tree++;
}
}
}
游戏初始化的时候我们需要写一个公共方法,就是渲染某个大块地中的水,我们可以知道的是,当大块中有一块水的时候,其他水渠都应该有水,所以默认水源在第一块的中间,所有水源旁边的水渠应该是有水的。
drawBlockWater() {
for(let i = 0; i < this.map.length; i++) {
let list = this.map[i];
let bool = list.some(e => e == 2 || e == 3);
if(bool) {
for(let j = 0; j < list.length; j++) {
if(list[j] == 1) {
this.map[i][j] = 2;
}
if(list[j] == 4) {
this.map[i][j] = 5;
}
}
}
}
this.$forceUpdate();
this.clearBlockWater();
this.isWin();
}
每次渲染完所有的块时,都要判断是否有没连接到水源的土地,如果没连接到,则需要把当前地的水抽完。这个后面会说。 还要判断是否赢了!
(2)点击砖块旋转
这时候上面获取dom信息就用的上了,我们点击某个砖块,只要改变它的transform:rotate值就可以了,每点击一次,旋转+90
onClick(index) {
let theDom = this.domList[index];
let rotateDegree = theDom.style.transform.replace(/[^0-9]/ig, "") * 1 || 0;
rotateDegree += 90;
theDom.style.transform = `rotate(${ rotateDegree }deg)`;
this.drawAllBlock();
}
(3)渲染周围土地中的水
如果自身土地周围有存在水,并且还连接到自己了,则自己也会注水。
需要说一下的就是,每块的连接点,其实就是上下左右的位置,我们也只需判断第1、3、5、7块即可。
drawAllBlock() {
// 是否存在连接的土地,如果有,则递归再次遍历,直到渲染完所有的连接土地
let bool = false;
for(let i = 0; i < this.map.length; i++) {
// 我们只要查找每个大块中 第 1、3、5、7块的周围是否相连即可——————1=上 3=左 5=右 7=下
let list = this.map[i];
let arr = [];
// 判断这四个地方是否存在水渠
if(list[1] == 1) { arr.push(1) };
if(list[3] == 1) { arr.push(3) };
if(list[5] == 1) { arr.push(5) };
if(list[7] == 1) { arr.push(7) };
// 遍历所有水渠
for(let a = 0; a < arr.length; a++) {
let aVal = arr[a];
// 根据水渠的下标志,来判断当前水渠点处在上下左右什么方位上,而getDir就是获取方位用的
let dir = this.getDir(i, aVal);
// 判断当前水渠点,在邻居土地上是否相连,如果相连,则把自己的水渠注水
if(this.isConnectNeighbor(i, dir).isTrue) {
this.map[i][aVal] = 2;
bool = true;
}
}
}
// 给当前土地注满水
this.drawBlockWater();
// 如果有,则继续遍历一遍,因为每遍历一遍,都有可能有新的土地注满水
if(bool) {
this.drawAllBlock();
}
}
示例:
(4)获取方位
根据大块坐标及小块坐标,来获取小块所处的方位
这里我们需要知道的是,我们旋转某个土地的时候,他们的小块也会旋转。比如原本默认土地中第2个小块,是在上方的,当我们点击之后,大块旋转90deg,此时第2个小块的方向就在右边了,所以,我们这个方法就是获取某个小块的方位,因为我们会根据方位来进行判断水是否连接
// index=大块的坐标 indexSmall=小块的下标(1, 3, 5, 7)
getDir(index, indexSmall) {
let currentBlockRotate = this.domList[index].style.transform.replace(/[^0-9]/ig, "") * 1 || 0;
let rotateNum = (currentBlockRotate / 90) % 4;
let dir = null;
if(rotateNum == 0) {
if(indexSmall == 1) { dir = 'up' };
if(indexSmall == 3) { dir = 'left' };
if(indexSmall == 5) { dir = 'right' };
if(indexSmall == 7) { dir = 'down' };
}
if(rotateNum == 1) {
if(indexSmall == 1) { dir = 'right' };
if(indexSmall == 3) { dir = 'up' };
if(indexSmall == 5) { dir = 'down' };
if(indexSmall == 7) { dir = 'left' };
}
if(rotateNum == 2) {
if(indexSmall == 1) { dir = 'down' };
if(indexSmall == 3) { dir = 'right' };
if(indexSmall == 5) { dir = 'left' };
if(indexSmall == 7) { dir = 'up' };
}
if(rotateNum == 3) {
if(indexSmall == 1) { dir = 'left' };
if(indexSmall == 3) { dir = 'down' };
if(indexSmall == 5) { dir = 'up' };
if(indexSmall == 7) { dir = 'right' };
}
return dir;
}
(5)获取某一块地中所有的渠道
因为土地不同,里面的水渠都各不相同,这个方法就是获取某个土地的水渠位置
// index = 大块地的下标
getBlockSewer(index) {
let list = this.map[index];
let arr = [];
if(list[1] != 0) { arr.push(1) };
if(list[3] != 0) { arr.push(3) };
if(list[5] != 0) { arr.push(5) };
if(list[7] != 0) { arr.push(7) };
return arr;
}
(6)判断两个土地是否有水连接
我们只需要知道当前土地的下标,和需要判断连接的方位即可。
比如上图,我们要判断第三块土地是否与第四块土地相连,只要传入 下标2 和方位 right即可得出。
这里需要注意的是,我们土地用的是一个一维数组,所以我们需要判断某个土地的上下左右是否存在土地时,我们需要进行一维数组的加减。
比如第2块土地的下面那个块,就是下标 1 + 每行多少个土地数。
还比如第四块土地的右边的一块,不能3 + 1,而是右边没有土地了,所以就得需要判断了(currentIndex + 1) % this.col == 0
isConnectNeighbor(currentIndex, dir) {
// 左右需要判断两次,因为大块地数组是一个一维数组,需要判断左右的临界值
if(
(!dir) ||
(dir == 'up' && currentIndex - this.col < 0) ||
(dir == 'down' && currentIndex + this.col > this.map.length - 1) ||
(dir == 'left' && currentIndex - 1 < 0) ||
(dir == 'left' && currentIndex % this.col == 0) ||
(dir == 'right' && currentIndex + 1 > this.map.length - 1) ||
(dir == 'right' && (currentIndex + 1) % this.col == 0)
) {
return false;
}
let bool = false;
// 获取当前大块A的上面大块B,判断B大块中是否有水,有水的话,说明全部空地都有水,只要判断有水的有没有在下方的
let dirBlockIndex = null;
switch(dir) {
case 'up': dirBlockIndex = currentIndex - this.col; break;
case 'down': dirBlockIndex = currentIndex + this.col; break;
case 'left': dirBlockIndex = currentIndex - 1; break;
case 'right': dirBlockIndex = currentIndex + 1; break;
}
const arr = this.map[dirBlockIndex];
// 获取当前土地的旋转度数
let currentBlockRotate = this.domList[dirBlockIndex].style.transform.replace(/[^0-9]/ig, "") * 1 || 0;
// 判断用户点击旋转了几次,因为再怎么旋转也只有四个方位
let rotateNum = (currentBlockRotate / 90) % 4;
// 上下左右的大块中,必须有水才可以进行下一步判断
if(arr.includes(2)) {
// 这里判断下面都一样,
// 比如我要查看上方那块土地有没有跟我相连,我只要判断它的水渠有没有哪个口是朝下的,就正好跟我的口朝上的对上了,即可。
if(dir == 'up') {
if((arr[1] == 2 && rotateNum == 2) || (arr[3] == 2 && rotateNum == 3) || (arr[5] == 2 && rotateNum == 1) || (arr[7] == 2 && rotateNum == 0)) {
bool = true;
}
}
if(dir == 'left') {
if((arr[1] == 2 && rotateNum == 1) || (arr[3] == 2 && rotateNum == 2) || (arr[5] == 2 && rotateNum == 0) || (arr[7] == 2 && rotateNum == 3)) {
bool = true;
}
}
if(dir == 'right') {
if((arr[1] == 2 && rotateNum == 3) || (arr[3] == 2 && rotateNum == 0) || (arr[5] == 2 && rotateNum == 2) || (arr[7] == 2 && rotateNum == 1)) {
bool = true;
}
}
if(dir == 'down') {
if((arr[1] == 2 && rotateNum == 0) || (arr[3] == 2 && rotateNum == 1) || (arr[5] == 2 && rotateNum == 3) || (arr[7] == 2 && rotateNum == 2)) {
bool = true;
}
}
}
// 返回是否连接成功,和连接成功的对方土地的下标值
return {
isTrue: bool,
index: dirBlockIndex
};
}
(7)清除不能连到水源头的土地中的水
以下这种情况,就是没及时清除多余的水渠
如果水源头没连接任何土地,则除了水源头,所有土地都得清理水渠。下面代码中会给出操作。 如果某一块没有与任何一块有连接,则清除自身里面的水
这里也是整个游戏的难点,这个难点就是我们如何知道,某一块是否连接到水源了???或者说哪些土地是跟水源连在一起的???
这里我用的方法是,先获取所有存在水的土地,然后获取每个存在水的土地的下标,及与它相邻并且连接水的土地的下标,最后我们会得到一个二维数组。(源头因为必须存在,所以不用遍历) 比如上面那个图,就会得到一个二维数组:
[
[1, 0, 2], // 第二个土地有两个跟它相连
[2, 1, 3], // 以此类推
[3, 2, 7],
[4, 8],
[5, 9],
[7, 3, 11],
[8, 4],
[9, 5],
[11, 7]
]
我们根据这个二维数据,分成一个一维数组(默认二维数组中的第一项)arr1,一个二维数组arr2。
arr1 = [1, 0, 2]; // 找出存在源头的土地,成为一维数组的默认值
arr2 = [[2, 1, 3], [3, 2, 7], ...]; // 剩余
根据图中可以看出,跟源头连成一条水渠的下标是 0,1,2,3,7,11。
所以找规律,只要arr2中存在arr1的元素,则就把arr2中的某一项给拼接到arr1中,然后把这一项从arr2中删除, 比如arr1中有2,则就需要把arr2中的[2, 1, 3], [3, 2, 7]给拼接到arr1中,...,以此类推。最后可以得出,一个没去重的一维数组。其实这个一维数组去重之后,就是我们要找的一条完整的水渠。arr2中剩下的,则是没连到水源的土地,我们就得把这些土地中的水都抽完即可。
首先先说明,上面那个处理arr1和arr2的小算法。
// a=一维数组 b=二维数组
// 如果b中某一数组元素包含数组 a 中的任意一个值,则将该元素合并到数组 a 中,并从数组 b 中删除该元素
handleArr(a, b) {
while (true) {
let foundMatch = false;
for (let i = 0; i < b.length; i++) {
if (a.some(x => b[i].includes(x))) {
a = a.concat(b[i]);
b.splice(i, 1);
foundMatch = true;
break;
}
}
if (!foundMatch) {
break;
}
}
return [a, b];
}
最后就是这一步的完整代码:
clearBlockWater() {
// 获取水源头数组和下标值
let waterHeadList = [];
let waterHeadIndex = null;
for(let i = 0; i < this.map.length; i++) {
if(this.map[i].includes(3)) {
waterHeadList = this.map[i];
waterHeadIndex = i;
break;
}
}
// 出水口的下标值
let waterHeadOut = waterHeadList.findIndex(item => item == 2);
// 如果水源地没跟任何方块连接,那就得把其他格子中的水都清除
if(!this.isConnectNeighbor(waterHeadIndex, this.getDir(waterHeadIndex, waterHeadOut)).isTrue) {
for(let i = 1; i < this.map.length; i++) {
for(let j = 0; j < this.map[i].length; j++) {
if(this.map[i][j] == 2) {
this.map[i][j] = 1;
}
if(this.map[i][j] == 5) {
this.map[i][j] = 4;
}
}
}
this.$forceUpdate();
} else {
// 获取所有有水的土地
let havaWaterList = [];
for(let i = 0; i < this.map.length; i++) {
if(this.map[i].includes(2) && !this.map[i].includes(3)) {
havaWaterList.push(i)
}
}
// 存储每块有水的土地自己的下标,和相邻连接的土地的下标
if(havaWaterList.length) {
const neighborList = [];
for(let i = 0; i < havaWaterList.length; i++) {
let index = havaWaterList[i];
const listSewer = this.getBlockSewer(index); // 获取当前土地所有的进出水口
neighborList[i] = [];
neighborList[i].push(index); // 保存自己的下标
for(let j = 0; j < listSewer.length; j++) {
if(this.isConnectNeighbor(index, this.getDir(index, listSewer[j])).isTrue) {
// 保存相连的邻居土地的下标
neighborList[i].push(
this.isConnectNeighbor(index, this.getDir(index, listSewer[j])).index
);
}
}
}
// 根据拿到的自身与相邻的大块土地的坐标,我们就要根据二维数组中,看看是否能连接到水源,不能连接的,都必须抽水
const aindex = neighborList.findIndex(item => item.includes(0)); // 拿到水源在neighborList中的下标
const alist = neighborList.splice(aindex, 1)[0]; // 默认水源头为第一个默认值
// 这时候就能拿到 完整路线和 不能连接水源头的路线
const [arr1, arr2] = this.handleArr(alist, neighborList);
// 处理不能连接水源的路线,变成一维数组再去重
const noConnectList = [...new Set(arr2.flat())];
for (let i = 0; i < noConnectList.length; i++) {
let blockIndex = noConnectList[i];
let blocklist = this.map[blockIndex];
for (let j = 0; j < blocklist.length; j++) {
if(this.map[blockIndex][j] == 2) {
this.map[blockIndex][j] = 1;
}
if(this.map[blockIndex][j] == 5) {
this.map[blockIndex][j] = 4;
}
}
}
this.$forceUpdate();
}
}
}
(8) 判断输赢
这个简单,树苗=4,浇完水=5,只要获取地图中等于5土地有多少个,再去跟树苗数比较。
isWin() {
let num = 0;
for(let i = 0; i < this.map.length; i++) {
if(this.map[i].includes(5)) {
num++;
}
}
if(num === this.tree) {
this.win = true
}
}
总结
这个小游戏,刚开始看起来以为很简单,做起来是真的累人。但是整个游戏做下来感觉还是不错的,很爽。其中碰到的问题、难点,要么是睡觉在梦里解决的,要么就是下班路上想到办法解决的,之前做其他游戏也是这种,就很奇妙。
后面还会做其他的广告小游戏,只能说竭尽所能吧,做自己喜欢的事,永远不会觉得无聊。
喜欢小游戏的可以看我其他小游戏文章。
谢谢观看!
最后完整代码地址: [zml-game-advert: 《广告小游戏合集》,比如抖音里面的一些广告小游戏,微信朋友圈内的一些广告小游戏等。 进行中... (gitee.com)](zml-game-center: 游戏中心 —— 制作的小游戏合集,供学习、娱乐、摸鱼使用。 其中包含《贪吃蛇》《吃豆人》《俄罗斯方块》等 (gitee.com))