
<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>
<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>