html
复制代码
<template>
<div class="snake-game">
<!-- 游戏标题和分数 -->
<div class="game-header">
<h1>贪吃蛇</h1>
<div class="score-panel">
<div class="current-score">分数: {{ currentScore }}</div>
<div class="high-score">最高分: {{ highScore }}</div>
</div>
</div>
<!-- 游戏区域 -->
<div class="game-area">
<div class="game-board" :style="{ width: `${boardSize}px`, height: `${boardSize}px` }">
<!-- 蛇身(绝对定位) -->
<div
v-for="(segment, index) in snake"
:key="index"
class="snake-segment"
:class="{ 'snake-head': index === 0 }"
:style="{
left: `${segment.x * cellSize}px`,
top: `${segment.y * cellSize}px`,
width: `${cellSize}px`,
height: `${cellSize}px`
}"
></div>
<!-- 食物(绝对定位,固定尺寸) -->
<div
class="food"
v-if="food"
:style="{
left: `${food.x * cellSize}px`,
top: `${food.y * cellSize}px`,
width: `${cellSize}px`,
height: `${cellSize}px`
}"
></div>
<!-- 游戏开始界面 -->
<div class="game-start" v-if="!isPlaying && !gameOver">
<button @click="startGame" class="start-btn">开始游戏</button>
</div>
<!-- 游戏结束界面 -->
<div class="game-over" v-if="gameOver">
<h2>游戏结束</h2>
<p>最终得分: {{ currentScore }}</p>
<button @click="startGame" class="restart-btn">再来一局</button>
</div>
</div>
</div>
<!-- 虚拟方向按键 -->
<div class="control-pad" v-if="isPlaying || gameOver">
<div class="control-row top-row">
<button
class="control-btn up"
@click="setDirection(0, -1)"
@touchstart.prevent="setDirection(0, -1)"
>
↑
</button>
</div>
<div class="control-row middle-row">
<button
class="control-btn left"
@click="setDirection(-1, 0)"
@touchstart.prevent="setDirection(-1, 0)"
>
←
</button>
<button
class="control-btn down"
@click="setDirection(0, 1)"
@touchstart.prevent="setDirection(0, 1)"
>
↓
</button>
<button
class="control-btn right"
@click="setDirection(1, 0)"
@touchstart.prevent="setDirection(1, 0)"
>
→
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
// 游戏配置
const gridSize = 15; // 游戏网格大小(15x15)
const initialSpeed = 250; // 初始速度(毫秒)
const speedIncrease = 5; // 每吃一个食物加速的毫秒数
// 响应式计算游戏板和单元格大小
const boardSize = computed(() => {
return Math.min(window.innerWidth * 0.9, 400); // 游戏板大小
});
const cellSize = computed(() => {
return boardSize.value / gridSize; // 每个单元格的像素大小(固定)
});
// 游戏状态
const snake = ref([{ x: 7, y: 7 }]); // 蛇的初始位置(网格坐标)
const food = ref(null); // 食物位置(网格坐标)
const direction = ref({ x: 1, y: 0 }); // 当前方向(初始向右)
const nextDirection = ref({ ...direction.value }); // 下一次移动的方向
const currentScore = ref(0); // 当前分数
const highScore = ref(0); // 最高分
const isPlaying = ref(false); // 是否正在游戏中
const gameOver = ref(false); // 游戏是否结束
const gameInterval = ref(null); // 游戏循环计时器
// 初始化游戏
const startGame = () => {
// 重置游戏状态
snake.value = [{ x: 7, y: 7 }];
direction.value = { x: 1, y: 0 };
nextDirection.value = { ...direction.value };
currentScore.value = 0;
gameOver.value = false;
isPlaying.value = true;
// 生成食物
generateFood();
// 开始游戏循环
if (gameInterval.value) clearInterval(gameInterval.value);
gameInterval.value = setInterval(moveSnake, initialSpeed);
};
// 生成食物(固定在网格坐标上)
const generateFood = () => {
let newFood;
// 确保食物不会出现在蛇身上
do {
newFood = {
x: Math.floor(Math.random() * gridSize), // 网格X坐标(0-14)
y: Math.floor(Math.random() * gridSize) // 网格Y坐标(0-14)
};
} while (snake.value.some(segment => segment.x === newFood.x && segment.y === newFood.y));
food.value = newFood; // 食物坐标一旦生成,不会随蛇移动改变
};
// 移动蛇(基于网格坐标计算)
const moveSnake = () => {
if (!isPlaying.value) return;
// 更新方向
direction.value = { ...nextDirection.value };
// 创建新头部(基于网格坐标计算)
const head = {
x: snake.value[0].x + direction.value.x,
y: snake.value[0].y + direction.value.y
};
// 检查是否碰撞
if (checkCollision(head)) {
endGame();
return;
}
// 将新头部添加到蛇身
snake.value.unshift(head);
// 检查是否吃到食物(基于网格坐标匹配)
if (head.x === food.value.x && head.y === food.value.y) {
currentScore.value += 10;
if (currentScore.value > highScore.value) {
highScore.value = currentScore.value;
localStorage.setItem('snakeHighScore', highScore.value);
}
generateFood(); // 生成新食物(新的网格坐标)
adjustSpeed();
} else {
snake.value.pop(); // 没吃到食物则移除尾部
}
};
// 检查碰撞
const checkCollision = (head) => {
// 撞墙(超出网格范围)
if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {
return true;
}
// 撞到自己
return snake.value.some((segment, index) => index !== 0 && segment.x === head.x && segment.y === head.y);
};
// 调整游戏速度
const adjustSpeed = () => {
if (gameInterval.value) {
clearInterval(gameInterval.value);
const newSpeed = Math.max(initialSpeed - (currentScore.value / 10) * speedIncrease, 100);
gameInterval.value = setInterval(moveSnake, newSpeed);
}
};
// 结束游戏
const endGame = () => {
isPlaying.value = false;
gameOver.value = true;
clearInterval(gameInterval.value);
};
// 设置方向
const setDirection = (x, y) => {
if ((direction.value.x === -x && direction.value.y === -y) || !isPlaying.value) {
return;
}
nextDirection.value = { x, y };
};
// 键盘控制(调试用)
const handleKeydown = (e) => {
if (!isPlaying.value) return;
switch(e.key) {
case 'ArrowUp': setDirection(0, -1); break;
case 'ArrowDown': setDirection(0, 1); break;
case 'ArrowLeft': setDirection(-1, 0); break;
case 'ArrowRight': setDirection(1, 0); break;
}
};
// 加载最高分
const loadHighScore = () => {
const saved = localStorage.getItem('snakeHighScore');
if (saved) highScore.value = parseInt(saved);
};
// 生命周期
onMounted(() => {
loadHighScore();
window.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown);
if (gameInterval.value) clearInterval(gameInterval.value);
});
</script>
<style scoped>
.snake-game {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
background-color: #f0f2f5;
padding: 20px 10px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 游戏标题和分数 */
.game-header {
width: 100%;
max-width: 400px;
margin-bottom: 20px;
text-align: center;
}
.game-header h1 {
color: #1a1a1a;
margin: 0 0 15px 0;
font-size: 28px;
}
.score-panel {
display: flex;
justify-content: space-around;
background-color: white;
padding: 12px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.current-score, .high-score {
font-size: 16px;
font-weight: 500;
color: #333;
}
/* 游戏区域 */
.game-area {
position: relative;
margin-bottom: 30px;
}
.game-board {
position: relative; /* 作为蛇和食物的定位容器 */
background-color: #e6f7ff;
border: 2px solid #1890ff;
border-radius: 8px;
overflow: hidden;
}
/* 蛇身和食物使用绝对定位 */
.snake-segment, .food {
position: absolute;
box-sizing: border-box;
}
.snake-segment {
background-color: #52c41a;
border-radius: 4px;
transition: all 0.1s ease;
}
.snake-head {
background-color: #2e7d32;
border: 2px solid #1b5e20;
}
.food {
background-color: #ff4d4f;
border-radius: 50%;
box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.5) inset;
}
/* 游戏开始和结束界面 */
.game-start, .game-over {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 6px;
color: white;
}
.game-over h2 {
margin: 0 0 15px 0;
font-size: 24px;
}
.game-over p {
margin: 0 0 20px 0;
font-size: 18px;
}
.start-btn, .restart-btn {
background-color: #1890ff;
color: white;
border: none;
border-radius: 6px;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.start-btn:hover, .restart-btn:hover {
background-color: #096dd9;
}
.start-btn:active, .restart-btn:active {
transform: scale(0.98);
}
/* 虚拟方向按键 */
.control-pad {
width: 240px;
height: 200px;
position: relative;
margin-top: auto;
margin-bottom: 20px;
}
.control-row {
display: flex;
justify-content: center;
}
.top-row {
margin-bottom: 10px;
}
.middle-row {
gap: 10px;
}
.control-btn {
width: 70px;
height: 70px;
border-radius: 10px;
background-color: #1890ff;
color: white;
border: none;
font-size: 20px;
font-weight: bold;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.control-btn:active {
transform: scale(0.95);
background-color: #096dd9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 适配小屏幕 */
@media (max-width: 360px) {
.control-pad {
width: 200px;
height: 170px;
}
.control-btn {
width: 60px;
height: 60px;
font-size: 18px;
}
.game-header h1 {
font-size: 24px;
}
}
</style>