You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript弹球打砖块游戏实现滑块移动端触摸控制

问题描述

我正在开发一款弹球打砖块网页游戏,目前Canvas底部的反弹滑块(paddle)功能已经验证正常:小球碰撞滑块后会正确向砖块方向反弹。
现有滑块控制逻辑在桌面端运行正常:通过监听键盘事件读取e.key属性判断左右方向键,即可控制滑块移动。但移动端需要实现无虚拟按钮的触摸控制功能:支持用户通过触摸操作控制滑块沿x轴水平移动,完全依靠触摸感应完成交互,不需要额外的虚拟方向按键。
预期交互效果:用户手指在屏幕上按住左右滑动时,滑块会跟随手指向对应方向移动,将碰撞的小球反弹到砖块区域,完成击打得分。

以下是当前已编写的完整项目代码:

现有代码

JavaScript 代码

const rulesBtn = document.getElementById('rules-btn');
const closeBtn = document.getElementById('close-btn');
const rules = document.getElementById('rules');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let score = 0;

const brickRowCount = 9;
const brickColumnCount = 5;
const delay = 500; //游戏重置延迟

// 小球属性配置
const ball = {
  x: canvas.width / 2,
  y: canvas.height / 2,
  size: 10,
  speed: 4,
  dx: 4,
  dy: -4,
  visible: true
};

// 滑块属性配置
const paddle = {
  x: canvas.width / 2 - 40,
  y: canvas.height - 20,
  w: 80,
  h: 10,
  speed: 8,
  dx: 0,
  visible: true
};

// 砖块属性配置
const brickInfo = {
  w: 70,
  h: 20,
  padding: 10,
  offsetX: 45,
  offsetY: 60,
  visible: true
};

// 初始化砖块矩阵
const bricks = [];
for (let i = 0; i < brickRowCount; i++) {
  bricks[i] = [];
  for (let j = 0; j < brickColumnCount; j++) {
    const x = i * (brickInfo.w + brickInfo.padding) + brickInfo.offsetX;
    const y = j * (brickInfo.h + brickInfo.padding) + brickInfo.offsetY;
    bricks[i][j] = { x, y, ...brickInfo };
  }
}

// 绘制小球
function drawBall() {
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.size, 0, Math.PI * 2);
  ctx.fillStyle = ball.visible ? '#0095dd' : 'transparent';
  ctx.fill();
  ctx.closePath();
}

// 绘制滑块
function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddle.x, paddle.y, paddle.w, paddle.h);
  ctx.fillStyle = paddle.visible ? '#0095dd' : 'transparent';
  ctx.fill();
  ctx.closePath();
}

// 绘制分数
function drawScore() {
  ctx.font = '20px Arial';
  ctx.fillText(`Score: ${score}`, canvas.width - 100, 30);
}

// 绘制所有砖块
function drawBricks() {
  bricks.forEach(column => {
    column.forEach(brick => {
      ctx.beginPath();
      ctx.rect(brick.x, brick.y, brick.w, brick.h);
      ctx.fillStyle = brick.visible ? '#0095dd' : 'transparent';
      ctx.fill();
      ctx.closePath();
    });
  });
}

// 滑块移动逻辑
function movePaddle() {
  paddle.x += paddle.dx;

  // 边界碰撞检测
  if (paddle.x + paddle.w > canvas.width) {
    paddle.x = canvas.width - paddle.w;
  }

  if (paddle.x < 0) {
    paddle.x = 0;
    }
}

// 小球移动逻辑
function moveBall() {
  ball.x += ball.dx;
  ball.y += ball.dy;

  // 左右墙壁碰撞
  if (ball.x + ball.size > canvas.width || ball.x - ball.size < 0) {
    ball.dx *= -1;
  }

  // 上下墙壁碰撞
  if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) {
    ball.dy *= -1;
  }

  // 滑块碰撞检测
  if (
    ball.x - ball.size > paddle.x &&
    ball.x + ball.size < paddle.x + paddle.w &&
    ball.y + ball.size > paddle.y
  ) {
    ball.dy = -ball.speed;
  }

  // 砖块碰撞检测
  bricks.forEach(column => {
    column.forEach(brick => {
      if (brick.visible) {
        if (
          ball.x - ball.size > brick.x &&
          ball.x + ball.size < brick.x + brick.w &&
          ball.y + ball.size > brick.y &&
          ball.y - ball.size < brick.y + brick.h
        ) {
          ball.dy *= -1;
          brick.visible = false;

          increaseScore();
        }
      }
    });
  });

  // 碰到底部边界 游戏重置
  if (ball.y + ball.size > canvas.height) {
    showAllBricks();
    score = 0;
  }
}

// 分数增加逻辑
function increaseScore() {
  score++;

  if (score % (brickRowCount * brickColumnCount) === 0) {

      ball.visible = false;
      paddle.visible = false;

      //延迟0.5秒后重启游戏
      setTimeout(function () {
          showAllBricks();
          score = 0;
          paddle.x = canvas.width / 2 - 40;
          paddle.y = canvas.height - 20;
          ball.x = canvas.width / 2;
          ball.y = canvas.height / 2;
          ball.visible = true;
          paddle.visible = true;
      },delay)
  }
}

// 重置所有砖块为可见状态
function showAllBricks() {
  bricks.forEach(column => {
    column.forEach(brick => (brick.visible = true));
  });
}

// 统一绘制所有元素
function draw() {
  // 清空画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  drawBall();
  drawPaddle();
  drawScore();
  drawBricks();
}

// 动画更新循环
function update() {
  movePaddle();
  moveBall();

  draw();

  requestAnimationFrame(update);
}

update();

// 键盘按下事件
function keyDown(e) {
  if (e.key === 'Right' || e.key === 'ArrowRight' ||) {
    paddle.dx = paddle.speed;
  } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
    paddle.dx = -paddle.speed;
  }
}

// 键盘松开事件
function keyUp(e) {
  if (
    e.key === 'Right' ||
    e.key === 'ArrowRight' ||
    e.key === 'Left' ||
    e.key === 'ArrowLeft'||
    e.key==='touch'
  ) {
    paddle.dx = 0;
  }
}

// 键盘事件监听
document.addEventListener('keydown', keyDown);
document.addEventListener('keyup', keyUp);
someElement.addEventListener('touchstart', process_touchstart,true);
someElement.addEventListener('touchmove', process_touchmove, true);


// 规则弹窗按钮事件
rulesBtn.addEventListener('click', () => rules.classList.add('show'));
closeBtn.addEventListener('click', () => rules.classList.remove('show'));

CSS 样式代码

* {
  box-sizing: border-box;
}

body {
  background-color: #0095dd;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-family: Arial, Helvetica, sans-serif;
  min-height: 100vh;
  margin: 0;
}

h1 {
  font-size: 45px;
  color: #fff;
}

canvas {
  background: #f0f0f0;
  display: block;
  border-radius: 5px;
}

.btn {
  cursor: pointer;
  border: 0;
  padding: 10px 20px;
  background: #000;
  color: #fff;
  border-radius: 5px;
}

.btn:focus {
  outline: 0;
}

.btn:hover {
  background: #222;
}

.btn:active {
  transform: scale(0.98);
}

.rules-btn {
  position: absolute;
  top: 30px;
  left: 30px;
}

.rules {
  position: absolute;
  top: 0;
  left: 0;
  background: #333;
  color: #fff;
  min-height: 100vh;
  width: 400px;
  padding: 20px;
  line-height: 1.5;
  transform: translateX(-400px);
  transition: transform 1s ease-in-out;
}

.rules.show {
  transform: translateX(0);
}

HTML 结构代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="style.css" />
    <title>Breakout!</title>
  </head>
  <body>
    <h1>Breakout!</h1>
    <button id="rules-btn" class="btn rules-btn">Show Rules</button>
    <div id="rules" class="rules">
      <h2>How To Play:</h2>
      <p>
        Use your right and left keys to move the paddle to bounce the ball up
        and break the blocks.
      </p>
      <p>If you miss the ball, your score and the blocks will reset.</p>
      <button id="close-btn" class="btn">Close</button>
    </div>

    <canvas id="canvas" width="800" height="600"></canvas>

    <script src="script.js"></script>
  </body>
</html>
解决方案

现有代码存在三个核心问题导致触摸控制无法生效:

  • 触摸事件绑定到了不存在的someElement上,且没有实现对应的process_touchstart、process_touchmove处理函数
  • 原键盘控制使用的速度增量移动逻辑,不适合触摸场景的跟手需求,触摸控制需要直接对齐手指坐标才能实现顺滑的跟随效果
  • 原keyDown函数存在语法错误:方向键判断条件末尾多了多余的||符号,会导致JS运行报错

按以下步骤修改即可实现预期的无按钮触摸控制:

  1. 调整HTML和CSS配置,屏蔽触摸默认行为
    • 在HTML的viewport meta标签中添加user-scalable=no,禁止移动端页面双指缩放干扰操作
    • 给canvas样式添加touch-action: none,屏蔽触摸滑动时的默认页面滚动行为
  2. 删除原代码中无效的两行触摸事件绑定代码,替换为以下触摸控制逻辑,直接绑定事件到canvas元素上:
// 初始化canvas边界缓存,页面尺寸变化时自动更新
let canvasRect = canvas.getBoundingClientRect();
window.addEventListener('resize', () => {
  canvasRect = canvas.getBoundingClientRect();
});

// 触摸开始:直接将滑块中心对齐触摸点
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  const relativeX = touch.clientX - canvasRect.left;
  // 坐标换算:适配canvas缩放显示场景,避免触摸位置偏移
  const scale = canvas.width / canvasRect.width;
  paddle.x = relativeX * scale - paddle.w / 2;
  // 边界限制,防止滑块移出画布
  paddle.x = Math.max(0, Math.min(canvas.width - paddle.w, paddle.x));
}, { passive: false });

// 触摸滑动:实时更新滑块位置跟随手指
canvas.addEventListener('touchmove', (e) => {
  e.preventDefault();
  const touch = e.touches[0];
  const relativeX = touch.clientX - canvasRect.left;
  const scale = canvas.width / canvasRect.width;
  paddle.x = relativeX * scale - paddle.w / 2;
  // 边界限制
  paddle.x = Math.max(0, Math.min(canvas.width - paddle.w, paddle.x));
}, { passive: false });
  1. 修复原keyDown函数的语法错误,删除判断条件末尾多余的||:
function keyDown(e) {
  if (e.key === 'Right' || e.key === 'ArrowRight') {
    paddle.dx = paddle.speed;
  } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
    paddle.dx = -paddle.speed;
  }
}

修改完成后,移动端用户在canvas任意位置左右滑动,滑块都会顺滑跟随手指移动,不需要额外添加虚拟方向按键,桌面端键盘控制也能正常运行。

内容的提问来源于stack exchange,提问作者Learncode

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 15:48:44