vue3 实现贪吃蛇手机版01

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>
相关推荐
asdfsdgss3 小时前
Angular CDK 自适应布局技巧:响应式工具实操手册
前端·javascript·angular.js
Momentary_SixthSense3 小时前
rust笔记
开发语言·笔记·rust
爱吃的强哥3 小时前
Electron_Vue3 自定义系统托盘及退出二次确认
前端·javascript·electron
多多*3 小时前
Spring Bean的生命周期 第二次思考
java·开发语言·rpc
大飞pkz3 小时前
【算法】排序算法汇总1
开发语言·数据结构·算法·c#·排序算法
Swift社区4 小时前
Foundation Model 在 Swift 中的类型安全生成实践
开发语言·安全·swift
草明4 小时前
当 Go 的 channel 被 close 后读写操作会怎么样?
开发语言·后端·golang
AI_56784 小时前
脑科学支持的Python学习法:每天2小时碎片化训练,用‘神经可塑性’打败拖延症“
开发语言·python·学习
技术小丁4 小时前
零依赖!教你用原生 JS 把 JSON 数组秒变 CSV 文件
前端·javascript