
<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`}"><divv-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><divv-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><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(() => {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);
const joystickActive = ref(false);
const joystickPosition = ref({ x: 0, y: 0 });
const joystickCenter = ref({ x: 0, y: 0 });
const joystickRadius = 30;
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 = () => {
};
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;
}
.controller {width: 100px; height: 100px; 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>