思路
1.盒子通过计算得出大小
2.蛇的核心通过设置新的position位置去更新蛇状态
3.将设计好的食物传进去判断吃食物,更新蛇状态
4.代码很多地方可优化,例如食物刷新的位置要加蛇的位置判断,蛇按键监听的判断需要优化,主要记录一下大概思路
5.项目是vue3+TS,但并没有使用vue3响应式的相关API,方便可以跑在别的框架里
代码
<template>
<div class="GluttonousSnake" ref="GluttonousSnakeBox">
<div class="gameBox" ref="gameBoxRef"></div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Food, Snake, initBoxDom } from "./GluttonousSnake"
const GluttonousSnakeBox = ref()
const gameBoxRef = ref()
onMounted(() => {
init()
})
let newSnake: Snake
let newFood: Food
const init = () => {
initBoxDom(gameBoxRef.value, GluttonousSnakeBox.value)
newFood = new Food()
newSnake = new Snake(newFood)
}
</script>
<style lang="scss" scoped>
.GluttonousSnake {
height: 100%;
position: relative;
:deep(.gameBox) {
border: 1px solid red;
box-sizing: border-box;
// position: relative;
.snake {
position: absolute;
transition: all 0.5s;
}
.food {
position: absolute;
}
}
}
</style>
type Direction = 'top' | 'bottom' | 'left' | 'right'
const HEIGHT = 20
const WIDTH = 20
let BoxDom: HTMLElement
export const initBoxDom = (dom: HTMLElement, GluttonousSnakeBox: HTMLElement) => {
BoxDom = dom
BoxDom.style.width = `${GluttonousSnakeBox.offsetWidth - GluttonousSnakeBox.offsetWidth % WIDTH}px`
BoxDom.style.height = `${GluttonousSnakeBox.offsetHeight - GluttonousSnakeBox.offsetHeight % HEIGHT}px`
}
export class Snake {
_food: Food | undefined
_position: [number, number] = [0, 3]
_snakeArr = [[0, 3], [0, 2], [0, 1]]
height = HEIGHT
width = WIDTH
length = this._snakeArr.length
direction: Direction = 'right' //方向
speedTime = 500
runSetInterval: number | undefined
constructor(food: Food) {
this._food = food
this.run()
this.onKeyWord()
}
set position(newValue: [number, number]) {
this._position = newValue
this.updata_snakeArr()
const flag = this.isLife()
if (flag) {
this.eatFood()
this.show_snakeArr()
} else {
clearInterval(this.runSetInterval)
alert('game over')
}
}
get position() {
return this._position
}
updata_snakeArr() {
if (this._snakeArr.length >= this.length) {
this._snakeArr.pop()
}
this._snakeArr.splice(0, 0, this._position)
}
show_snakeArr() {
const snakeDom = document.querySelectorAll('.snake')
snakeDom.forEach((item) => {
const x = item.getAttribute('x');
const y = item.getAttribute('y');
(item as HTMLElement).style.backgroundColor = 'red'
const flag = this._snakeArr.some(snake => {
if (snake[0] === Number(x) && snake[1] === Number(y)) {
return true
}
})
if (!flag) {
item.remove()
}
})
this._snakeArr.forEach((snake, index) => {
let Flag = true
snakeDom.forEach(dom => {
const x = dom.getAttribute('x')
const y = dom.getAttribute('y')
if (snake[0] === Number(x) && snake[1] === Number(y)) {
Flag = false
}
})
if (Flag) {
const position = snake
const snakeDom = document.createElement('div')
snakeDom.style.width = `${this.width}px`
snakeDom.style.height = `${this.height}px`
snakeDom.style.backgroundColor = index === 0 ? 'yellow' : 'red'
snakeDom.className = 'snake'
snakeDom.setAttribute('x', String(position[0]))
snakeDom.setAttribute('y', String(position[1]))
snakeDom.style.top = `${position[0] * this.height}px`
snakeDom.style.left = `${position[1] * this.width}px`
BoxDom.appendChild(snakeDom)
}
})
}
run() {
this.runSetInterval = setInterval(() => {
let newPosition: [number, number] = [0, 0]
switch (this.direction) {
case 'top':
newPosition = [this._position[0] - 1, this._position[1]]
break;
case 'bottom':
newPosition = [this._position[0] + 1, this._position[1]]
break;
case 'left':
newPosition = [this._position[0], this._position[1] - 1]
break;
case 'right':
newPosition = [this._position[0], this._position[1] + 1]
break;
default:
break;
}
this.position = newPosition
}, this.speedTime)
}
isLife() {
const [SnakeTop, SnakeLeft] = this._position
const BoxBottom = BoxDom.offsetHeight
const BoxRight = BoxDom.offsetWidth
if (SnakeTop * this.height < 0 || SnakeLeft * this.width < 0 || (SnakeTop + 1) * this.height > BoxBottom || (SnakeLeft + 1) * this.width > BoxRight) {
return false
} else {
let Flag = true
if (this.length > 4) {
Flag = !this._snakeArr.slice(1).some(item => {
const [x, y] = item
if (x === SnakeTop && y === SnakeLeft) {
return true
}
})
}
return Flag
}
}
onKeyWord() {
document.body.onkeydown = (e) => {
const ev = e || window.event;
switch (ev.keyCode) {
case 38:
if (this.direction != 'bottom') { // 不允许返回,向上的时候不能向下
this.direction = "top";
}
break;
case 40:
if (this.direction != "top") {
this.direction = "bottom";
}
break;
case 37:
if (this.direction != "right") {
this.direction = "left";
}
break;
case 39:
if (this.direction != "left") {
this.direction = "right";
}
break;
}
};
}
eatFood() {
if (this._food?.position[0] === this._position[0] && this._food?.position[1] === this._position[1]) {
this.length += 1
this._food?.rest()
}
}
}
export class Food {
_position: [number, number] = [0, 0]
width = WIDTH
height = HEIGHT
constructor() {
this.createPosition()
}
set position(newValue: [number, number]) {
this._position = newValue
this.show_Food()
}
get position() {
return this._position
}
show_Food() {
const position = this._position
const foodDom = document.createElement('div')
foodDom.style.width = `${this.width}px`
foodDom.style.height = `${this.height}px`
foodDom.style.backgroundColor = 'red'
foodDom.className = 'food'
foodDom.setAttribute('x', String(position[0]))
foodDom.setAttribute('y', String(position[1]))
foodDom.style.top = `${position[0] * this.height}px`
foodDom.style.left = `${position[1] * this.width}px`
BoxDom.appendChild(foodDom)
}
createPosition() {
const heightX = BoxDom.offsetHeight / HEIGHT
const widthY = BoxDom.offsetWidth / WIDTH
const x = this.random(0, heightX)
const y = this.random(0, widthY)
this.position = [x, y]
}
rest() {
const foodDom = document.querySelector('.food')
foodDom?.remove()
this.createPosition()
}
random(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
}