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运行报错
按以下步骤修改即可实现预期的无按钮触摸控制:
- 调整HTML和CSS配置,屏蔽触摸默认行为
- 在HTML的viewport meta标签中添加
user-scalable=no,禁止移动端页面双指缩放干扰操作 - 给canvas样式添加
touch-action: none,屏蔽触摸滑动时的默认页面滚动行为
- 在HTML的viewport meta标签中添加
- 删除原代码中无效的两行触摸事件绑定代码,替换为以下触摸控制逻辑,直接绑定事件到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 });
- 修复原
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
相关产品推荐
相关产品推荐

