JavaScript Canvas小球弹跳算法优化:实现对撞与gridSize卡顿修复
小球互相碰撞反弹逻辑修复
你当前写的碰撞逻辑有3个核心问题,直接修改如下即可:
- 未排除当前小球自身,导致每帧都会触发碰撞判断
- 重复处理同一组碰撞(小球A遍历到小球B时处理一次,小球B遍历到小球A时又处理一次,导致方向反转两次等于没反转)
- 碰撞处理后额外修改了被碰撞小球的位置,后续循环到该小球时会再次更新位置,导致位移异常
- 原边界判断有笔误,y方向边界误判用了
settings.width,需要修正为settings.height
修复后的#work方法代码:
#work() { // 用Set记录已经处理过的碰撞对,避免重复处理 const handledPairs = new Set(); for (let i = 0; i < this.dots.length; i++) { const dot = this.dots[i]; // 先处理边界碰撞 if (dot.position.x + dot.direction.x * settings.gridSize >= settings.width - settings.gridSize) { dot.direction.x = -1; } else if (dot.position.x + dot.direction.x * settings.gridSize < 0) { dot.direction.x = 1; } if (dot.position.y + dot.direction.y * settings.gridSize >= settings.height - settings.gridSize) { dot.direction.y = -1; } else if (dot.position.y + dot.direction.y * settings.gridSize < 0) { dot.direction.y = 1; } // 处理小球间碰撞,只遍历索引比当前大的小球,避免重复判断 for (let j = i + 1; j < this.dots.length; j++) { const cur = this.dots[j]; const pairId = `${i}-${j}`; if (handledPairs.has(pairId)) continue; // 判断下一帧位置是否重叠或者当前已经重叠 const nextDotX = dot.position.x + dot.direction.x * settings.gridSize; const nextDotY = dot.position.y + dot.direction.y * settings.gridSize; const nextCurX = cur.position.x + cur.direction.x * settings.gridSize; const nextCurY = cur.position.y + cur.direction.y * settings.gridSize; const isCollide = (nextDotX === nextCurX && nextDotY === nextCurY) || (dot.position.x === cur.position.x && dot.position.y === cur.position.y); if (isCollide) { // 网格移动场景下直接交换两个小球的方向即可实现反弹效果 [dot.direction.x, cur.direction.x] = [cur.direction.x, dot.direction.x]; [dot.direction.y, cur.direction.y] = [cur.direction.y, dot.direction.y]; handledPairs.add(pairId); } } // 更新当前小球位置 dot.position.x += dot.direction.x * settings.gridSize; dot.position.y += dot.direction.y * settings.gridSize; } }
卡顿性能优化方案
卡顿和gridSize关联的核心原因是:gridSize越小,单步移动距离越短,相同移动速度下需要更高的帧率才能流畅,同时碰撞检测的计算量也会上升。可以从以下几个方面优化:
- 替换定时器方案:把
setInterval换成requestAnimationFrame,和屏幕刷新率同步,默认60fps流畅度远高于原来的20fps(50ms间隔),如果要控制移动速度可以加时间差计算:constructor() { this.#generateDots(); this.lastTime = 0; const tick = (time) => { if (time - this.lastTime >= settings.duration) { this.#clear(); this.#work(); this.#draw(); this.lastTime = time; } requestAnimationFrame(tick); } requestAnimationFrame(tick); } - 优化碰撞检测效率:原来的双重循环是O(n²)复杂度,dot数量多的时候卡顿明显。因为你的小球是严格网格对齐的,可以维护一个坐标映射表,直接查当前位置有没有其他小球,不用嵌套遍历所有小球:
#work() { // 先构建当前坐标和小球索引的映射 const posMap = new Map(); this.dots.forEach((dot, idx) => { const key = `${dot.position.x},${dot.position.y}`; posMap.set(key, idx); }); // 后续碰撞检测直接查posMap即可,复杂度降为O(n) } - 初始化优化:生成小球时判断位置是否重复的逻辑,用Set代替filter遍历,生成速度更快:
#generateDots() { this.dots = []; const existPos = new Set(); while (this.dots.length < settings.dotCount) { let newDot = new Dot(); const key = `${newDot.position.x},${newDot.position.y}`; if (!existPos.has(key)) { this.dots.push(newDot); existPos.add(key); } } } - 渲染优化:如果gridSize很小导致小球数量很多,可以批量绘制减少状态切换,比如把同色的小球放在一起绘制,避免频繁修改
context.fillStyle。
内容的提问来源于stack exchange,提问作者RoyBlunk
相关产品推荐
相关产品推荐

