【taro react】(游戏) ---- 贪吃蛇

1. 预览

2. 实现思路

  1. 实现食物类,食物坐标和刷新食物的位置,以及获取食物的坐标点;
  2. 实现计分面板类,实现吃食物每次的计分以及积累一定程度的等级,实现等级和分数的增加;
  3. 实现蛇类,蛇类分为蛇头和蛇身,蛇头有方向,蛇身每一节跟着前一节移动;
  4. 实现控制器类,初始化上边实现的各个类,同时绑定键盘事件,实现方向的修改;
  5. 使用 requestAnimationFrame 实现页面刷新,蛇移动,坐标点的更新。

3. 食物类

  1. 接口 Point 的实现,用于返回食物的坐标点;
  2. maxX 和 maxY 用于记录横向和纵向的最大坐标值;
  3. 实现 change 实现食物的刷新;
    3.1 传入当前蛇的坐标点,用于判断刷新的点是否是蛇中间的坐标点;
    3.2 使用 Math.round 和 Math.random 生成一个食物坐标点 (x,y);
    3.3 过滤 snakes 蛇坐标点,看看该点是否存在蛇中;
    3.4 如果存在,continue 跳出该次循环,继续生成新的坐标,走3.2,3.3流程;
    3.5 不存在,就保存该坐标点,跳出循环,完成此次坐标的生成。
  4. 实现 getPoints,获取最新的食物坐标。
typescript 复制代码
interface Point{
  x: number,
  y: number,
}

class Food{
  // 食物坐标
  x: number;
  y: number;
  // 格子的最大数量
  maxX: number;
  maxY: number;
  constructor(maxX: number = 35, maxY: number = 35){
    this.maxX = maxX;
    this.maxY = maxY;
  }
  // 修改食物的坐标
  change(snakes: Point[] = []){
    while(true){
      let x = Math.round(Math.random() * (this.maxX - 1));
      let y = Math.round(Math.random() * (this.maxY - 1));
      let filters = snakes.filter(item => {
        if(item.x === x && item.y === y){
          return item;
        }
      })
      if(filters.length){
        continue
      }
      this.x = x;
      this.y = y;
      break
    }
  }
  // 获取食物坐标
  getPoints(): Point{
    return {
      x: this.x,
      y: this.y
    }
  }
}

export default Food;

4. 计分面板类

  1. 初始化分数、等级、最大等级、每次升级所需要的分数;
  2. 分数增长 addScore 实现:
    2.1 分数自增 ++this.score;
    2.2 判断自增后的分数是否满足升级条件 this.score % this.upLevelScore === 0;
    2.3 满足升级条件,调用升级方法 addLevel;
  3. 等级增加 addLevel 实现:
    3.1 判断当前等级是否小于最高等级;
    3.2 满足条件,进行等级升级;
  4. 实现当前等级和分数的获取 getScoreAndLevel 方法实现。
kotlin 复制代码
class ScorePanel{
  score: number = 0;
  level: number = 1;
  // 最大等级
  maxLevel: number;
  // 多少分升一级
  upLevelScore: number;
  constructor(maxLevel: number = 10, upLevelScore: number = 10){
    this.maxLevel = maxLevel;
    this.upLevelScore = upLevelScore;
  }
  // 分数增加
  addScore(){
    ++this.score
    // 满足升级条件,升级
    if(this.score % this.upLevelScore === 0){
      this.addLevel()
    }
  }
  // 等级增加
  addLevel(){
    if(this.level < this.maxLevel){
      ++this.level
    }
  }
  // 获取当前分数和等级
  getScoreAndLevel(){
    return {
      score: this.score,
      level: this.level
    }
  }
}
export default ScorePanel;

5. 蛇类

5.1 全局变量

  1. 由于蛇是一个列表,因此需要一个key的id,因此 generateId 作为id生成器;
  2. DIRECTION_RIGHT、DIRECTION_DOWN、DIRECTION_LEFT、DIRECTION_UP常量定义;
  3. 接口 Point 实现,id参数在食物时不存在,因此可以不存在。
typescript 复制代码
import { generateId } from './utils';
export const DIRECTION_RIGHT = 0;
export const DIRECTION_DOWN = 1;
export const DIRECTION_LEFT = 2;
export const DIRECTION_UP = 3;

interface Point{
  x: number,
  y: number,
  id?: string,
}

5.2 蛇头类

  1. 初始化蛇头坐标(x,y),方向 direction,唯一标识key的id;
  2. 蛇头移动函数move的实现:
    2.1 方向向右【DIRECTION_RIGHT】,x坐标自增1;
    2.2 方向向下【DIRECTION_DOWN】,y坐标自增1;
    2.3 方向向左【DIRECTION_LEFT】,x坐标自减1;
    2.4 方向向上【DIRECTION_UP】,y坐标自减1。
  3. 修改方向更新 setDirection
    3.1 传入方向和当前方向,相同,对方向不做处理;
    3.2 方向相反不做处理,比如当前方向向右,传入方向向左,此次方向不允许改变。
kotlin 复制代码
// 蛇头
export class SnakeHead{
  x: number;
  y: number;
  direction: number;
  id: string;

  constructor(direction:number, x:number, y:number){
    this.direction = direction || DIRECTION_RIGHT;
    this.x = x;
    this.y = y;
    this.id = generateId()
  }
  move(){
    if(this.direction == DIRECTION_RIGHT){
      // 向右
      this.x += 1
    } else if(this.direction == DIRECTION_DOWN){
      // 向下
      this.y += 1
    } else if(this.direction == DIRECTION_LEFT){
      // 向左
      this.x -= 1
    } else if(this.direction == DIRECTION_UP){
      // 向上
      this.y -= 1
    }
  }
  // 修改移动方向
  setDirection(direction:number){
    if(this.direction === direction){
      return;
    } else {
      if(
        this.direction === DIRECTION_RIGHT && direction !== DIRECTION_LEFT ||
        this.direction === DIRECTION_DOWN && direction !== DIRECTION_UP ||
        this.direction === DIRECTION_LEFT && direction !== DIRECTION_RIGHT ||
        this.direction === DIRECTION_UP && direction !== DIRECTION_DOWN
      ){
        this.direction = direction
      }
    }
  }
}

5.3 蛇身类

  1. 初始化每节蛇身的坐标点和唯一标识;
  2. 实现蛇身的移动,当前坐标点是前移节的坐标。
typescript 复制代码
// 蛇身
export class SnakeBody{
  x: number;
  y: number;
  id: string;
  constructor(x:number, y:number){
    this.x = x;
    this.y = y;
    this.id = generateId()
  }
  // 将当前位置的模块移动到前一个模块的位置
  move(prevItem: (SnakeHead | SnakeBody)){
    if(prevItem){
      this.x = prevItem.x
      this.y = prevItem.y
    }
  }
}

5.4 蛇类

  1. 初始化蛇列表、蛇头、蛇身最后一节、每次吃食物蛇身的增长长度、记录当前次吃食物身体增长的长度、蛇能存在的最大坐标;
  2. 初始化蛇:
    2.1 创建蛇的坐标点列表;
    2.2 创建蛇头函数执行;
    2.3 创建蛇身函数执行。
  3. 修改蛇的行进方向改变实现 setDirection,直接调用蛇头的修改方向方法;
  4. 获取两个数之间的随机值 random 方法实现;
  5. 创建蛇头函数 createHead 实现:
    5.1 获取坐标随机值的最大值maxX的一半;
    5.2 由于初始化蛇身最少三节,因此对最小值处理必须大于0;
    5.3 创建蛇头 snakeHead;
    5.4 将蛇头对象存放到蛇的列表中。
  6. 蛇身 createBodies 实现:
    6.1 蛇身最少三节,因此循环产生每一节蛇身;
    6.2 创建每一节蛇身,对y坐标,不做处理,使用蛇头的x坐标一次递减。
  7. 蛇移动函数 move 实现:
    7.1 获取移动前左后一节蛇身对象;
    7.2 记录最后一节蛇身对象的坐标用于吃食物后身体的增长;
    7.3 由于移动防止脱节,因此从最后一节一次往前一节循环移动;
    7.4 由于蛇身的移动需要获取前一节蛇身的坐标,因此移动时,传入前一节蛇身对象;
    7.5 移动完成,检测当前移动后蛇头是否碰撞墙或者撞到自身。
  8. 撞墙检测函数 hasOver 实现:
    8.1 获取蛇头坐标,看看是否超出盒子的x,y的范围;
  9. 撞自身检测函数 hasSelf实现:
    9.1 因为蛇头不可能撞到自己蛇头,因此去掉蛇头,从1开始遍历蛇身坐标;
    9.2 如果存在有蛇身坐标和蛇头坐标相同,说明撞到自己,结束游戏。
  10. 吃掉食物判断函数 eatFood:
    10.1 传入当前食物的坐标值;
    10.2 判断食物坐标和蛇头坐标是否重合;
    10.3 更新增长身体增长变量;
    10.4 返回吃到食物未 true;
    10.5 否则说明没有吃到食物,返回 false。
  11. 获取最新蛇的全部坐标点函数 getPoints:
    11.1 返回蛇的坐标点和唯一标识id。
  12. 设置每次吃食物蛇的增长长度 setUpdateBodyNumber 函数实现;
  13. 实现蛇的增长函数 updateBody:
    13.1 判断 currentUpdateBody > 0 是否满足;
    13.2 添加一个最新的蛇身对象,坐标是移动前最后一节的坐标;
    13.3 添加完成,增长记录变量自减1。
kotlin 复制代码
class Snake{
  // 完整蛇列表
  snakes: (SnakeHead | SnakeBody)[]
  // 蛇头
  snakeHead: SnakeHead;
  // 蛇身最后一节
  snakeLastBody: Point;
  // 每次食物身体增长长度
  updateBodyNumber: number;
  // 当前次身体增长的值
  currentUpdateBody: number;
  // 格子的最大数量
  maxX: number;
  maxY: number;

  constructor(maxX: number = 35, maxY: number = 35, updateBodyNumber: number = 3){
    this.maxX = maxX;
    this.maxY = maxY;
    this.updateBodyNumber = updateBodyNumber;
    this.currentUpdateBody = 0;
    // 初始化蛇
    this.init()
  }
  // 初始化蛇
  init(){
    this.snakes = []
    // 创建蛇头
    this.createHead()
    // 创建蛇身
    this.createBodies()
  }
  setDirection(direction: number){
    this.snakeHead.setDirection(direction)
  }
  random(n:number,m:number):number{
    return Math.round(Math.random() * (m - n) + n)
  }
  // 创建蛇头
  createHead(){
    let max = Math.round(this.maxX / 2);
    let min = max - 5 > 0 ? max - 5 : 0;
    this.snakeHead = new SnakeHead(DIRECTION_RIGHT, this.random(min,max), this.random(min,max))
    this.snakes.push(this.snakeHead)
  }
  // 创建蛇身
  createBodies(){
    for(let i = 1; i <= 3; i++){
      this.snakes.push(new SnakeBody(this.snakeHead.x - i, this.snakeHead.y))
    }
  }
  // 移动函数
  move(){
    // 移动前记录蛇身最后一节的坐标
    let last = this.snakes.at(-1) as SnakeBody
    this.snakeLastBody = { x: last.x, y: last.y }
    let len:number = this.snakes.length;
    for(let i = len - 1; i >= 0; i--){
      this.snakes[i].move(this.snakes[i - 1])
    }
    // 移动完成,检测是否撞到墙和自身
    this.hasOver()
    this.hasSelf()
  }
  // 判断是否撞墙或者撞到自身
  hasOver(){
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    if(head.x < 0 || head.x >= this.maxX || head.y < 0 || head.y >= this.maxY){
      throw('撞墙了!')
    }
  }
  // 判断是否撞到蛇自身
  hasSelf(){
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    let len = this.snakes.length
    for(let i = 1; i < len; i++){
      let item = this.snakes[i]
      if(head.x === item.x && head.y === item.y){
        throw('撞到自己了!')
      }
    }
  }
  // 是否吃掉食物
  eatFood(food: Point): boolean{
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    if(head.x === food.x && head.y === food.y){
      this.currentUpdateBody = this.updateBodyNumber;
      return true
    }
    return false
  }
  // 获取最新坐标点列表
  getPoints(): Point[]{
    let snakes: Point[] = this.snakes.map(item => {
      return {
        x: item.x,
        y: item.y,
        id: item.id
      }
    })
    return snakes
  }
  // 设置身体增长的长度
  setUpdateBodyNumber(bodyNumber:number){
    this.updateBodyNumber = bodyNumber;
  }
  // 身体增长
  updateBody(){
    if(this.currentUpdateBody > 0){
      this.snakes.push(new SnakeBody(this.snakeLastBody.x, this.snakeLastBody.y))
      this.currentUpdateBody--
    }
  }
}

export default Snake;

5.5 完整蛇类代码

kotlin 复制代码
import { generateId } from './utils';
export const DIRECTION_RIGHT = 0;
export const DIRECTION_DOWN = 1;
export const DIRECTION_LEFT = 2;
export const DIRECTION_UP = 3;

interface Point{
  x: number,
  y: number,
  id?: string,
}

// 蛇头
export class SnakeHead{
  x: number;
  y: number;
  direction: number;
  id: string;

  constructor(direction:number, x:number, y:number){
    this.direction = direction || DIRECTION_RIGHT;
    this.x = x;
    this.y = y;
    this.id = generateId()
  }
  move(){
    if(this.direction == DIRECTION_RIGHT){
      // 向右
      this.x += 1
    } else if(this.direction == DIRECTION_DOWN){
      // 向下
      this.y += 1
    } else if(this.direction == DIRECTION_LEFT){
      // 向左
      this.x -= 1
    } else if(this.direction == DIRECTION_UP){
      // 向上
      this.y -= 1
    }
  }
  // 修改移动方向
  setDirection(direction:number){
    if(this.direction === direction){
      return;
    } else {
      if(
        this.direction === DIRECTION_RIGHT && direction !== DIRECTION_LEFT ||
        this.direction === DIRECTION_DOWN && direction !== DIRECTION_UP ||
        this.direction === DIRECTION_LEFT && direction !== DIRECTION_RIGHT ||
        this.direction === DIRECTION_UP && direction !== DIRECTION_DOWN
      ){
        this.direction = direction
      }
    }
  }
}
// 蛇身
export class SnakeBody{
  x: number;
  y: number;
  id: string;
  constructor(x:number, y:number){
    this.x = x;
    this.y = y;
    this.id = generateId()
  }
  // 将当前位置的模块移动到前一个模块的位置
  move(prevItem: (SnakeHead | SnakeBody)){
    if(prevItem){
      this.x = prevItem.x
      this.y = prevItem.y
    }
  }
}

class Snake{
  // 完整蛇列表
  snakes: (SnakeHead | SnakeBody)[]
  // 蛇头
  snakeHead: SnakeHead;
  // 蛇身最后一节
  snakeLastBody: Point;
  // 每次食物身体增长长度
  updateBodyNumber: number;
  // 当前次身体增长的值
  currentUpdateBody: number;
  // 格子的最大数量
  maxX: number;
  maxY: number;

  constructor(maxX: number = 35, maxY: number = 35, updateBodyNumber: number = 3){
    this.maxX = maxX;
    this.maxY = maxY;
    this.updateBodyNumber = updateBodyNumber;
    this.currentUpdateBody = 0;
    // 初始化蛇
    this.init()
  }
  // 初始化蛇
  init(){
    this.snakes = []
    // 创建蛇头
    this.createHead()
    // 创建蛇身
    this.createBodies()
  }
  setDirection(direction: number){
    this.snakeHead.setDirection(direction)
  }
  random(n:number,m:number):number{
    return Math.round(Math.random() * (m - n) + n)
  }
  // 创建蛇头
  createHead(){
    let max = Math.round(this.maxX / 2);
    let min = max - 5 > 0 ? max - 5 : 0;
    this.snakeHead = new SnakeHead(DIRECTION_RIGHT, this.random(min,max), this.random(min,max))
    this.snakes.push(this.snakeHead)
  }
  // 创建蛇身
  createBodies(){
    for(let i = 1; i <= 3; i++){
      this.snakes.push(new SnakeBody(this.snakeHead.x - i, this.snakeHead.y))
    }
  }
  // 移动函数
  move(){
    // 移动前记录蛇身最后一节的坐标
    let last = this.snakes.at(-1) as SnakeBody
    this.snakeLastBody = { x: last.x, y: last.y }
    let len:number = this.snakes.length;
    for(let i = len - 1; i >= 0; i--){
      this.snakes[i].move(this.snakes[i - 1])
    }
    // 移动完成,检测是否撞到墙和自身
    this.hasOver()
    this.hasSelf()
  }
  // 判断是否撞墙或者撞到自身
  hasOver(){
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    if(head.x < 0 || head.x >= this.maxX || head.y < 0 || head.y >= this.maxY){
      throw('撞墙了!')
    }
  }
  // 判断是否撞到蛇自身
  hasSelf(){
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    let len = this.snakes.length
    for(let i = 1; i < len; i++){
      let item = this.snakes[i]
      if(head.x === item.x && head.y === item.y){
        throw('撞到自己了!')
      }
    }
  }
  // 是否吃掉食物
  eatFood(food: Point): boolean{
    // 获取蛇头
    let head:SnakeHead = this.snakeHead
    if(head.x === food.x && head.y === food.y){
      this.currentUpdateBody = this.updateBodyNumber;
      return true
    }
    return false
  }
  // 获取最新坐标点列表
  getPoints(): Point[]{
    let snakes: Point[] = this.snakes.map(item => {
      return {
        x: item.x,
        y: item.y,
        id: item.id
      }
    })
    return snakes
  }
  // 设置身体增长的长度
  setUpdateBodyNumber(bodyNumber:number){
    this.updateBodyNumber = bodyNumber;
  }
  // 身体增长
  updateBody(){
    if(this.currentUpdateBody > 0){
      this.snakes.push(new SnakeBody(this.snakeLastBody.x, this.snakeLastBody.y))
      this.currentUpdateBody--
    }
  }
}

export default Snake;

6. 控制器类

  1. 初始化食物、蛇、计分面板;
  2. 获取初始化时蛇的坐标列表;
  3. 调用食物刷新方法,刷新食物坐标;
  4. 绑定键盘事件:
    4.1 key 是 ArrowDown,调用 this.snake.setDirection(DIRECTION_DOWN);
    4.2 key 是 ArrowLeft,调用 this.snake.setDirection(DIRECTION_LEFT);
    4.3 key 是 ArrowUp,调用 this.snake.setDirection(DIRECTION_UP);
    4.4 key 是 ArrowRight,调用 this.snake.setDirection(DIRECTION_RIGHT)。
kotlin 复制代码
import Food from "./Food";
import Snake, {
  DIRECTION_RIGHT,
  DIRECTION_LEFT,
  DIRECTION_UP,
  DIRECTION_DOWN,
} from "./Snake";
import ScorePanel from './ScorePanel';

interface Point{
  x: number,
  y: number
}

class GameContral{
  // 食物
  food: Food;
  // 蛇
  snake: Snake;
  // 计分面板
  scorePanel: ScorePanel;
  // 格子数
  cell: number;
  constructor(cell: number = 15){
    this.cell = cell;
    // 初始化
    this.init()
  }
  // 初始化
  init(){
    // 初始化蛇
    this.snake = new Snake(this.cell, this.cell);
    // 初始化食物
    this.food = new Food(this.cell, this.cell);
    // 初始化食物位置
    let points: Point[] = this.snake.getPoints()
    this.food.change(points)
    // 初始化计分面板
    this.scorePanel = new ScorePanel();
    // 绑定键盘事件
    document.addEventListener('keydown', this.keyboardHandler.bind(this))
  }
  // 键盘操作
  keyboardHandler(event: KeyboardEvent){
    let key:string = event.key;
    switch(key){
      case 'ArrowDown':
        this.snake.setDirection(DIRECTION_DOWN)
        break;
      case 'ArrowLeft':
        this.snake.setDirection(DIRECTION_LEFT)
        break;
      case 'ArrowUp':
        this.snake.setDirection(DIRECTION_UP)
        break;
      case 'ArrowRight':
        this.snake.setDirection(DIRECTION_RIGHT)
        break;
      default:
        this.snake.setDirection(DIRECTION_RIGHT)
        break;
    }
  }
}

export default GameContral;

7. 界面实现

7.1 接口实现

  1. 坐标点接口 Point、计分和等级接口 Score、页面渲染数据接口 State、逻辑处理接口 RefData 实现;
yaml 复制代码
interface Point{
  x: number,
  y: number,
  id?: string,
}
interface Score{
  score: number,
  level: number
}

interface State{
  isStart: boolean,
  snakes: Point[],
  food: Point,
  cells: number,
  scorePanel: Score
}
interface RefData{
  gameContral: GameContral,
  food: Food,
  snake: Snake,
  isFirst: boolean
}

7.2 逻辑变量和渲染变量初始化

  1. foodAndSnake 存储食物类对象、蛇对象、控制器对象,是否第一次执行等做逻辑处理,不需要界面渲染;
  2. data 存储是否开始,蛇身坐标列表、食物坐标、计分面板,用于页面渲染,数据都是重新筛选后,不存在多于数据。
yaml 复制代码
  let foodAndSnake = useRef<RefData>({
    gameContral: new GameContral(),
    food: new Food(),
    snake: new Snake(),
    isFirst: true
  })
  // 格子数量
  let [data, setData] = useSetState<State>({
    isStart: false,
    snakes: [],
    food: {x: 0, y: 0},
    cells: 30,
    scorePanel: {score: 0, level: 1}
  })

7.3 初始化

  1. useEffect 监听 onmount 的时候,初始化页面;
  2. init 初始化各个类对象:
    2.1 初始化 GameContral 游戏控制器类;
    2.2 赋值食物类对象;
    2.3 赋值蛇类对象;
  3. 更新食物坐标、蛇坐标、计分面板
    3.1 获取最新的蛇坐标列表;
    3.2 获取最新的食物坐标;
    3.3 获取最新的计分面板信息。
scss 复制代码
  // 初始化食物和蛇得数据
  useEffect(() => {
    init()
  },[])
  // 初始化
  function init(){
    let gameContral = new GameContral(30)
    foodAndSnake.current.gameContral = gameContral;
    foodAndSnake.current.food = gameContral.food;
    foodAndSnake.current.snake = gameContral.snake;
    setPoints()
  }
  // 获取食物和蛇得坐标
  function setPoints(){
    // 获取蛇的最新坐标
    let snakes: Point[] = foodAndSnake.current.snake.getPoints()
    // 获取食物的最新坐标
    let food: Point = foodAndSnake.current.food.getPoints()
    // 获取计分面板的最新分数和等级
    let scorePanel: Score = foodAndSnake.current.gameContral.scorePanel.getScoreAndLevel()
    setData({ snakes, food, scorePanel })
  }

7.4 监听动画实现

  1. 对蛇进行移动,调用蛇对象的移动函数;
  2. 调用蛇对象的 eatFood 判断是否吃食物;
  3. 吃了食物,进行刷新;
  4. 如果吃了食物,进行加分;
  5. 更新身体长度;
  6. 更新蛇坐标列表,食物坐标,计分面板信息;
  7. 如果上边流程出现异常报错,直接结束游戏,游戏结束的逻辑在异常处处理;
  8. 随着等级的提升,速度越来越快。【300 - (data.scorePanel.level - 1) * 30】
scss 复制代码
  // 监听刷新界面
  useAnimationFrame(() => {
    try {
      // 对蛇进行移动
      foodAndSnake.current.snake.move()
      // 判断是否吃食物
      let isEating: boolean = foodAndSnake.current.snake.eatFood(data.food)
      if(isEating){
        // 吃了食物,进行刷新
        foodAndSnake.current.food.change(foodAndSnake.current.snake.getPoints())
        // 如果吃了食物,进行加分
        foodAndSnake.current.gameContral.scorePanel.addScore()
      }
      // 更新身体长度
      foodAndSnake.current.snake.updateBody()
      setPoints()
    } catch (error) {
      setData({isStart: false})
      console.log('error',error)
    }
  },data.isStart,{
    delay: 300 - (data.scorePanel.level - 1) * 30
  })

7.5 useAnimationFrame 实现

scss 复制代码
import { useMemoizedFn } from '../useMemoizedFn'
import { useRef, useCallback, useEffect } from 'react'
import { isObject, isNumber } from '../utils'

interface Options {
  immediate?: boolean,
  delay: number
}

export function useAnimationFrame(fn: () => void, running?: boolean, options: Options = {delay: 0}){
  const timerCallback = useMemoizedFn(fn)
  const requestId = useRef<number>(0)
  const handleTime = useRef<number>(Date.now())

  const clear = useCallback(() => {
    if (requestId.current) {
      cancelAnimationFrame(requestId.current)
    }
  }, []);

  const tick = useCallback(() => {
    if(isObject(options) && isNumber(options.delay) && options.delay){
      let current = Date.now()
      if(current - handleTime.current > options.delay){
        handleTime.current = current
        timerCallback()
      }
    } else {
      timerCallback()
    }
    if(running){
      requestId.current = requestAnimationFrame(tick)
    }
  },[running, options.delay])

  useEffect(() => {
    if (!running) {
      return
    }
    handleTime.current = Date.now()
    requestId.current = requestAnimationFrame(tick)
    return clear
  }, [running, options.delay])

  return clear
}

7.6 蛇和食物的绘制

  1. 不同屏幕的计算函数 handleSize;
  2. 食物绘制函数 drawFood 实现,通过食物坐标进行定位;
  3. 蛇列表绘制函数 drawSnake 实现,根据蛇列表循环计算坐标点。
javascript 复制代码
  // 计算对应屏幕的尺寸
  function handleSize(size: number):number{
    let windowWidth = window.innerWidth > 750 ? 750 : window.innerWidth;
    return (windowWidth / 750) * size;
  }
  // 绘制食物
  function drawFood():JSX.Element{
    return <View 
    style={`top:${handleSize(data.food.y * 20)}px;left:${handleSize(data.food.x * 20)}px;`}
    className='rui-snake-food'></View>
  }
  // 绘制蛇
  function drawSnake():JSX.Element[]{
    return data.snakes.map(item => <View 
      key={item.id}
      id={item.id}
      style={`top:${handleSize(item.y * 20)}px;left:${handleSize(item.x * 20)}px;`}
      className='rui-snake-body'></View>)
  }

7.7 界面布局

xml 复制代码
    <View className='rui-snake-container'>
      <View className='rui-stage-content'>
        {/* 食物 */}
        {drawFood()}
        {/* 蛇 */}
        {drawSnake()}
      </View>
      <View className='rui-score-panel'>
        <View>
          SCORE: <Text>{data.scorePanel.score}</Text>
        </View>
        <View>
          LEVEL: <Text>{data.scorePanel.level}</Text>
        </View>
      </View>
      <View 
      className='rui-handle-panel'>
        <View onClick={resetStart}>开始</View>
      </View>
    </View>

7.8 界面完整代码

typescript 复制代码
import { View, Text } from '@tarojs/components';
import React, { useEffect, useRef } from "react";
import { useAnimationFrame, useSetState } from '../../utils/hooks'
import Food from './modules/Food'
import Snake from './modules/Snake'
import GameContral from './modules/GameContral'
import './index.scss';

interface Point{
  x: number,
  y: number,
  id?: string,
}
interface Score{
  score: number,
  level: number
}

interface State{
  isStart: boolean,
  snakes: Point[],
  food: Point,
  cells: number,
  scorePanel: Score
}
interface RefData{
  gameContral: GameContral,
  food: Food,
  snake: Snake,
  isFirst: boolean
}

const SnakeGame = () => {
  let foodAndSnake = useRef<RefData>({
    gameContral: new GameContral(),
    food: new Food(),
    snake: new Snake(),
    isFirst: true
  })
  // 格子数量
  let [data, setData] = useSetState<State>({
    isStart: false,
    snakes: [],
    food: {x: 0, y: 0},
    cells: 30,
    scorePanel: {score: 0, level: 1}
  })
  // 初始化食物和蛇得数据
  useEffect(() => {
    init()
  },[])
  // 监听刷新界面
  useAnimationFrame(() => {
    try {
      // 对蛇进行移动
      foodAndSnake.current.snake.move()
      // 判断是否吃食物
      let isEating: boolean = foodAndSnake.current.snake.eatFood(data.food)
      if(isEating){
        // 吃了食物,进行刷新
        foodAndSnake.current.food.change(foodAndSnake.current.snake.getPoints())
        // 如果吃了食物,进行加分
        foodAndSnake.current.gameContral.scorePanel.addScore()
      }
      // 更新身体长度
      foodAndSnake.current.snake.updateBody()
      setPoints()
    } catch (error) {
      setData({isStart: false})
      console.log('error',error)
    }
  },data.isStart,{
    delay: 300 - (data.scorePanel.level - 1) * 30
  })
  // 重新开始
  function resetStart(){
    // 是否是第一次点击开始,是就不进行初始化,否则初始化面板
    if(foodAndSnake.current.isFirst){
      foodAndSnake.current.isFirst = false
    } else {
      init()
    }
    setData({isStart: true})
  }
  // 初始化
  function init(){
    let gameContral = new GameContral(30)
    foodAndSnake.current.gameContral = gameContral;
    foodAndSnake.current.food = gameContral.food;
    foodAndSnake.current.snake = gameContral.snake;
    setPoints()
  }
  // 获取食物和蛇得坐标
  function setPoints(){
    // 获取蛇的最新坐标
    let snakes: Point[] = foodAndSnake.current.snake.getPoints()
    // 获取食物的最新坐标
    let food: Point = foodAndSnake.current.food.getPoints()
    // 获取计分面板的最新分数和等级
    let scorePanel: Score = foodAndSnake.current.gameContral.scorePanel.getScoreAndLevel()
    setData({ snakes, food, scorePanel })
  }
  // 计算对应屏幕的尺寸
  function handleSize(size: number):number{
    let windowWidth = window.innerWidth > 750 ? 750 : window.innerWidth;
    return (windowWidth / 750) * size;
  }
  // 绘制食物
  function drawFood():JSX.Element{
    return <View 
    style={`top:${handleSize(data.food.y * 20)}px;left:${handleSize(data.food.x * 20)}px;`}
    className='rui-snake-food'></View>
  }
  // 绘制蛇
  function drawSnake():JSX.Element[]{
    return data.snakes.map(item => <View 
      key={item.id}
      id={item.id}
      style={`top:${handleSize(item.y * 20)}px;left:${handleSize(item.x * 20)}px;`}
      className='rui-snake-body'></View>)
  }
  return (
    <View className='rui-snake-container'>
      <View className='rui-stage-content'>
        {/* 食物 */}
        {drawFood()}
        {/* 蛇 */}
        {drawSnake()}
      </View>
      <View className='rui-score-panel'>
        <View>
          SCORE: <Text>{data.scorePanel.score}</Text>
        </View>
        <View>
          LEVEL: <Text>{data.scorePanel.level}</Text>
        </View>
      </View>
      <View 
      className='rui-handle-panel'>
        <View onClick={resetStart}>开始</View>
      </View>
    </View>
  )
}
export default SnakeGame;

8. SCSS 样式实现

css 复制代码
// 基础颜色变量
$bg-color: #b7d4a8;
$border-color: #000000;
$head-color: lightgreen;
$body-color: red;
$food-color: red;

*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.rui-snake-container{
  box-sizing: border-box;
  width: 100vw;
  height: 100vh;
  background-color: $bg-color;
  border: 10px solid $border-color;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;

  .rui-stage-content{
    width: 704px;
    height: 704px;
    border: 2px solid $border-color;
    position: relative;

    .rui-snake-body{
      width: 20px;
      height: 20px;
      border: 1px solid $bg-color;
      background-color: $border-color;
      position: absolute;
    }
    .rui-snake-food{
      width: 20px;
      height: 20px;
      overflow: hidden;
      position: absolute;
      // transform: rotate(45deg);
      border: 1px solid $bg-color;
      background-color: $food-color;
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-content: space-between;
      .rui-food-li{
        width: 8px;
        height: 8px;
        background-color: $border-color;
      }
    }
    
    .rui-snake-row{
      display: flex;
      align-items: center;
      .rui-snake-cell{
        width: 20px;
        height: 20px;
        flex: none;
      }
    }
  }
  .rui-score-panel{
    width: 700px;
    font: 30px '微软雅黑';
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .rui-handle-panel{
    width: 600px;
    font: 30px '微软雅黑';
    text-align: center;
  }
}

9. 总结

  1. 最开准备使用 30 * 30 个格子,进行判断渲染,渲染数据每次更改太多,渲染时间很久,因此不建议使用;
  2. 为什么每次设置渲染数据都要筛选一次,因为直接将类渲染,数据随着蛇成长,会越来越大,很卡。
相关推荐
谢尔登9 小时前
【React Native】快速入门
javascript·react native·react.js
进取星辰9 小时前
32、跨平台咒语—— React Native初探
javascript·react native·react.js
君的名字11 小时前
怎么判断一个Android APP使用了React Native 这个跨端框架
android·react native·react.js
iamtsfw12 小时前
从头实现react native expo本地生成APK
javascript·react native·react.js
whatever who cares12 小时前
react native搭建项目
react.js
每一天,每一步13 小时前
React+MapBox GL JS引入URL服务地址实现自定义图标标记地点、区域绘制功能
前端·javascript·react.js
HaanLen15 小时前
React19源码系列之渲染阶段performUnitOfWork
前端·javascript·react.js·react19源码
GISer_Jing17 小时前
React Hooks底层执行逻辑详解、自定义Hooks、Fiber&Scheduler
前端·javascript·react.js
蓉妹妹1 天前
React+Taro 微信小程序做一个页面,背景图需贴手机屏幕最上边覆盖展示
react.js·微信小程序·taro
进取星辰1 天前
34、React Server Actions深度解析
前端·react.js·前端框架