详解vue.js实现贪吃蛇游戏

贪吃蛇是一款经典的h5小游戏,对于许多入门js的同学来说这是一个很好的练手项目,可以锻炼思维并且更熟悉js与视图层的交互。网上普遍的做法都是用原生js实现的,很少有用框架来实现的。所以我试着用vue.js做了出来,在难度和代码量上确实比我之前用原生js做的小了点,下面讲解我的每一步思路和步骤:

Step 1

首先先创建棋盘
详解vue.js实现贪吃蛇游戏_第1张图片
先创建一个大的容器,然后每行20格,共20行填上小格子。这里稍微计算一下高宽,用两层for循环实现。因为之后要频繁对dom结点操作,所以用ref钩住,调用方式为this.$refs.grid[index]

html

<div class="board">
   <div v-for="(i,index) in 20" :key="index">
     <div class="commonGrid" v-for="(j,index) in 20" ref="grid" :key="index">
     </div>
   </div>
</div>
.board {
  width: 300px;
  height: 300px;
  flex-flow: row;
  border: 1px solid black;
}
.commonGrid {
  float: left;
  width: 14px;
  height: 14px;
  border: 0.5px solid rgb(214, 125, 125);
}
.snakeGrid {
  background-color: red;
}
.eggGrid {
  background-color: yellow;
}

Step2

好了,有了棋盘,我们要把蛇和蛋放进去了吧。思路是定义一个数组,每个对象包含一个格子的行、列,然后再把蛇渲染到棋盘上。还需要分辨一下头尾。下标为0的对象是头,最后一个为尾,一进入页面就默认生成一条蛇,蛋也是如此。
在data里定义全局变量 snake: [],egg:{}

  created() {
    //初始化蛇身,下标第一位是头,最后一位是尾
    for (let i = 4; i > 0; i--)
      this.snake = [
        ...this.snake,
        {
          row: 2,
          column: i + 1
        }
      ];
  },
  mounted() {
    this.mountSnake()
    this.createEgg()
  }

在methods里定义渲染蛇和蛋的函数

mountSnake() {
 //forEach循环,改变对应格子颜色
   this.snake.forEach(body => {
    // 由于refs是一维数组,所以要转换一下:行数*20 + 列数就是对应的格子
   this.$refs.grid[body.row * 20 + body.column].setAttribute(
      "class",
      "commonGrid snakeGrid"
    );
  });
},
createEgg(){
  let randrow = null
  let randcolumn = null
  while(true) {
	  randrow = Math.floor(Math.random()*20) //生成0-19的随机数
	  randcolumn = Math.floor(Math.random()*20) 
	  // 不能生长在蛇的身上
	  if(this.notInSnake(randrow, randcolumn))
	     break
   }
   this.egg = {
   	  row: randrow,
   	  column: randcolumn
   }

   this.$refs.grid[randrow * 20 + randcolumn].setAttribute("class", "commonGrid eggGrid")
},
notInSnake(row,column) {
      let flag = false 
      this.snake.forEach(item => {
        if(item.row != row && item.column != column) {
          flag = true
        }
      })
      return flag
    },

可以看到棋盘上躺着一条蛇啦和一个蛋啦

Step3

第三步是最难的一步。要让蛇动起来,首先要有方向,然后设定定时器,每次循环判断蛇的方向,然后往该方向前移一格。而改变方向需要一个间隔很短的定时器,以判断玩家是否按键盘改变方向了。
data

data() {
    return {
      pauseButton: false,
      snake: [],
      dir: 'right', // 初始化方向
      moveTimer: null,
      dirTimer: null,
    };
  },

methods
timer会返回一个计时器对象的id值,可以用来暂停时清除定时器。

  start() {
  // 点击'开始游戏'的事件处理
      this.pauseButton = true;	// 暂停
      this.moveTimer = this.setTimer()
      this.dirTimer = this.changeDir()
    },

这里判断方向需要一些逻辑思维,在浏览器中按f12查看每个格子的信息。可以发现行数是从上往下递增,列数是从左到右递增的。
所以每次判断 当方向往右时,创建一个新对象,行数为头部的行数,列数为头部的列数+1,放入snake[0],再取出尾部。以此类推。详见代码

    setTimer() {
      return setInterval(() => {
        let newRow = null;
        let newColumn = null
        switch (this.dir) {
          case "right":
            newRow = this.snake[0].row;
            newColumn = this.snake[0].column + 1;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            });
            break;
          case "up":
            newRow = this.snake[0].row - 1;
            newColumn = this.snake[0].column;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break;
          case "left":
            newRow = this.snake[0].row;
            newColumn = this.snake[0].column - 1;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break;
          case "down":
            newRow = this.snake[0].row + 1;
            newColumn = this.snake[0].column;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break
        }
        if(!this.judgeEgg()) //判断是否吃到蛋
        {
          const delItem = this.snake.pop()
          this.$refs.grid[delItem.row * 20 + delItem.column].setAttribute(
            "class",
            "commonGrid"
          )
        }
        this.judgeFail()
        this.mountSnake()
      }, 200)
    },
     changeDir() {
      return setInterval(() => {
        document.onkeydown = event => {
          const e =
            event || window.event || arguments.callee.caller.arguments[0];
          if (e && e.keyCode == 37 && this.dir != 'right') {
            // 按 左
            this.dir = "left"
          } else if (e && e.keyCode == 38 && this.dir != 'down') {
            // 按 上键
            this.dir = "up"
          } else if (e && e.keyCode == 39 && this.dir != 'left') {
            // 按 右键
            this.dir = "right"
          } else if (e && e.keyCode == 40 && this.dir != 'up') {
            //按 下键
            this.dir = "down"
          }
          this.mountSnake()
        };
      }, 10);
    },

可以看到,蛇已经会动了
详解vue.js实现贪吃蛇游戏_第2张图片

Step4

接下来要实现吃蛋功能。在setTimer()可以看到,if(!this.judgeEgg())语句就是判断蛇的头部是否碰到蛋了,如果碰到了,尾巴就不用减去了,这样就实现了长度+1,并且要重新生成一个蛋

judgeEgg() {
      let flag = false
      this.snake.forEach( body => {
        if(body.row === this.egg.row && body.column === this.egg.column) {
          flag = true
          this.createEgg()
          this.score ++ 
        }
      })
      return flag
    },

详解vue.js实现贪吃蛇游戏_第3张图片

Step5

离成功之差一步了!就是要判断失败条件,也就是撞到自己或者墙壁就算输。

 judgeFail() {
      // 判断是否吃到自己,只需判断新加入的那一块格子是否与身体重叠
      for (let i = 1; i < this.snake.length ; i++){
        if(this.snake[0].row === this.snake[i].row 
        && this.snake[0].column === this.snake[i].column) {
          this.fail()
          return 
        }
      }
      // 判断是否撞到墙,只需判断新加入的那一块格子是否越界
      if (this.snake[0].row > 19 
      || this.snake[0].row < 0 
      || this.snake[0].column > 19 
      || this.snake[0].column < 0)
        this.fail()
 },
 fail() {
      this.pauseButton = false
      clearInterval(this.moveTimer)
      clearInterval(this.dirTimer)
      this.failFlag = true
 },

finally

按照前五步的思路走,就可以实现贪吃蛇小游戏了!
最后就是把样式优化一下,增加点功能 如计算得分、开始/暂停、失败弹框、调整速度等。
总的来说难度中等,考验了对js和vue的掌握程度。涉及到对es6、计时器、对象、生命周期等知识的理解和运用。还是非常不错的练手项目。
下面我把所有代码贴出来

<template>
  <div class="container">
    <div class="info" v-if="!failFlag">
      <button @click="start" v-if="!pauseButton">开始游戏</button>
      <button @click="pause" v-else>暂停</button>
      <span>长度: {{score}}</span>
    </div>
    <div class="board" v-if="!failFlag">
      <div v-for="(i,index) in 20" :key="index">
        <div class="commonGrid" v-for="(j,index) in 20" ref="grid" :key="index"></div>
      </div>
    </div>
    <div v-else class="failBox"> 
      <img src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=2872222233,2453972241&fm=26&gp=0.jpg'/>
    <button @click="again">再来一次</button>
    </div>
  </div>
</template>

<script>
import { Message } from "element-ui";
import { setInterval, clearInterval } from "timers";

export default {
  data() {
    return {
      pauseButton: false,
      //二维数组
      snake: [],
      egg: {
        row: Number,
        column: Number
      },
      score: 4,
      //蛇前进的方向
      dir: String,
      moveTimer: null,
      dirTimer: null,
      failFlag: false
    };
  },
  created() {
    //初始化蛇身,下标第一位是头,最后一位是尾
    for (let i = 4; i > 0; i--)
      this.snake = [
        ...this.snake,
        {
          row: 2,
          column: i + 1
        }
      ];
    this.dir = "right"
  },
  mounted() {
    this.mountSnake()
    this.createEgg()
  },
  methods: {
    mountSnake() {
      this.snake.forEach(body => {
        this.$refs.grid[body.row * 20 + body.column].setAttribute(
          "class",
          "commonGrid snakeGrid"
        );
      });
    },

    start() {
      this.pauseButton = true;
      this.moveTimer = this.setTimer()
      this.dirTimer = this.changeDir()
    },
    setTimer() {
      return setInterval(() => {
        let newRow = null;
        let newColumn = null
        switch (this.dir) {
          case "right":
            newRow = this.snake[0].row;
            newColumn = this.snake[0].column + 1;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            });
            break;
          case "up":
            newRow = this.snake[0].row - 1;
            newColumn = this.snake[0].column;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break;
          case "left":
            newRow = this.snake[0].row;
            newColumn = this.snake[0].column - 1;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break;
          case "down":
            newRow = this.snake[0].row + 1;
            newColumn = this.snake[0].column;
            this.snake.unshift({
              row: newRow,
              column: newColumn
            })
            break
        }
        if(!this.judgeEgg()) //判断是否吃到蛋
        {
          const delItem = this.snake.pop()
          this.$refs.grid[delItem.row * 20 + delItem.column].setAttribute(
            "class",
            "commonGrid"
          )
        }
        this.judgeFail()
        this.mountSnake()
      }, 200)
    },
    changeDir() {
      return setInterval(() => {
        document.onkeydown = event => {
          const e =
            event || window.event || arguments.callee.caller.arguments[0];
          if (e && e.keyCode == 37 && this.dir != 'right') {
            // 按 左
            this.dir = "left"
          } else if (e && e.keyCode == 38 && this.dir != 'down') {
            // 按 上键
            this.dir = "up"
          } else if (e && e.keyCode == 39 && this.dir != 'left') {
            // 按 右键
            this.dir = "right"
          } else if (e && e.keyCode == 40 && this.dir != 'up') {
            //按 下键
            this.dir = "down"
          }
          this.mountSnake()
        };
      }, 10);
    },
    judgeEgg() {
      let flag = false
      this.snake.forEach( body => {
        if(body.row === this.egg.row && body.column === this.egg.column) {
          flag = true
          this.createEgg()
          this.score ++ 
        }
      })
      return flag
    },
    judgeFail() {
      // 判断是否吃到自己,只需判断新加入的那一块格子是否与身体重叠
      for (let i = 1; i < this.snake.length ; i++){
        if(this.snake[0].row === this.snake[i].row 
        && this.snake[0].column === this.snake[i].column) {
          this.fail()
          return 
        }
      }
      // 判断是否撞到墙,只需判断新加入的那一块格子是否越界
      if (this.snake[0].row > 19 
      || this.snake[0].row < 0 
      || this.snake[0].column > 19 
      || this.snake[0].column < 0)
        this.fail()
    },
    notInSnake(row,column) {
      let flag = false 
      this.snake.forEach(item => {
        if(item.row != row && item.column != column) {
          flag = true
        }
      })
      return flag
    },
    createEgg(){
      let randrow = null
      let randcolumn = null
      while(true) {
        randrow = Math.floor(Math.random()*20) //生成0-19的随机数
        randcolumn = Math.floor(Math.random()*20) 
        if(this.notInSnake(randrow, randcolumn))
          break
      }
      this.egg = {
        row: randrow,
        column: randcolumn
      }
      console.log(this.egg)
      this.$refs.grid[randrow * 20 + randcolumn].setAttribute("class", "commonGrid eggGrid")
    },
    pause() {
      this.pauseButton = false
      clearInterval(this.moveTimer)
      clearInterval(this.dirTimer)
      this.moveTimer = null
      this.dirTimer = null
    },
    fail() {
      this.pauseButton = false
      clearInterval(this.moveTimer)
      clearInterval(this.dirTimer)
      this.failFlag = true
    },
    again() {
      location.reload()
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.container {
  height: 80vh;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.board {
  width: 300px;
  height: 300px;
  flex-direction: row;
  border: 1px solid black;
}

button {
  cursor: pointer;
  width: 80px;
  height: 30px;
  margin: 10px 20px;
}

.commonGrid {
  float: left;
  width: 14px;
  height: 14px;
  border: 0.5px solid rgb(214, 125, 125);
}

.snakeGrid {
  background-color: red;
}

.eggGrid {
  background-color: yellow;
}

.failBox {
  display: flex;
  flex-direction: column;
  justify-items: center;
  align-items: center;
}
</style>

你可能感兴趣的:(前端)