html
复制代码
<template>
<div class="game-container">
<div class="game-header">
<h1>贪吃蛇</h1>
<div class="scores">
<div>分数: {{ score }}</div>
<div>最高分: {{ highScore }}</div>
</div>
</div>
<!-- 游戏区域 -->
<div class="game-wrapper">
<div class="game-board" :style="{
width: `${boardSize}px`,
height: `${boardSize}px`
}">
<!-- 蛇身 -->
<div
v-for="(segment, index) in snake"
:key="index"
class="snake-segment"
:style="{
left: `${segment.x * cellSize}px`,
top: `${segment.y * cellSize}px`,
width: `${cellSize}px`,
height: `${cellSize}px`,
background: index === 0 ? 'darkgreen' : 'green'
}"
></div>
<!-- 食物 -->
<div
v-if="food"
class="food"
:style="{
left: `${food.x * cellSize}px`,
top: `${food.y * cellSize}px`,
width: `${cellSize}px`,
height: `${cellSize}px`
}"
></div>
<!-- 游戏结束提示 -->
<div v-if="gameOver" class="game-over">
<h2>游戏结束!</h2>
<p>最终得分: {{ score }}</p>
<button @click="startGame">再来一局</button>
</div>
<!-- 开始游戏按钮 -->
<div v-if="!isPlaying && !gameOver" class="start-screen">
<button @click="startGame">开始游戏</button>
</div>
</div>
</div>
<!-- 虚拟摇杆控制器(缩小50%) -->
<div class="controller" v-if="isPlaying || gameOver">
<div class="joystick-outer"
@touchstart="handleJoystickStart"
@touchmove="handleJoystickMove"
@touchend="handleJoystickEnd"
@touchcancel="handleJoystickEnd">
<!-- 拖拽轨迹指示器 -->
<div class="joystick-track"
v-if="joystickActive"
:style="{
width: `${distance * 2}px`,
height: `${distance * 2}px`,
left: `${joystickCenter.x - distance}px`,
top: `${joystickCenter.y - distance}px`,
transform: `rotate(${angle}deg)`
}"></div>
<div class="joystick-inner"
:style="{
left: `${joystickPosition.x}px`,
top: `${joystickPosition.y}px`,
transform: `translate(-50%, -50%) scale(${joystickActive ? 1.1 : 1})`
}"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
// 响应式计算游戏板大小(适应不同屏幕)
const boardSize = computed(() => {
// 取屏幕宽度的90%,但不超过500px
const maxSize = Math.min(window.innerWidth * 0.9, 500);
// 确保是单元格大小的整数倍
return Math.floor(maxSize / 20) * 20;
});
// 游戏配置
const gridSize = 20; // 网格数量
const cellSize = computed(() => boardSize.value / gridSize); // 每个格子的像素大小
const initialSpeed = 200; // 初始速度(毫秒)
// 游戏状态
const snake = ref([{ x: 10, y: 10 }]); // 蛇的身体,初始位置
const food = ref(null); // 食物位置
const direction = ref({ x: 1, y: 0 }); // 移动方向,初始向右
const nextDirection = ref({ ...direction.value }); // 下一次移动方向
const score = ref(0); // 当前分数
const highScore = ref(0); // 最高分
const isPlaying = ref(false); // 是否正在游戏中
const gameOver = ref(false); // 游戏是否结束
const gameLoop = ref(null); // 游戏循环计时器
// 虚拟摇杆状态(尺寸缩小50%)
const joystickActive = ref(false);
const joystickPosition = ref({ x: 0, y: 0 });
const joystickCenter = ref({ x: 0, y: 0 });
const joystickRadius = 30; // 摇杆半径(原60px,缩小50%)
const distance = ref(0); // 摇杆偏离中心的距离
const angle = ref(0); // 摇杆角度(用于轨迹显示)
// 初始化游戏
const startGame = () => {
// 重置游戏状态
snake.value = [{ x: 10, y: 10 }];
direction.value = { x: 1, y: 0 };
nextDirection.value = { ...direction.value };
score.value = 0;
gameOver.value = false;
isPlaying.value = true;
// 生成初始食物
generateFood();
// 开始游戏循环
if (gameLoop.value) clearInterval(gameLoop.value);
gameLoop.value = setInterval(moveSnake, initialSpeed);
};
// 生成食物
const generateFood = () => {
// 确保食物不会出现在蛇身上
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * gridSize),
y: Math.floor(Math.random() * gridSize)
};
} 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) {
score.value += 10;
generateFood();
} 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) => {
return index !== 0 && segment.x === head.x && segment.y === head.y;
});
};
// 结束游戏
const endGame = () => {
isPlaying.value = false;
gameOver.value = true;
clearInterval(gameLoop.value);
// 更新最高分
if (score.value > highScore.value) {
highScore.value = score.value;
}
};
// 虚拟摇杆控制(增强拖拽效果)
const handleJoystickStart = (e) => {
e.preventDefault();
if (!isPlaying.value) return;
// 获取摇杆外框的位置和尺寸
const rect = e.currentTarget.getBoundingClientRect();
joystickCenter.value = {
x: rect.width / 2,
y: rect.height / 2
};
// 设置初始位置为中心
joystickPosition.value = { ...joystickCenter.value };
joystickActive.value = true;
// 处理初始触摸
handleJoystickMove(e);
};
const handleJoystickMove = (e) => {
e.preventDefault();
if (!joystickActive.value || !isPlaying.value) return;
// 获取触摸位置
const touch = e.touches[0];
const rect = e.currentTarget.getBoundingClientRect();
const touchX = touch.clientX - rect.left;
const touchY = touch.clientY - rect.top;
// 计算相对于中心的偏移
const deltaX = touchX - joystickCenter.value.x;
const deltaY = touchY - joystickCenter.value.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 更新拖拽轨迹参数
distance.value = Math.min(dist, joystickRadius);
angle.value = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
// 限制在摇杆范围内
let constrainedX, constrainedY;
if (dist <= joystickRadius) {
constrainedX = touchX;
constrainedY = touchY;
} else {
// 计算限制在圆内的位置
const ratio = joystickRadius / dist;
constrainedX = joystickCenter.value.x + deltaX * ratio;
constrainedY = joystickCenter.value.y + deltaY * ratio;
}
// 更新摇杆位置
joystickPosition.value = { x: constrainedX, y: constrainedY };
// 根据偏移方向确定蛇的移动方向
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平方向
if (deltaX > 0 && direction.value.x !== -1) {
nextDirection.value = { x: 1, y: 0 }; // 右
} else if (deltaX < 0 && direction.value.x !== 1) {
nextDirection.value = { x: -1, y: 0 }; // 左
}
} else {
// 垂直方向
if (deltaY > 0 && direction.value.y !== -1) {
nextDirection.value = { x: 0, y: 1 }; // 下
} else if (deltaY < 0 && direction.value.y !== 1) {
nextDirection.value = { x: 0, y: -1 }; // 上
}
}
};
const handleJoystickEnd = (e) => {
e.preventDefault();
if (!joystickActive.value) return;
// 重置摇杆状态
joystickPosition.value = { ...joystickCenter.value };
joystickActive.value = false;
distance.value = 0; // 重置轨迹
};
// 监听窗口大小变化
const handleResize = () => {
// 重新计算boardSize会触发响应式更新
};
// 生命周期钩子
onMounted(() => {
window.addEventListener('resize', handleResize);
// 初始化摇杆位置
joystickPosition.value = { x: joystickRadius, y: joystickRadius };
joystickCenter.value = { x: joystickRadius, y: joystickRadius };
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (gameLoop.value) clearInterval(gameLoop.value);
});
// 监听游戏状态变化
watch(isPlaying, (newVal) => {
if (!newVal && gameLoop.value) {
clearInterval(gameLoop.value);
}
});
</script>
<style scoped>
.game-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
box-sizing: border-box;
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.game-header {
width: 100%;
max-width: 500px;
text-align: center;
margin-bottom: 20px;
}
.game-header h1 {
color: #333;
margin: 0 0 15px 0;
font-size: 1.8rem;
}
.scores {
display: flex;
justify-content: space-around;
font-size: 1.1rem;
color: #555;
background-color: #fff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.game-wrapper {
position: relative;
margin-bottom: 40px;
}
.game-board {
position: relative;
border: 2px solid #333;
background-color: #e8f5e9;
border-radius: 4px;
overflow: hidden;
}
.snake-segment {
position: absolute;
box-sizing: border-box;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.food {
position: absolute;
box-sizing: border-box;
background-color: #e53935;
border-radius: 50%;
border: 1px solid #c62828;
}
.game-over {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
}
.game-over h2 {
margin: 0;
font-size: 1.5rem;
}
.game-over p {
font-size: 1.2rem;
margin: 0;
}
.start-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
button {
padding: 12px 24px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #388e3c;
}
/* 虚拟摇杆样式(缩小50%) */
.controller {
width: 100px; /* 原200px,缩小50% */
height: 100px; /* 原200px,缩小50% */
position: relative;
}
.joystick-outer {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 50%;
position: relative;
touch-action: none; /* 防止触摸时页面滚动 */
border: 2px solid rgba(0,0,0,0.2);
}
/* 拖拽轨迹指示器 */
.joystick-track {
position: absolute;
background-color: rgba(76, 175, 80, 0.2);
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
.joystick-inner {
position: absolute;
background-color: #4caf50;
border-radius: 50%;
width: 60%; /* 相对于外框的比例 */
height: 60%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
transition: transform 0.1s ease;
z-index: 2;
}
/* 适配小屏幕设备 */
@media (max-width: 360px) {
.game-header h1 {
font-size: 1.5rem;
}
.scores {
font-size: 1rem;
}
}
</style>