如何在Canvas+Box2D中将底部直墙改为弧形碗状结构
实现Box2D弧形碗状容器(替换底部直墙)
核心思路
- 用Box2D的
b2ChainShape构建近似圆弧的静态刚体,通过多段均匀分布的顶点模拟平滑弧形 - 保留原有
b2MouseJoint拖拽交互逻辑,确保小球可正常拖拽推拉 - 渲染时同步绘制弧形路径,保证视觉表现与物理碰撞体一致
完整代码
HTML文件(index.html)
<!DOCTYPE html> <html> <head> <title>Box2D Bowl Container</title> <style> canvas { border: 1px solid #333; } </style> </head> <body> <canvas id="canvas" width="600" height="400"></canvas> <!-- 引入Box2D库,可从官方渠道获取或使用公开CDN资源 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/box2dweb/2.1.a.3/Box2dWeb-2.1.a.3.min.js"></script> <script src="ball2.js"></script> </body> </html>
JS文件(ball2.js)
// 初始化Box2D常用类别名 var b2Vec2 = Box2D.Common.Math.b2Vec2; var b2BodyDef = Box2D.Dynamics.b2BodyDef; var b2Body = Box2D.Dynamics.b2Body; var b2FixtureDef = Box2D.Dynamics.b2FixtureDef; var b2ChainShape = Box2D.Collision.Shapes.b2ChainShape; var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape; var b2CircleShape = Box2D.Collision.Shapes.b2CircleShape; var b2MouseJointDef = Box2D.Dynamics.Joints.b2MouseJointDef; var b2World = Box2D.Dynamics.b2World; // 物理世界核心参数 var world = new b2World(new b2Vec2(0, 9.8), true); var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var scale = 30; // 像素与物理单位(米)的转换比例 var mouseJoint = null; var selectedBody = null; // 生成弧形底部的顶点数组 function createBowlPoints(leftEdge, rightEdge, arcCenterY, radius) { var points = []; const segments = 20; // 分段数,数值越大弧形越平滑 const startAngle = Math.PI; // 左端点对应弧度 const endAngle = 0; // 右端点对应弧度 const angleStep = (endAngle - startAngle) / segments; for (let i = 0; i <= segments; i++) { const currentAngle = startAngle + i * angleStep; // Box2D坐标系y轴向下,需调整坐标计算逻辑 const x = arcCenterY + radius * Math.cos(currentAngle); const y = arcCenterY + radius * Math.sin(currentAngle); points.push(new b2Vec2(x, y)); } return points; } // 创建碗状容器(左右上墙+弧形底部) function createBowlContainer() { const fixtureDef = new b2FixtureDef(); fixtureDef.density = 1.0; fixtureDef.friction = 0.5; fixtureDef.restitution = 0.2; const bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_staticBody; // 左墙 bodyDef.position.Set(1, 0); fixtureDef.shape = new b2PolygonShape(); fixtureDef.shape.SetAsBox(0.5, canvas.height / scale); world.CreateBody(bodyDef).CreateFixture(fixtureDef); // 右墙 bodyDef.position.Set((canvas.width / scale) - 1, 0); world.CreateBody(bodyDef).CreateFixture(fixtureDef); // 上墙 bodyDef.position.Set(canvas.width / (2 * scale), 1); fixtureDef.shape.SetAsBox(canvas.width / (2 * scale) - 1, 0.5); world.CreateBody(bodyDef).CreateFixture(fixtureDef); // 弧形底部 const bowlPoints = createBowlPoints(1, (canvas.width/scale)-1, canvas.height/scale - 3, 4); const chainShape = new b2ChainShape(); // 设置前后连接顶点,避免链形端点碰撞检测异常 chainShape.CreateChain(bowlPoints, bowlPoints.length); chainShape.SetPrevVertex(new b2Vec2(1, canvas.height/scale)); chainShape.SetNextVertex(new b2Vec2((canvas.width/scale)-1, canvas.height/scale)); fixtureDef.shape = chainShape; bodyDef.position.Set(0,0); world.CreateBody(bodyDef).CreateFixture(fixtureDef); } // 创建可拖拽小球 function createBall() { const bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_dynamicBody; bodyDef.position.Set(canvas.width/(2*scale), canvas.height/(2*scale)); const fixtureDef = new b2FixtureDef(); fixtureDef.density = 1.0; fixtureDef.friction = 0.3; fixtureDef.restitution = 0.7; fixtureDef.shape = new b2CircleShape(0.8); world.CreateBody(bodyDef).CreateFixture(fixtureDef); } // 渲染物理世界 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); let body = world.GetBodyList(); while (body) { let fixture = body.GetFixtureList(); while (fixture) { const shape = fixture.GetShape(); ctx.beginPath(); switch(shape.GetType()) { case Box2D.Collision.Shapes.b2PolygonShape.e_polygon: const vertices = shape.GetVertices(); ctx.moveTo(vertices[0].x*scale, vertices[0].y*scale); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(vertices[i].x*scale, vertices[i].y*scale); } ctx.lineTo(vertices[0].x*scale, vertices[0].y*scale); break; case Box2D.Collision.Shapes.b2CircleShape.e_circle: const pos = body.GetPosition(); ctx.arc(pos.x*scale, pos.y*scale, shape.GetRadius()*scale, 0, Math.PI*2); break; case Box2D.Collision.Shapes.b2ChainShape.e_chain: const points = shape.GetVertices(); ctx.moveTo(points[0].x*scale, points[0].y*scale); for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i].x*scale, points[i].y*scale); } break; } ctx.strokeStyle = '#333'; ctx.fillStyle = '#ddd'; ctx.fill(); ctx.stroke(); fixture = fixture.GetNext(); } body = body.GetNext(); } } // 鼠标交互逻辑 function getBodyAtMouse() { const mousePos = getMousePosition(); const p = new b2Vec2(mousePos.x/scale, mousePos.y/scale); const aabb = new Box2D.Collision.b2AABB(); aabb.lowerBound.Set(p.x - 0.001, p.y - 0.001); aabb.upperBound.Set(p.x + 0.001, p.y + 0.001); let selectedBody = null; world.QueryAABB(function(fixture) { if (fixture.GetBody().GetType() !== b2Body.b2_staticBody) { if (fixture.GetShape().TestPoint(fixture.GetBody().GetTransform(), p)) { selectedBody = fixture.GetBody(); return false; } } return true; }, aabb); return selectedBody; } function getMousePosition() { const rect = canvas.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; } canvas.addEventListener('mousedown', function(e) { selectedBody = getBodyAtMouse(); if (selectedBody) { const mousePos = getMousePosition(); const md = new b2MouseJointDef(); md.bodyA = world.GetGroundBody(); md.bodyB = selectedBody; md.target.Set(mousePos.x/scale, mousePos.y/scale); md.collideConnected = true; md.maxForce = 1000 * selectedBody.GetMass(); mouseJoint = world.CreateJoint(md); selectedBody.SetAwake(true); } }); canvas.addEventListener('mousemove', function(e) { if (mouseJoint) { const mousePos = getMousePosition(); mouseJoint.SetTarget(new b2Vec2(mousePos.x/scale, mousePos.y/scale)); } }); canvas.addEventListener('mouseup', function(e) { if (mouseJoint) { world.DestroyJoint(mouseJoint); mouseJoint = null; } }); // 主循环 function update() { world.Step(1/60, 10, 10); world.ClearForces(); render(); requestAnimationFrame(update); } // 初始化程序 createBowlContainer(); createBall(); update();
关键代码说明
弧形碰撞体生成
createBowlPoints通过极坐标计算圆弧顶点,用20段折线近似平滑弧形(可调整segments参数控制精度)- 用
b2ChainShape创建链形刚体,并通过SetPrevVertex/SetNextVertex连接左右墙底部,解决链形端点碰撞检测异常问题
容器结构适配
- 保留原有的左右直墙和上墙,仅替换底部为弧形链形,保证容器封闭性与物理碰撞逻辑正确
渲染同步
- 在渲染函数中增加
b2ChainShape的绘制逻辑,遍历顶点连线,确保视觉与物理碰撞体完全匹配
- 在渲染函数中增加
拖拽交互保留
- 基于
b2MouseJoint的拖拽逻辑完全保留,小球可正常被拖拽、推拉,物理交互不受容器形状修改影响
- 基于
内容的提问来源于stack exchange,提问作者Shawn Michael
相关产品推荐
相关产品推荐

