Bootstrap

基于react函数式组件实现贪吃蛇及useRef闭包问题解决

最近用react写了个贪吃蛇,写的过程中参考了一些文档,基本都是用class写的,用函数式组件写的不多,所以记录下用函数式组件实现的,不算完美,仅供需要的小伙伴参考。

hooks结合定时器一起使用的时候会出现闭包问题,数据更新后拿不到期望的值,卡了一上午,最后用useRef解决。

只需要这样定义,在需要使用的地方用foodRef.current就能获取到期望值了

  const [food, setFood] = useState<any>({ x: 0, y: 5 });
  const foodRef = useRef<any>()
  foodRef.current = food

附两个useRef相关文档参考:

用 useRef() 去解决 react-hook ”闭包陷阱“_brucehongsen的博客-CSDN博客

使用 React Hooks 声明 setInterval — Overreacted

继续贪吃蛇,先上效果图:

代码实现主要包括以下几点:

1.绘制背景,可以用表格也可以用空白背景

2.定义snake、food、direction等状态

3.监听键盘事件

4.实现snake的移动:转向控制、整体移动

5.snake吃食物:snake长度+1,生成下一个食物点,每吃到一个食物snake移动速度加快20,即定时器间隔-20

6.碰撞检测:碰到墙体和碰到自己

以下为完整代码:

import React, { useEffect, useState, useRef } from "react";
import "./index.css";

let interval;
let direction = "right";
const size = { row: 20, col: 20 };
const red = <div className="red"></div>;
const black = <div className="black"></div>;
const Snake: React.FC = (props: any) => {
  const [isRunning, setIsRunning] = useState<Boolean>(false);
  const [snake, setSnake] = useState<any>([{ x: 0, y: 0 }]);
  const [speed, setSpeed] = useState<number>(500)
  const [food, setFood] = useState<any>({ x: 0, y: 5 });
  const foodRef = useRef<any>()
  foodRef.current = food
  const speedRef = useRef<number>()
  speedRef.current = speed

  useEffect(() => {
    if (isRunning) {
      document.addEventListener("keydown", keyDownPress);
    }
    return () => {
      document.removeEventListener("keydown", keyDownPress);
    };
  }, [isRunning]);

  // 监听键盘事件
  const keyDownPress = (e) => {
    switch (e.key) {
      case "ArrowDown":
        direction = "down";
        break;
      case "ArrowLeft":
        direction = "left";
        break;
      case "ArrowUp":
        direction = "up";
        break;
      case "ArrowRight":
        direction = "right";
        break;
      default:
        break;
    }
  };

  // 开始
  const clickStart = () => {
    console.log(speedRef.current)
    interval && clearInterval(interval)
    interval = setInterval(function () {
      move();
    }, speedRef.current);
    setIsRunning(true);
  };

  // snake移动,碰撞检测、吃食物、整体移动等都是在move()函数处理
  const move = () => {
    let food = foodRef.current || {x: 0,y: 0}
    let first = { x: snake[0].x, y: snake[0].y };
    let last = {};
    switch (direction) {
      case "up":
        first.x -= 1;
        break;
      case "down":
        first.x += 1;
        break;
      case "left":
        first.y -= 1;
        break;
      case "right":
        first.y += 1;
        break;
      default:
        break;
    }

    if (snake.length > 1 && first.x == snake[1].x && first.y == snake[1].y) {
      // 前进方向不能逆行
      switch (direction) {
        case "up":
          first.x += 2;
          break;
        case "down":
          first.x -= 2;
          break;
        case "left":
          first.y += 2;
          break;
        case "right":
          first.y -= 2;
          break;
        default:
          break;
      }
    }

    // 碰撞检测
    if (collision(first)) {
      clearInterval(interval);
      alert("Game Over!");
      return;
    }

    let eat = false;
    // 吃到食物
    if ( first.x === food.x && first.y === food.y) {
      eat = true;
      last = { x: snake[snake.length - 1].x, y: snake[snake.length - 1].y };
    }

    // 蛇整体动起来
    for (let s in snake) {
      var next_first = { x: snake[s].x, y: snake[s].y };
      snake[s].x = first.x;
      snake[s].y = first.y;
      first = next_first;
    }
    // 吃到食物之后蛇变长
    if (eat) {
      eat = false
      snake[snake.length] = last;
      showNewFood();
    }

    // 这里如果直接用setSnake(snake),react会判断数组没变,不会重新渲染
    // 前面用JSON.parse(JSON.stringify(snake))深拷贝也不行。。。
    setSnake([...snake]);
  };

  // 碰撞检测
  const collision = (first) => {
    // 碰到边界
    if (
      first.x > size.row - 1 ||
      first.x < 0 ||
      first.y > size.col - 1 ||
      first.y < 0
    ) {
      return true;
    }
    // 碰到自己
    // for(let i = 0; i < snake.length; i++) {
    //   if(snake[i].x === first.x && snake[i].y === first.y) {
    //     return true
    //   }
    // }
    let _snake = snake.filter(
      (item) => item.x === first.x && item.y === first.y
    );
    if (_snake.length > 0) {
      return true;
    }
    return false;
  };

  // 用于生成下一个食物点(排除snake和当前food)
  const showNewFood = () => {
    let nextArr: any = [];
    for (let r = 0; r < size.row; r++) {
      for (let c = 0; c < size.col; c++) {
        if (getStatus(r, c) === 0) {
          let n = { x: r, y: c };
          nextArr.push(n);
        }
      }
    }
    let nextIndex = Math.round(Math.random() * nextArr.length - 1);
    setFood({ x: nextArr[nextIndex].x, y: nextArr[nextIndex].y });
    // 加速后需要重启定时器
    setSpeed(speedRef.current - 20)
    clickPause()
    clickStart()
  };

  // 判断当前点是snake或者food,渲染的时候调用,生成新食物的时候也要用
  const getStatus = (r, c) => {
    for (let s in snake) {
      if (snake[s].x === r && snake[s].y === c) {
        // snake
        return 1;
      }
      if (food.x === r && food.y === c) {
        // food
        return 2;
      }
    }
    return 0;
  };

  // 暂停
  const clickPause = () => {
    interval && clearInterval(interval);
    setIsRunning(false);
  };

  // 渲染表格,数据更新后会自动调用这个函数
  const refresh = () => {
    let trArr = [];
    for (let i = 0; i < size.row; i++) {
      let tdArr = [];
      for (let j = 0; j < size.col; j++) {
        let value = getStatus(i, j);
        if (value === 0) {
          tdArr.push(<td key={i * 20 + j}></td>);
        } else if (value === 1) {
          tdArr.push(<td key={i * 20 + j}>{black}</td>);
        } else if (value === 2) {
          tdArr.push(<td key={i * 20 + j}>{red}</td>);
        }
      }
      trArr.push(<tr key={i}>{tdArr}</tr>);
    }
    return <tbody>{trArr}</tbody>;
  };
  return (
    <div>
      <div className="game_background">
        <table border="1">{refresh()}</table>
      </div>
      <div className="start">
        <button
          className="button"
          onClick={() => {
            clickStart();
          }}
        >
          开始
        </button>
        <button
          className="button"
          onClick={() => {
            clickPause();
          }}
        >
          暂停
        </button>
      </div>
    </div>
  );
};
export default Snake;
.game_background{
  width: 60%;
  float: left;
  margin: 20px;
}
.game_background table {
  border-collapse: collapse;
}
.game_background table td{
   width: 30px;
   height: 30px;
   padding: 0;
   margin: 0;
}
.black {
  background-color: black;
  width: 100%;
  height: 100%;
}
.red {
  background-color: red;
  width: 100%;
  height: 100%;
}
.start {
  text-align: center;
  width: 600px;
}
.button {
  margin: 10px;
}

代码链接:

https://gitee.com/Seven_77777/snake

;