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

news2024/11/16 12:38:50

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,获取最新的食物坐标。
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 方法实现。
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参数在食物时不存在,因此可以不存在。
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 方向相反不做处理,比如当前方向向右,传入方向向左,此次方向不允许改变。
// 蛇头
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. 实现蛇身的移动,当前坐标点是前移节的坐标。
// 蛇身
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。
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 完整蛇类代码

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)。
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 实现;
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 存储是否开始,蛇身坐标列表、食物坐标、计分面板,用于页面渲染,数据都是重新筛选后,不存在多于数据。
  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 获取最新的计分面板信息。
  // 初始化食物和蛇得数据
  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】
  // 监听刷新界面
  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 实现

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 实现,根据蛇列表循环计算坐标点。
  // 计算对应屏幕的尺寸
  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 界面布局

    <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 界面完整代码

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 样式实现

// 基础颜色变量
$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. 为什么每次设置渲染数据都要筛选一次,因为直接将类渲染,数据随着蛇成长,会越来越大,很卡。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/957177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

16 Linux之JavaEE定制篇-搭建JavaEE环境

16 Linux之JavaEE定制篇-搭建JavaEE环境 文章目录 16 Linux之JavaEE定制篇-搭建JavaEE环境16.1 概述16.2 安装JDK16.3 安装tomcat16.4 安装idea2020*16.5 安装mysql5.7 学习视频来自于B站【小白入门 通俗易懂】2021韩顺平 一周学会Linux。可能会用到的资料有如下所示&#xff0…

Matlab图像处理-灰度插值法

最近邻法 最近邻法是一种最简单的插值算法&#xff0c;输出像素的值为输入图像中与其最邻近的采样点的像素值。是将(u0,v0)(u_0,v_0)点最近的整数坐标u,v(u,v)点的灰度值取为(u0,v0)(u_0,v_0)点的灰度值。 在(u0,v0)(u_0,v_0)点各相邻像素间灰度变化较小时&#xff0c;这种方…

使用ELK收集解析nginx日志和kibana可视化仪表盘

文章目录 ELK生产环境配置filebeat 配置logstash 配置 kibana仪表盘配置配置nginx转发ES和kibanaELK设置账号和密码 ELK生产环境配置 ELK收集nginx日志有多种方案&#xff0c;一般比较常见的做法是在生产环境服务器搭建filebeat 收集nginx的文件日志并写入到队列&#xff08;k…

图解 STP

网络环路 现在我们的生活已经离不开网络&#xff0c;如果我家断网&#xff0c;我会抱怨这什么破网络&#xff0c;影响到我刷抖音、打游戏&#xff1b;如果公司断网&#xff0c;那老板估计会骂娘&#xff0c;因为会影响到公司正常运转&#xff0c;直接造成经济损失。网络通信中&…

传统分拣弊端明显,AI机器视觉赋能物流行业包裹分类产线数智化升级

随着电子商务的快速发展&#xff0c;物流行业的包裹数量持续增长&#xff0c;给物流企业带来了巨大的运营压力。目前&#xff0c;国内大型物流运转中心已开始采用机器视觉自动化设备&#xff0c;但多数快递公司处于半自动化状态&#xff0c;中小型物流分拣中心目前仍靠人工录入…

iOS系统修复软件 Fix My iPhone for Mac

Fix My iPhone for Mac是一款iOS系统恢复工具。修复您的iPhone卡在Apple徽标&#xff0c;黑屏&#xff0c;冻结屏幕&#xff0c;iTunes更新/还原错误和超过20个iOS 12升级失败。这个macOS桌面应用程序提供快速&#xff0c;即时的解决方案来修复您的iOS系统问题&#xff0c;而不…

基于单片机的遥控器设计

一、项目介绍 随着科技的不断发展&#xff0c;红外遥控器已经成为我们日常生活中普遍使用的一种电子设备。它能够给我们带来便捷和舒适&#xff0c;减少人工操作的繁琐性。然而&#xff0c;在实际应用中&#xff0c;有时候我们可能需要制作一个自己的红外遥控器&#xff0c;以…

【云原生进阶之PaaS中间件】第一章Redis-2.4缓存更新机制

1 缓存和数据库的数据一致性分析 1.1 Redis 中如何保证缓存和数据库双写时的数据一致性&#xff1f; 无论先操作db还是cache&#xff0c;都会有各自的问题&#xff0c;根本原因是cache和db的更新不是一个原子操作&#xff0c;因此总会有不一致的问题。想要彻底解决这种问题必须…

目标检测YOLO算法,先从yolov1开始

学习资源 有一套配套的学习资料&#xff0c;才能让我们的学习事半功倍。 yolov1论文原址&#xff1a;You Only Look Once: Unified, Real-Time Object Detection 代码地址&#xff1a;darknet: Convolutional Neural Networks (github.com) 深度学习经典检测方法 one-stag…

React 18 对 state 进行保留和重置

参考文章 对 state 进行保留和重置 各个组件的 state 是各自独立的。根据组件在 UI 树中的位置&#xff0c;React 可以跟踪哪些 state 属于哪个组件。可以控制在重新渲染过程中何时对 state 进行保留和重置。 UI 树 浏览器使用许多树形结构来为 UI 建立模型。DOM 用于表示 …

3DCAT携手华为,打造XR虚拟仿真实训实时云渲染解决方案

2023年5月8日-9日&#xff0c;以 因聚而生 众志有为 为主题的 华为中国合作伙伴大会2023 在深圳国际会展中心隆重举行。本次大会汇聚了ICT产业界的广大新老伙伴朋友&#xff0c;共同探讨数字化转型的新机遇&#xff0c;共享数字化未来的新成果。 华为中国合作伙伴大会2023现场&…

Python小知识 - 使用Python进行数据分析

使用Python进行数据分析 数据分析简介 数据分析&#xff0c;又称为信息分析&#xff0c;是指对数据进行综合处理、归纳提炼、概括总结的过程&#xff0c;是数据处理的第一步。 数据分析的目的是了解数据的内在规律&#xff0c;为数据挖掘&#xff0c;并应用于商业决策、科学研究…

04ShardingSphere-JDBC垂直分片

1、准备服务器 比如商城项目中&#xff0c;有用户、订单等系统&#xff0c;数据库在设计时用户信息与订单信息在同一表中。这里创建用户服务、订单服务实现数据库的用户信息和订单信息垂直分片 服务器规划&#xff1a;使用docker方式创建如下容器 服务器&#xff1a;容器名se…

P13 VUE 二级menu实现

主要修改以下几个点&#xff1a; CommonAside.vue中 外层便利有孩子节点&#xff0c;关键词key是对应的标签&#xff0c;class动态图表渲染 内层遍历不能再用item&#xff0c;用subitem&#xff0c;遍历该item.childeren&#xff0c;关键词是path&#xff0c; <templat…

java 批量下载将多个文件(minio中存储)压缩成一个zip包

我的需求是将minio中存储的文件按照查询条件查询出来统一压成一个zip包然后下载下来。 思路&#xff1a;针对这个需求&#xff0c;其实可以有多个思路&#xff0c;不过也大同小异&#xff0c;一般都是后端返回流文件前端再处理下载&#xff0c;也有少数是压缩成zip包之后直接给…

【C++设计模式】详解装饰模式

2023年8月31日&#xff0c;周四上午 这是我目前碰到的最难的设计模式..... 非常难以理解而且比较灵活多半&#xff0c;学得贼难受&#xff0c;写得贼费劲..... 2023年8月31日&#xff0c;周四晚上19:48 终于写完了&#xff0c;花了一天的时间来学习装饰模式和写这篇博客。 …

《Kubernetes部署篇:Ubuntu20.04基于二进制安装安装kubeadm、kubelet和kubectl》

一、背景 由于客户网络处于专网环境下&#xff0c; 使用kubeadm工具安装K8S集群&#xff0c;由于无法连通互联网&#xff0c;所有无法使用apt工具安装kubeadm、kubelet、kubectl&#xff0c;当然你也可以使用apt-get工具在一台能够连通互联网环境的服务器上下载kubeadm、kubele…

说说Kappa架构

分析&回答 对于实时数仓而言&#xff0c;Lmabda架构有很明显的不足&#xff0c;首先同时维护两套系统&#xff0c;资源占用率高&#xff0c;其次这两套系统的数据处理逻辑相同&#xff0c;代码重复开发。 能否有一种架构&#xff0c;只需要维护一套系统&#xff0c;就可以…

【核磁共振成像】相位差重建

目录 一、相位差map重建一般步骤和反正切函数主值范围二、反正切运算三、可预期相位误差和伴随场的校正四、图形变形校正 一、相位差map重建一般步骤和反正切函数主值范围 MRI是一个相敏成像模态&#xff0c;MR原始数据傅里叶变换后的复数图像中每个像素值有模和相位。标准模重…

HTML5

写在前面 一、开个头 安装vscode 1.1 什么是网页 网站是指因特网上根据一定的规则&#xff0c;使用HTML等制作的用于展示特定内容相关的网页集合。 网页是网站中的一“页”&#xff0c;通常是HTML格式的文件&#xff0c;它要通过浏览器来阅读。 网页是构成网站的基本元素…