1. 预览
2. 实现思路
实现食物类,食物坐标和刷新食物的位置,以及获取食物的坐标点; 实现计分面板类,实现吃食物每次的计分以及积累一定程度的等级,实现等级和分数的增加; 实现蛇类,蛇类分为蛇头和蛇身,蛇头有方向,蛇身每一节跟着前一节移动; 实现控制器类,初始化上边实现的各个类,同时绑定键盘事件,实现方向的修改; 使用 requestAnimationFrame 实现页面刷新,蛇移动,坐标点的更新。
3. 食物类
接口 Point 的实现,用于返回食物的坐标点; maxX 和 maxY 用于记录横向和纵向的最大坐标值; 实现 change 实现食物的刷新; 3.1 传入当前蛇的坐标点,用于判断刷新的点是否是蛇中间的坐标点; 3.2 使用 Math.round 和 Math.random 生成一个食物坐标点 (x,y); 3.3 过滤 snakes 蛇坐标点,看看该点是否存在蛇中; 3.4 如果存在,continue 跳出该次循环,继续生成新的坐标,走3.2,3.3流程; 3.5 不存在,就保存该坐标点,跳出循环,完成此次坐标的生成。 实现 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. 计分面板类
初始化分数、等级、最大等级、每次升级所需要的分数; 分数增长 addScore 实现: 2.1 分数自增 ++this.score; 2.2 判断自增后的分数是否满足升级条件 this.score % this.upLevelScore === 0; 2.3 满足升级条件,调用升级方法 addLevel; 等级增加 addLevel 实现: 3.1 判断当前等级是否小于最高等级; 3.2 满足条件,进行等级升级; 实现当前等级和分数的获取 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 全局变量
由于蛇是一个列表,因此需要一个key的id,因此 generateId 作为id生成器; DIRECTION_RIGHT、DIRECTION_DOWN、DIRECTION_LEFT、DIRECTION_UP常量定义; 接口 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 蛇头类
初始化蛇头坐标(x,y),方向 direction,唯一标识key的id; 蛇头移动函数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。 修改方向更新 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 蛇身类
初始化每节蛇身的坐标点和唯一标识; 实现蛇身的移动,当前坐标点是前移节的坐标。
// 蛇身
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 蛇类
初始化蛇列表、蛇头、蛇身最后一节、每次吃食物蛇身的增长长度、记录当前次吃食物身体增长的长度、蛇能存在的最大坐标; 初始化蛇: 2.1 创建蛇的坐标点列表; 2.2 创建蛇头函数执行; 2.3 创建蛇身函数执行。 修改蛇的行进方向改变实现 setDirection,直接调用蛇头的修改方向方法; 获取两个数之间的随机值 random 方法实现; 创建蛇头函数 createHead 实现: 5.1 获取坐标随机值的最大值maxX的一半; 5.2 由于初始化蛇身最少三节,因此对最小值处理必须大于0; 5.3 创建蛇头 snakeHead; 5.4 将蛇头对象存放到蛇的列表中。 蛇身 createBodies 实现: 6.1 蛇身最少三节,因此循环产生每一节蛇身; 6.2 创建每一节蛇身,对y坐标,不做处理,使用蛇头的x坐标一次递减。 蛇移动函数 move 实现: 7.1 获取移动前左后一节蛇身对象; 7.2 记录最后一节蛇身对象的坐标用于吃食物后身体的增长; 7.3 由于移动防止脱节,因此从最后一节一次往前一节循环移动; 7.4 由于蛇身的移动需要获取前一节蛇身的坐标,因此移动时,传入前一节蛇身对象; 7.5 移动完成,检测当前移动后蛇头是否碰撞墙或者撞到自身。 撞墙检测函数 hasOver 实现: 8.1 获取蛇头坐标,看看是否超出盒子的x,y的范围; 撞自身检测函数 hasSelf实现: 9.1 因为蛇头不可能撞到自己蛇头,因此去掉蛇头,从1开始遍历蛇身坐标; 9.2 如果存在有蛇身坐标和蛇头坐标相同,说明撞到自己,结束游戏。 吃掉食物判断函数 eatFood: 10.1 传入当前食物的坐标值; 10.2 判断食物坐标和蛇头坐标是否重合; 10.3 更新增长身体增长变量; 10.4 返回吃到食物未 true; 10.5 否则说明没有吃到食物,返回 false。 获取最新蛇的全部坐标点函数 getPoints: 11.1 返回蛇的坐标点和唯一标识id。 设置每次吃食物蛇的增长长度 setUpdateBodyNumber 函数实现; 实现蛇的增长函数 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. 控制器类
初始化食物、蛇、计分面板; 获取初始化时蛇的坐标列表; 调用食物刷新方法,刷新食物坐标; 绑定键盘事件: 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 接口实现
坐标点接口 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 逻辑变量和渲染变量初始化
foodAndSnake 存储食物类对象、蛇对象、控制器对象,是否第一次执行等做逻辑处理,不需要界面渲染; 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 初始化
useEffect 监听 onmount 的时候,初始化页面; init 初始化各个类对象: 2.1 初始化 GameContral 游戏控制器类; 2.2 赋值食物类对象; 2.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 监听动画实现
对蛇进行移动,调用蛇对象的移动函数; 调用蛇对象的 eatFood 判断是否吃食物; 吃了食物,进行刷新; 如果吃了食物,进行加分; 更新身体长度; 更新蛇坐标列表,食物坐标,计分面板信息; 如果上边流程出现异常报错,直接结束游戏,游戏结束的逻辑在异常处处理; 随着等级的提升,速度越来越快。【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 蛇和食物的绘制
不同屏幕的计算函数 handleSize; 食物绘制函数 drawFood 实现,通过食物坐标进行定位; 蛇列表绘制函数 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. 总结
最开准备使用 30 * 30 个格子,进行判断渲染,渲染数据每次更改太多,渲染时间很久,因此不建议使用; 为什么每次设置渲染数据都要筛选一次,因为直接将类渲染,数据随着蛇成长,会越来越大,很卡。