广告小游戏我忍你很久了 之 《花园灌溉》

1,470 阅读10分钟

前言

喜欢刷视频应该都见过这样的广告吧

微信图片_20230418015537.jpg

是不是很多这种广告里面的人,游戏玩的跟*一样。

没错,俺也一样(我也这么觉得)。

所以今天就解决这个广告小游戏。

一、游戏分析

  1. 从上面游戏截图中可以看出来,游戏是由多个块组成,并且每个块分成九个小块。小块中的元素,可以是路、水渠、水、水源头、小树、大树。
  2. 并且玩家可以根据点击块来进行旋转,让块与块之间的水渠能形成一条水路,来进行树木的灌溉。
  3. 当所有树木全部都长大,即可通关。
  4. 游戏要点:让块旋转,旋转后能自动找到一条水路,并且水路必须连接源头才能有水,不然会变得干涸。

二、游戏布局

游戏虽然看起来是个三维数组,但是我是硬用了二维数组来解决的

image.png

首先是布局方面

<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();
    }
}

示例:

image.png

image.png

(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)判断两个土地是否有水连接

image.png

我们只需要知道当前土地的下标,和需要判断连接的方位即可。

比如上图,我们要判断第三块土地是否与第四块土地相连,只要传入 下标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)清除不能连到水源头的土地中的水

以下这种情况,就是没及时清除多余的水渠

image.png

如果水源头没连接任何土地,则除了水源头,所有土地都得清理水渠。下面代码中会给出操作。 如果某一块没有与任何一块有连接,则清除自身里面的水

这里也是整个游戏的难点,这个难点就是我们如何知道,某一块是否连接到水源了???或者说哪些土地是跟水源连在一起的???

image.png

这里我用的方法是,先获取所有存在水的土地,然后获取每个存在水的土地的下标,及与它相邻并且连接水的土地的下标,最后我们会得到一个二维数组。(源头因为必须存在,所以不用遍历) 比如上面那个图,就会得到一个二维数组:

[
  [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
    }
}

image.png

总结

这个小游戏,刚开始看起来以为很简单,做起来是真的累人。但是整个游戏做下来感觉还是不错的,很爽。其中碰到的问题、难点,要么是睡觉在梦里解决的,要么就是下班路上想到办法解决的,之前做其他游戏也是这种,就很奇妙。

后面还会做其他的广告小游戏,只能说竭尽所能吧,做自己喜欢的事,永远不会觉得无聊。

喜欢小游戏的可以看我其他小游戏文章。

谢谢观看!

最后完整代码地址: [zml-game-advert: 《广告小游戏合集》,比如抖音里面的一些广告小游戏,微信朋友圈内的一些广告小游戏等。 进行中... (gitee.com)](zml-game-center: 游戏中心 —— 制作的小游戏合集,供学习、娱乐、摸鱼使用。 其中包含《贪吃蛇》《吃豆人》《俄罗斯方块》等 (gitee.com))