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

如何在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();

关键代码说明

  1. 弧形碰撞体生成

    • createBowlPoints通过极坐标计算圆弧顶点,用20段折线近似平滑弧形(可调整segments参数控制精度)
    • 用b2ChainShape创建链形刚体,并通过SetPrevVertex/SetNextVertex连接左右墙底部,解决链形端点碰撞检测异常问题
  2. 容器结构适配

    • 保留原有的左右直墙和上墙,仅替换底部为弧形链形,保证容器封闭性与物理碰撞逻辑正确
  3. 渲染同步

    • 在渲染函数中增加b2ChainShape的绘制逻辑,遍历顶点连线,确保视觉与物理碰撞体完全匹配
  4. 拖拽交互保留

    • 基于b2MouseJoint的拖拽逻辑完全保留,小球可正常被拖拽、推拉,物理交互不受容器形状修改影响

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 22:45:45