如何实现跟随鼠标的圆形碰撞后不排斥且平滑绕动?
解决Canvas双圆形平滑贴合绕动问题(类似Agar.io分裂细胞效果)
嘿,我完全懂你想要的效果——两个圆形像Agar.io里的分裂细胞那样,碰到一起时不互相排斥,而是紧紧贴合并平滑绕着对方滑动,同时还持续向鼠标位置移动。你之前尝试的布尔标记和反转加速度的思路没踩对路子,反而导致了融合或者瞬移的问题,我来帮你调整代码实现这个效果。
核心问题分析
你之前的碰撞检测只是判断了重叠,但没有正确处理位置修正和切线方向的移动逻辑:
- 直接反转加速度会让圆来回抖动甚至“融合”,因为加速度反向后会往回跑,和鼠标的拉力冲突;
- 简单调整位置又没考虑移动的连续性,所以出现瞬移。
正确的思路应该是:
- 当两圆重叠时,先把它们修正到刚好外切的位置;
- 让圆沿着两圆连线的切线方向移动(而不是反向),同时保留向鼠标的拉力,实现平滑绕动。
修改后的完整代码
<!DOCTYPE html> <html> <head> <style> body { margin: 0; padding: 0; overflow: hidden; display: block; } </style> </head> <body> <canvas id="cv"></canvas> <script> // Setting all up const canvas = document.getElementById("cv"); const ctx = canvas.getContext("2d"); canvas.width = innerWidth; canvas.height = innerHeight; // Math Variables and utilities const PI = Math.PI; const TWO_PI = PI * 2; const HALF_PI = PI / 2; const random = (n1, n2 = 0) => { return Math.random() * (n2 - n1) + n1; }; const distance = (x1, y1, x2, y2) => { let dX = x1 - x2; let dY = y1 - y2; return Math.sqrt(dX * dX + dY * dY); }; // 新增:计算向量的单位向量 const normalize = (x, y) => { const mag = Math.sqrt(x*x + y*y); return mag === 0 ? {x:0, y:0} : {x: x/mag, y: y/mag}; }; // 新增:计算向量的切线方向(垂直方向) const getTangent = (x, y) => { return {x: -y, y: x}; }; let circles = []; // Array that stores the two circles let mouse = { x: canvas.width/2, y: canvas.height/2 }; // mouse object // Creating the circle class function Circle(px, py, r, ctx) { this.x = px; // X Position this.y = py; // Y Position this.r = r; // Radius this.ctx = ctx; // Canvas context this.acc = 0.3; // 调整加速度,让移动更流畅 this.velocity = {x:0, y:0}; // 新增速度属性,替代直接修改位置 // Draw circle function this.show = function() { this.ctx.beginPath(); this.ctx.fillStyle = "#fff"; this.ctx.arc(this.x, this.y, this.r, 0, TWO_PI, false); this.ctx.fill(); }; this.update = function(targetX, targetY) { // 计算指向鼠标的方向向量 const targetDir = normalize(targetX - this.x, targetY - this.y); // 向鼠标方向加速 this.velocity.x += targetDir.x * this.acc; this.velocity.y += targetDir.y * this.acc; // 限制速度,避免过快 const speed = Math.sqrt(this.velocity.x**2 + this.velocity.y**2); if (speed > 2) { const ratio = 2 / speed; this.velocity.x *= ratio; this.velocity.y *= ratio; } // 更新位置 this.x += this.velocity.x; this.y += this.velocity.y; }; // Circle collision handling this.handleCollision = function(other) { const d = distance(this.x, this.y, other.x, other.y); const minDist = this.r + other.r; if (d < minDist && d > 0) { // 1. 修正位置:把两个圆推到刚好外切的位置 const overlap = minDist - d; const dir = normalize(this.x - other.x, this.y - other.y); // 分摊重叠距离,避免单个圆移动过多 this.x += dir.x * overlap * 0.5; this.y += dir.y * overlap * 0.5; other.x -= dir.x * overlap * 0.5; other.y -= dir.y * overlap * 0.5; // 2. 调整速度:保留切线方向的速度,移除径向的冲突分量 const tangent = getTangent(dir.x, dir.y); // 计算当前速度在切线上的投影 const thisTangentSpeed = this.velocity.x * tangent.x + this.velocity.y * tangent.y; const otherTangentSpeed = other.velocity.x * tangent.x + other.velocity.y * tangent.y; // 更新速度为切线方向,同时保留向鼠标的拉力(这里用平均速度让效果更平滑) this.velocity.x = tangent.x * (thisTangentSpeed + otherTangentSpeed) * 0.5; this.velocity.y = tangent.y * (thisTangentSpeed + otherTangentSpeed) * 0.5; } } } // Generating the circles const genCircles = () => { for (let i = 0; i < 2; i++) { circles.push(new Circle(random(canvas.width / 2), random(canvas.height / 2), 50, ctx)); } }; genCircles(); // 把鼠标事件监听移到外面,避免重复绑定 canvas.addEventListener("mousemove", (e) => { mouse.x = e.x; mouse.y = e.y; }, true); // Displaying and updating the circles const showCircles = () => { for (let i = 0; i < circles.length; i++) { // 先更新每个圆的位置(向鼠标移动) circles[i].update(mouse.x, mouse.y); // 然后处理和其他圆的碰撞 for (let j = i + 1; j < circles.length; j++) { circles[i].handleCollision(circles[j]); } // 绘制 circles[i].show(); } }; // Loop to make it run const update = () => { requestAnimationFrame(update); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height); showCircles(); }; update(); </script> </body> </html>
关键改动说明
- 新增速度属性:用
velocity代替直接修改位置,让移动更平滑,便于后续速度调整 - 碰撞处理逻辑优化:
- 位置修正:计算重叠距离,让两个圆各移动一半的重叠量,避免瞬移
- 速度调整:提取速度的切线分量,移除径向的冲突速度,让圆沿着对方边缘滑动,而不是反向弹开
- 优化事件绑定:把
mousemove监听移到循环外,避免每次渲染都重复绑定事件 - 速度限制:防止圆因为加速度累积而移动过快,保证平滑性
现在运行代码,你会看到两个圆碰到一起时会紧紧贴合并平滑绕动,同时持续向鼠标位置移动,和Agar.io里的分裂细胞效果一致。
内容的提问来源于stack exchange,提问作者rochismo
相关产品推荐
相关产品推荐

