用vue3做一个贪吃蛇

1,927 阅读4分钟

最近公司业务不忙,趁着摸鱼的时间学习一下vue3的新知识,顺便搞了个贪吃蛇,代码地址

贪吃蛇实现原理

游戏界面设置

考虑到现在已经是由数据驱动的年头了,我直接放弃直接操作dom的想法,而是改成操作数组的方式。 设置1个25×25方格的游戏界面(如下图)

image.png

  1. 方格数组存储信息为[从上到下的坐标位置,从左到右的坐标位置, 该方格颜色(0为白色,1为红色,2为绿色,3为黄色)]
  2. 将这所有的方格存储为一个二维数组,该二维数组就是当前的游戏界面(将该二维数组设置为响应式,这样才能及时展示每个方格的颜色)

游戏界面初始化如下

let checkerboardInfo = reactive([]); //响应式的引用数据类型设置,在vue3中得使用reactive()处理

//设置游戏界面
const setCheckerboard = () => {
  for (let i = 0; i < 25; i++) {
    for (let j = 0; j < 25; j++) {
      checkerboardInfo.push([i, j, 0]);
    }
  }
}
食物和蛇的设置

贪吃蛇初始化位置在游戏界面最中间位置(初始化只有蛇头,蛇头在方格中用红色背景表示,蛇身在方格中用绿色背景表示)

//蛇所有点坐标位置的二维数组
let snakeInfo = [];

//初始化(蛇始终在中心生成)
const setSnake = () => {
  snakeInfo.push([12, 12]);
  
  //蛇头使用坐标+1表示
  //蛇身使用坐标+2表示
  //改变该方格的背景颜色
  changeCheckerboard([12, 12], 1)
};
setSnake();

在贪吃蛇中,食物每次只会随机在地图上出现一个,在被蛇吃掉以后会再次随机在地图上生成一个新的食物。食物不能出蛇身上,也不能出现在游戏界面外(食物在方格中用黄色背景表示)

//存储食物的坐标点
let food = null;

//设置食物(保证食物不会出现在蛇身上)
const setFood = () => {
  food = null;
  
  //随机生成食物的坐标位置(不能超过游戏界面范围)
  const A = Math.floor(Math.random()*(25));
  const B = Math.floor(Math.random()*(25));

  //设立蛇二维数组中是否有食物坐标的标杆
  let flag = false;
  
  //遍历蛇数组
  for (let index = 0; index < snakeInfo.length; index++) {
    let [x, y, num] = snakeInfo[index];
    
    //如果食物坐标点在蛇数组中跳出循环
    if( A===x && B===y ){
      flag = true;
      break
    }
  };
  
  //蛇的二维数组如果该食物坐标的点,则递归重新生成食物坐标
  if(flag){
    setFood();
  }else{
    //食物使用坐标+3表示,改变该方格的背景颜色
    changeCheckerboard([A, B], 3)
    food = [A, B];
  }
}
setFood();

抽取一个渲染单个方格颜色的方法changeCheckerboard

//改变棋盘格子颜色([A, B]为坐标,color是需要渲染为什么颜色(0为白色,1为红色,2为绿色,3为黄色))
const changeCheckerboard = ([A, B], color) => {
  for (let i = 0; i < checkerboardInfo.length; i++) {
      let [x, y, num] = checkerboardInfo[i];
      
      //如果游戏界面二维数组的坐标值与传入坐标的值相等则改变该方格颜色
      if( A===x && B===y ){
      
        //vue3重新更改了响应式数据的写法,直接更改数组中的值也能被监听到
        checkerboardInfo[i][2]=color;
        break
      }
    }
}

抽取一个清空当前游戏界面所有方格背景色的方法

//清空棋盘颜色
const clearCheckerboard = () => {
  for (let index = 0; index < checkerboardInfo.length; index++) {
    if(checkerboardInfo[index][0]===food[0]&&checkerboardInfo[index][1]===food[1]){
      continue
    };
    checkerboardInfo[index][2] = 0
  }
};
蛇的移动

蛇的移动分为5种情况:

  1. 移动时没吃到食物
  2. 移动时吃到食物
  3. 移动后超出游戏界面边界
  4. 移动后吃到自己
  5. 移动后没吃到自己也没超出游戏界面边界
移动时没吃到食物(如下图)

image.png

9f8a1c6ff586cd1e2d9d4118696ae6b.png

通过观察得出结论:蛇的长度没有变化,即储存蛇的二维数组长度没有变化,只是将移动后的方格坐标位置存储进入储存蛇的二维数组中,然后将储存蛇的二维数组的最后一个坐标删除

//没吃到食物(arr为蛇移动到的方格坐标)
const notEatFood = (arr) => {

  //没吃到食物就将数组头部添加刚移动的坐标数组,尾部删除一个坐标数组(长度不变)
  //蛇头永远是第一个坐标
  snakeInfo.unshift(arr);
  snakeInfo.pop();
  clearCheckerboard();
  
  //通过遍历储存蛇的二维数组,更改游戏界面二维数组存储信息
  for (let index = 0; index < snakeInfo.length; index++) {
    if( index === 0 ){
      
      changeCheckerboard(arr, 1)
    }else{
      changeCheckerboard(snakeInfo[index], 2)
    }
  }
} 
移动时吃到食物

吃食物之前

image.png

吃食物之后

image.png

通过观察得出结论:蛇的长度+1,即储存蛇的二维数组加入新的坐标点,将移动后的方格坐标位置存储进入储存蛇的二维数组中,吃完食物后得再生成食物

//吃到食物
const eatFood = (arr) => {

  //吃到食物就在蛇这个数组首部添加一个刚移动到的坐标数组(数组长度加1)
  snakeInfo.unshift(arr);
  clearCheckerboard();
  for (let index = 0; index < snakeInfo.length; index++) {
    if( index === 0 ){
      changeCheckerboard(arr, 1)
    }else{
      changeCheckerboard(snakeInfo[index], 2)
    }
  };
  
  //吃了食物后得重新生成食物坐标
  setFood();
};
移动后超出游戏界面边界
const judgmentBoundary = () => {
  for (let index = 0; index < snakeInfo.length; index++) {
  
    //蛇上的任意坐标超出游戏界面范围则返回false
    if( (snakeInfo[index][0]>24||snakeInfo[index][0]<0) || (snakeInfo[index][1]>24||snakeInfo[index][1]<0) ){
      return false;
    }
  };
  return true
};
移动后吃到自己
//判断是否移动时碰到蛇身
const didYouTouchTheSnake = (arr) =>{
  for (let index = 0; index < snakeInfo.length; index++) {
    //蛇上的任意坐标等于传入的点则返回false
    if(arr[0]===snakeInfo[index][0]&&arr[1]===snakeInfo[index][1]){
      return true
    }
  }
  return false
}
移动函数

蛇的下一个移动位置,必定是蛇头部位置的周围四个坐标点之一(如下图)。故监听键盘事件,获取当前方向案件的数值(40: 下; 38:上; 37:左; 39:右;),控制蛇的移动

image.png

//方向(vue3的基本类型数据需要使用.value获取设置或更改的值)
let direction = ref(0);

//事件
const listener = (event)=>{
  //只监听上下左右四个按键
  const keyCodeArr =  [37, 38, 39, 40];
  if(keyCodeArr.includes(event.keyCode)){
    direction.value = event.keyCode;
    //只要点击就会立刻对蛇进行移动
    snakeMove(direction.value)
  }
}

//在window上挂载事件监听器
window.addEventListener("keydown", listener)
//设置失败的标杆
const isFail = ref(false); //基本数据类型在vue3中使用ref()处理

//操作蛇的移动
const snakeMove = (num) => {
  //失败后直接跳过
  if( isFail.value ){
    return
  }

  //下一步走哪的坐标数组
  let arr = [...snakeInfo[0]];

  //将下一步走哪的坐标数组只能是贪吃蛇头的四周四个坐标(处理下一步走哪的坐标数组)
  switch (num) {
    //键盘对应数字如下
    //40:下;38;上;37:左;39:右;
    case 37:
      arr[1] = arr[1]-1
      break;
    case 38:
      arr[0] = arr[0]-1
      break;
    case 39:
      arr[1] = arr[1]+1
      break;
    case 40:
      arr[0] = arr[0]+1
      break;
  };

  //当蛇触碰到自身时
  if(didYouTouchTheSnake(arr)){
    isFail.value = true;
  }

  //判断是否吃到食物
  if(food[0]===arr[0]&&food[1]===arr[1]){
    eatFood(arr);
  }else{
    notEatFood(arr);
  }

  //当蛇超出边界时
  if(!judgmentBoundary()){
    isFail.value = true;
  }
};

此时的蛇是个只能通过不断点击按钮才能移动的蛇(点一次才能动一次)

如何让该蛇自主移动?

通过监听direction(方向)值的变化和定时器配合使用实现,只要方向键值发生了改变就做以下处理

//存放定时器的空指针
let timer = null

watchEffect(()=>{

  //使用监听器监听改变方向的值(出现变化每隔0.2s沿着改变后的方向移动一次)
  switch (direction.value) {
    //值发生改变后,每次将之前的定时器关闭再开启新的关于蛇体移动的定时器指令
    case 37:
      timer&&clearInterval(timer);
      timer = setInterval(()=>{
        snakeMove(direction.value)
      },200)
      break;
    case 38:
      timer&&clearInterval(timer);
      timer = setInterval(()=>{
        snakeMove(direction.value)
      },200)
      break;
    case 39:
      timer&&clearInterval(timer);
      timer = setInterval(()=>{
        snakeMove(direction.value)
      },200)
      break;
    case 40:
      timer&&clearInterval(timer);
      timer = setInterval(()=>{
        snakeMove(direction.value)
      },200)
      break;
    case 0:
      timer&&clearInterval(timer);
      break;
  };
});
vue组件结构关系

home.vue -> Checkerboard.vue ->CheckerboardItem.vue

home.vue

<template>
  <div class="home">
    <Checkerboard :checkerboardInfo='checkerboardInfo' />
  </div>
</template>

checkerboard.vue

<template>
  <div class="checkerboard">
    <CheckerboardItem v-for="item in checkerboardInfo" :key="item" :checkerboardItemInfo='item'/>
  </div>
</template>

CheckerboardItem.vue

只通过存入的方格颜色信息(方格信息数组中的第三个值)来判断当前方格显示啥颜色

const props = defineProps({
  checkerboardItemInfo:Array,
});

//通过toRefs解构方格信息数组中的第三个值(只有使用toRefs才能保持该引用数据解构后的数据依然保持响应式)
let [ x, y, num] = toRefs(props.checkerboardItemInfo)

let color = ref('');

//使用监听器完成数据监听,给背景色设置不同值
watchEffect(()=>{
  switch (num.value) {
    case 0:
      color.value = 'while'
      break;
    case 1:
      color.value = 'red'
      break;
    case 2:
      color.value = 'green'
      break;
    case 3:
      color.value = 'orange'
      break;
  }
})
<style lang="less" scoped>
.checkerboardItem{
  //vue3.2能在css中使用v-bind绑定响应式数据
  background-color: v-bind(color);
}
</style>

好了,贪吃蛇搞定,点赞投币➕关注哦,兄弟们(这是在下的第一篇文章【狗头(#^.^#)】)