丝滑的贪吃蛇

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>贪吃蛇游戏</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
      background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
    }
    canvas {
      border: 1px solid #000;
    }
    #menu {
      height: 50px;
      width: 600px;
      margin-bottom: 5px;
      border-radius: 5px;
      background-image: radial-gradient(circle 248px at center, #16d9e3 0%, #30c7ec 47%, #46aef7 100%);
      -webkit-box-shadow: 6px 6px 15px -6px rgba(13,13,13,0.51);
      box-shadow: 6px 6px 15px -6px rgba(13,13,13,0.51);
      display: flex;
      flex-direction: row;
      justify-content: space-around;
      align-items: center;
    }
    .button {
      display: inline-block;
      padding: 8px 16px;
      margin: 5px;
      background-color: #738aff; /* 深蓝色 */
      color: #ffffff; /* 白色文字 */
      text-align: center;
      text-decoration: none;
      font-size: 14px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }
    .button:hover {
      background-color: #6a71dd;
    }

    /* 背景遮罩 */
    .overlay {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
      z-index: 1000;
    }

    /* 提示窗 */
    .modal {
      display: none;
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      z-index: 1001;
    }

    /* 按钮 */
    .modal button {
      margin-top: 10px;
      float: right;
    }

    #gameInfo div {
      color: white;
    }

    #leftBtnGroup {
      height: 320px;
      width: 120px;
      border-radius: 5px;
      margin-top: 40px;
      margin-right: 25px;
      display: flex;
      justify-content: space-around;
      flex-direction: column;
      align-content: center;
      align-items: center;
    }

    #rightBtnGroup {
      height: 320px;
      width: 120px;
      border-radius: 5px;
      margin-top: 40px;
      margin-left: 25px;
      display: flex;
      justify-content: space-around;
      flex-direction: column;
      align-content: center;
      align-items: center;
    }

    .directionBtn {
      display: flex;
      height: 150px;
      width: 100px;
      background-color: #f9fafb;
      font-size: 45px;
      border-radius: 25px;
      box-shadow: inset 7px 7px 14px #e8e9e9, inset -7px -7px 14px #ffffff;
      align-items: center;
      justify-content: center;
    }

    .directionBtn:hover {
      background: linear-gradient(145deg, #e0e1e2, #ffffff);
      box-shadow:  7px 7px 14px #e8e9e9,
      -7px -7px 14px #ffffff;
    }
  </style>
</head>
<body>
<div id="leftBtnGroup">
  <div class="directionBtn" id="upBtn" onclick="updateDirection(38)"></div>

  <div class="directionBtn" id="leftBtn" onclick="updateDirection(37)"></div>
</div>
<div>
  <div id="menu">
    <div id="gameInfo">
      <div id="score">得分: 0</div>
      <div id="runTime">00:00:000</div>
    </div>
    <div>
      <button id="startBtn" class="button" onclick="start()">开始游戏</button>
      <button id="pauseBtn" class="button" onclick="pause()">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</button>
    </div>
  </div>
  <canvas id="gameCanvas" width="600" height="320"></canvas>
</div>

<div id="rightBtnGroup">
  <div class="directionBtn" id="bottomBtn" onclick="updateDirection(40)"></div>

  <div class="directionBtn" id="rightBtn" onclick="updateDirection(39)"></div>
</div>

<!-- 遮罩层 -->
<div class="overlay" id="overlay"></div>
<!-- 提示窗 -->
<div class="modal" id="modal">
  <p id="msg">这是一个提示窗!</p>
  <button id="closeModalBtn" onclick="restart()">重新开始</button>
</div>

<script type='text/javascript'>
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');

  const box = 10;
  const canvasWidth = canvas.width / box;
  const canvasHeight = canvas.height / box;

  // 定时器id
  let intervalId;
  // 记录是否暂停
  let isPaused = false;
  // 蛇
  let snake = [];
  // 页面中食物数量
  let footNum = 5;
  // 食物
  let food = [];
  // 当前蛇头方向
  let direction = 'RIGHT';
  // 后续方向
  let nextDirection = "RIGHT";
  // 速度
  let speed = 0.1;
  // 分数
  let score = 0;
  // 游戏状态
  let gameStatus = 0;

  // 时间功能
  let timer;
  let isRunning = false;
  let startTime;
  let elapsedTime = 0;

  function init() {
    // 清空数组
    snake.length = 0;
    food.length = 0;
    score = 0;
    // 方向
    direction = 'RIGHT';
    nextDirection = "RIGHT";

    // 初始化蛇
    snake[0] = { x: 9, y: 10, direction: "RIGHT" };
    snake[1] = { x: 8, y: 10, direction: "RIGHT" };
    snake[2] = { x: 7, y: 10, direction: "RIGHT" };
    snake[3] = { x: 6, y: 10, direction: "RIGHT" };
    // 随机食物
    generateFood();

    // 绑定键盘事件
    document.addEventListener('keydown', changeDirection);
    // 定时刷新页面
    if (intervalId) clearInterval(intervalId);
    // 刷新页面
    //intervalId = requestAnimationFrame(updateGame)

    intervalId = setInterval(updateGame, 10);
  }

  // 更新游戏状态
  function updateGame() {
    // 整数 才可执行下一步动作
    if (isInteger(snake[0].x) && isInteger(snake[0].y)) {
      if (direction !== nextDirection) {
        const newDirection = nextDirection;
        if (newDirection === 'LEFT' && direction !== 'RIGHT') {
          direction = newDirection;
        } else if (newDirection === 'RIGHT' && direction !== 'LEFT') {
          direction = newDirection;
        } else if (newDirection === 'UP' && direction !== 'DOWN') {
          direction = newDirection;
        } else if (newDirection === 'DOWN' && direction !== 'UP') {
          direction = newDirection;
        }
      }
    }

    var xValue = 0;
    var yValue = 0;
    // 确定本次移动后的蛇头坐标
    if (direction === 'LEFT') xValue = - speed;
    if (direction === 'UP') yValue = - speed;
    if (direction === 'RIGHT') xValue = + speed;
    if (direction === 'DOWN') yValue = + speed;

    // 深拷贝蛇对象
    var lastSnake = snake.map(segment => ({ ...segment }));

    // 蛇身 每次移动都是后面的 使用前面的坐标
    for (var i = lastSnake.length-1; i > 0; i--) {
      // 如果是刚吃完食物则蛇尾先不动
      if (i === lastSnake.length -1 && lastSnake[lastSnake.length - 1].x === Math.floor(lastSnake[lastSnake.length-2].x) && lastSnake[lastSnake.length - 1].y === Math.floor(lastSnake[lastSnake.length-2].y)) {
        continue;
      }

      var moveX = 0;
      var moveY = 0;

      // 如果前面的蛇身方向为上下
      if (lastSnake[i - 1].direction === "UP" || lastSnake[i - 1].direction === "DOWN") {
        moveX = getMoveDirection(lastSnake[i - 1].x, lastSnake[i].x);
        if (moveX === 0) {
          moveY = getMoveDirection(lastSnake[i - 1].y, lastSnake[i].y);
        }
      } else {
        moveY = getMoveDirection(lastSnake[i - 1].y, lastSnake[i].y);
        if (moveY === 0) {
          moveX = getMoveDirection(lastSnake[i - 1].x, lastSnake[i].x);
        }
      }

      var newX = Number((lastSnake[i].x + moveX).toFixed(10));
      var newY = Number((lastSnake[i].y + moveY).toFixed(10));
      var newDir = lastSnake[i].direction;
      // 如果是整数说明 可能需要调整方向 蛇神需要使用前面的方向
      if (isInteger(newX) && isInteger(newY)) {
        newDir = lastSnake[i-1].direction;
      }
      lastSnake[i] = {
        x: newX,
        y: newY,
        direction: newDir
      }
    }
    // 由于函数执行会消耗时间 所以蛇定位数据会出现不准确的情况 所以在这里更新为准确数据
    lastSnake[0] = {
      x: Number((lastSnake[0].x + xValue).toFixed(10)),
      y: Number((lastSnake[0].y + yValue).toFixed(10)),
      direction: direction
    };

    snake = lastSnake;

    // 检查蛇是否撞到边界或自己
    if (snake[0].x >= canvasWidth || snake[0].x < 0 || snake[0].y >= canvasHeight || snake[0].y < 0 || collision(snake[0], snake.slice(1))) {
      clearInterval(intervalId);
      stopTimer();
      showModal("Game Over! 最终得分: " + score);
      return;
    }

    // 吃到食物
    for (let i = 0; i < footNum; i++) {
      if (snake[0].x === food[i].x && snake[0].y === food[i].y) {
        score++;
        snake.push({...snake[snake.length - 1]});  // 增加蛇的长度
        generateFood(i);
      }
    }

    // 绘制画布
    draw();
    if (intervalId) clearInterval(intervalId);
    intervalId = setInterval(updateGame, 10);
  }

  // 生成食物
  function generateFood(footIndex) {
    // 食物被吃后 补充食物
    if (isInteger(footIndex) && footIndex >= 0 && footIndex < 5) {
      food[footIndex] = {
        x: Math.floor(Math.random() * canvasWidth),
        y: Math.floor(Math.random() * canvasHeight),
        foodShape: Math.floor(Math.random() * (3 - 1 + 1)) + 1
      }
    }
    // 初始
    else {
      for (let i = 0; i < footNum; i++) {
        food[i] = {
          x: Math.floor(Math.random() * canvasWidth),
          y: Math.floor(Math.random() * canvasHeight),
          foodShape: Math.floor(Math.random() * (3 - 1 + 1)) + 1
        }
      }
    }
  }

  // 按钮切换方向
  function updateDirection(btn) {
    const event = {
      keyCode: btn
    }
    changeDirection(event);
  }

  // 改变蛇的移动方向
  function changeDirection(event) {
    if (event.keyCode === 37 && direction !== 'RIGHT') nextDirection = 'LEFT';
    if (event.keyCode === 38 && direction !== 'DOWN') nextDirection = 'UP';
    if (event.keyCode === 39 && direction !== 'LEFT') nextDirection = 'RIGHT';
    if (event.keyCode === 40 && direction !== 'UP') nextDirection = 'DOWN';
  }

  // 计算移动方向
  const getMoveDirection = (previous, current) => {
    const diff = Number((previous - current).toFixed(10));
    if (diff > 0) return speed;
    if (diff === 0) return 0;
    return -speed;
  };

  // 检查是否碰到自己
  function collision(head, array) {
    for (let i = 0; i < array.length; i++) {
      if (head.x === array[i].x && head.y === array[i].y) return true;
    }
    return false;
  }

  // 绘制游戏画面
  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制蛇
    for (let i = snake.length-1; i >= 0; i--) {
      ctx.fillStyle = i === 0 ? 'green' : 'lightgreen';
      ctx.fillRect(snake[i].x * box, snake[i].y * box, box, box);

      // 处于蛇神转弯处把 转弯处的节点点亮
      if (i !== 0) {
        if ((!isInteger(snake[i].x) && !isInteger(snake[i-1].y)) || (!isInteger(snake[i].y) && !isInteger(snake[i-1].x))) {

          var curveX = isInteger(snake[i].x) ? snake[i].x : snake[i-1].x;
          var curveY = isInteger(snake[i].y) ? snake[i].y : snake[i-1].y;

          ctx.fillRect(curveX * box, curveY * box, box, box);
        }
      }
    }

    // 绘制食物
    ctx.fillStyle = 'red';

    for (let i = 0; i < footNum; i++) {
      switch (food[i].foodShape) {
        case 1:
          ctx.beginPath();
          ctx.arc(food[i].x * box + box / 2, food[i].y * box + box / 2, box / 2, 0, 2 * Math.PI);
          ctx.fill();
          break;
        case 2:
          ctx.fillRect(food[i].x * box, food[i].y * box, box, box);
          break;
        case 3:
          ctx.beginPath();
          ctx.moveTo(food[i].x * box + box / 2, food[i].y * box);
          ctx.lineTo(food[i].x * box + box, food[i].y * box + box);
          ctx.lineTo(food[i].x * box, food[i].y * box + box);
          ctx.closePath();
          ctx.fill();
          break;
      }
    }

    // 绘制得分
    updateScore(score);
  }

  // 判断是否为整数
  const isInteger = (value) => !isNaN(value) && parseInt(value) === Number(value);

  // 打开提示弹窗
  function showModal(msg) {
    const overlay = document.getElementById('overlay');
    const modal = document.getElementById('modal');
    const msgP = document.getElementById('msg');
    msgP.innerText = msg;
    overlay.style.display = 'block';
    modal.style.display = 'block';
  }

  // 关闭提示弹窗
  function closeModal() {
    const overlay = document.getElementById('overlay');
    const modal = document.getElementById('modal');
    overlay.style.display = 'none';
    modal.style.display = 'none';
  }

  // 启动游戏
  function start() {
    if (gameStatus === 0) {
      // 清楚之前的定时器
      if (intervalId) clearInterval(intervalId);
      // 初始化游戏
      init();
      // 计时开始
      startTimer();
      // 游戏运行状态
      gameStatus = 1;
      let startBtn = document.getElementById("startBtn");
      startBtn.innerText = "重新开始";
    }
    else {
      restart();
    }
  }

  //重新开始
  function restart() {
    closeModal();
    init();
    resetTimer();
  }

  // 暂停/继续
  function pause() {
    // 暂停
    if (intervalId && !isPaused) {
      const paragraph = document.getElementById('pauseBtn');
      paragraph.innerHTML = '继&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;续';
      clearInterval(intervalId);
      isPaused = true;
      // 暂停计时
      stopTimer();
      return;
    }
    // 继续
    if (intervalId && isPaused) {
      const paragraph = document.getElementById('pauseBtn');
      paragraph.innerHTML = '暂&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;停';
      if (intervalId) clearInterval(intervalId);
      // 定时刷新页面
      intervalId = setInterval(updateGame, 10);
      isPaused = false;
      startTimer();
    }
  }

  function startTimer() {
    if (isRunning) return;
    isRunning = true;
    startTime = Date.now() - elapsedTime;
    timer = requestAnimationFrame(updateTime);
  }

  function updateTime() {
    if (!isRunning) return;
    elapsedTime = Date.now() - startTime;
    updateDisplayTime();
    timer = requestAnimationFrame(updateTime);
  }

  function stopTimer() {
    if (!isRunning) return;
    isRunning = false;
    cancelAnimationFrame(timer);
  }

  function resetTimer() {
    stopTimer();
    elapsedTime = 0;
    updateDisplayTime();
    startTimer();
  }

  // 时间显示
  function updateDisplayTime() {
    const timeDisplay = document.getElementById('runTime');
    timeDisplay.textContent = formatTime(elapsedTime);
  }

  // 得分显示
  function updateScore(score) {
    const timeDisplay = document.getElementById('score');
    timeDisplay.textContent = "得分: " + score;
  }

  // 时间格式化
  function formatTime(ms) {
    const hours = String(Math.floor(ms / 3600000)).padStart(2, '0');
    const minutes = String(Math.floor((ms % 3600000) / 60000)).padStart(2, '0');
    const seconds = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
    const milliseconds = String(ms % 1000).padStart(3, '0');
    return `${hours}:${minutes}:${seconds}:${milliseconds}`;
  }

</script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值