How to properly delete a box2d body in version: Box2dWeb-2.1.a.3, Box2D_v2.3.1r3? Box2D bug?

Viewed 1707

Update

Since the problem has been found I've also find out that Box2D for web is leaking on every side :/

To show this I made a simple circle moving in a static polygon and here is the result after some time.

enter image description here

Notice how the following items are leaking as I'm not creating any body or changing the world in any way:

  • b2Vec2
  • Features
  • b2ManifoldPoint
  • b2ContactID
  • b2Manifold
  • b2ContactEdge
  • b2PolyAndCircleContact
  • Array
  • ...

Original post

I have a problem because I'm profiling my game and the garbage collector doesnt' delete my bodies, contacts and other stuff. Then I've looked at what are they keeping from the GC and was the Box2D itself. This might lead to 2 options: I'm doing it bad or Box2D is leaking. I consider is my cause.

What exactly is keeping it?

  • contact.m_nodeA.other was appearing to be the most used to keep it from GC.
  • other times: m_fixtureB in a contact... see image

enter image description here enter image description here

You can see that the body has a __destroyed property. That is set manually before deleting it with world.DestroyBody(body)

When I destroy a body I call it after I call the step method on the world.

As you can see from the box2d method it doesn't get rid of the other variable nor it changes it to another body and my body is not GC.

Any idea of what I'm missing here?

Now I can fix the problem only if the world.Step is not ran:

var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);
var step = false;

var fixtureDef = new Box2D.FixtureDef();
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
fixtureDef.shape = new Box2D.PolygonShape();
fixtureDef.shape.SetAsBox(1, 1);
var bodyDef = new Box2D.BodyDef;
bodyDef.type = Box2D.Body.b2_dynamicBody;
bodyDef.position.x = 0.4;
bodyDef.position.y = 0.4;

var bodies = []
var fix = [];
window.c = function(){
    for(var i = 0; i < 100; i++){
        var body = world.CreateBody(bodyDef);
        body._id = i;

        fix.push(body.CreateFixture(fixtureDef));
        bodies.push(body);

    }
    if(step){world.Step(1/60, 3, 3); world.ClearForces();}
    console.log('Created', bodies)
    fixtureDef = null;
    bodyDef = null;
}

window.d = function(){
    _.each(bodies, function(body, i){
        body.DestroyFixture(fix[i]);
        world.DestroyBody(body);

        fix[i] = null;
        bodies[i] = null;
    })
    if(step){world.Step(1/60, 3, 3); world.ClearForces();}
    bodies = null;
    fix = null;
}

Change the step to true and the memory leak problem appears again.

Reproduce the memory leak problem:

Code in your file:

var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);

var bodies = []
window.c = function(){
    for(var i = 0; i < 100; i++){
        var bodyDef = new Box2D.BodyDef();
        bodyDef.type = 2;

        var shape = new Box2D.PolygonShape();
        shape.SetAsBox(1, 1);

        var fixtureDef   = new Box2D.FixtureDef();
        fixtureDef.shape = shape;
        var body = world.CreateBody(bodyDef);
        body._id = i;
        body.CreateFixture(fixtureDef);
        bodies.push(body);
    }
    world.Step(0.3, 3, 3);
    console.log('Created', bodies)
}
window.d = function(){
    _.each(bodies, function(body, i){
        world.DestroyBody(body);
        bodies[i] = null;
    })
    world.Step(0.3, 3, 3);
    bodies = null;
}

Open google chrome:

  • Then open your profile and make a snapshot.
  • Now run the c() method in your console to create 100 bodies
  • Now snapshot 2
  • Search in snapshot for b2Body and you'll find 100 Object count
  • Now run d() to delete all your bodies;
  • Force Garbage collection by clicking on the garbage can
  • Make a snapshot 3
  • Search for b2Body and you'll also find 100 Object count

At the last step should only be 0 objects as they have been destroyed. Instead of this you'll find this: enter image description here

Now you can see there are a lot of references from b2ContactEdge. Now if you remove the world.Step part of the code you will only see 2 references to the body.

If you remove this line

body.CreateFixture(fixtureDef);

or making the body static is not leaking anymore.

My game loop

...gameLoop = function(o){
    // used a lot here
    var world = o.world;

    // calculate the new positions
    var worldStepSeconds = o.worldStepMs / 1000;

    // step world
    world.Step(worldStepSeconds, o.velocityIterations, o.positionIterations)

    // render debug
    if(o.renderDebug){
        world.DrawDebugData();
    }

    // always to not accumulate forces, maybe some bug occurs
    world.ClearForces();

    // tick all ticking entities
    _.each(o.getTickEntitiesFn(), function(actor){
        if(!actor) return;
        actor.tick(o.worldStepMs, o.lastFrameMs);
    })


    // update PIXI entities
    var body = world.GetBodyList();
    var worldScale = world.SCALE;
    var destroyBody = world.DestroyBody.bind(world);
    while(body){
        var actor = null;
        var visualEntity = null;
        var box2DEntity = o.getBox2DEntityByIdFn(body.GetUserData());
        if(box2DEntity){
            visualEntity = o.getVisualEntityByIdFn(box2DEntity.getVisualEntityId());
            if(box2DEntity.isDestroying()){
                // optimization
                body.__destroyed = true;
                world.DestroyBody(body);
                box2DEntity.completeDestroy();
            }
        }
        if(visualEntity){
            if(visualEntity.isDestroying()){
                visualEntity.completeDestroy();
            }else{
                var inverseY = true;
                var bodyDetails = Utils.getScreenPositionAndRotationOfBody(world, body, inverseY);
                visualEntity.updateSprite(bodyDetails.x, bodyDetails.y, bodyDetails.rotation);
            }
        }
        // this delegates out functionality for each body processed
        if(o.triggersFn.eachBody) o.triggersFn.eachBody(world, body, visualEntity);

        body = body.GetNext();
    }

    // when a joint is created is then also created it's visual counterpart and then set to userData.
    var joint = world.GetJointList();
    while(joint){
        var pixiGraphics = joint.GetUserData();
        if(pixiGraphics){
            // In order to draw a distance joint we need to know the start and end positions.
            // The joint saves the global (yes) anchor positions for each body.
            // After that we need to scale to our screen and invert y axis.
            var anchorA           = joint.GetAnchorA();
            var anchorB           = joint.GetAnchorB();
            var screenPositionA = anchorA.Copy();
            var screenPositionB = anchorB.Copy();
            // scale
            screenPositionA.Multiply(world.SCALE);
            screenPositionB.Multiply(world.SCALE);
            // invert y
            screenPositionA.y = world.CANVAS_HEIGHT - screenPositionA.y
            screenPositionB.y = world.CANVAS_HEIGHT - screenPositionB.y

            // draw a black line
            pixiGraphics.clear();
            pixiGraphics.lineStyle(1, 0x000000, 0.7);
            pixiGraphics.moveTo(screenPositionA.x, screenPositionA.y);
            pixiGraphics.lineTo(screenPositionB.x, screenPositionB.y);
        }
        joint = joint.GetNext();
    }

    // render the PIXI scene
    if(o.renderPixi){
        o.renderer.render(o.stage)
    }

    // render next frame
    requestAnimFrame(o.requestAnimFrameFn);
}

Code from Box2d:

b2ContactManager.prototype.Destroy = function (c) {
var fixtureA = c.GetFixtureA();
var fixtureB = c.GetFixtureB();
var bodyA = fixtureA.GetBody();
var bodyB = fixtureB.GetBody();
if (c.IsTouching()) {
this.m_contactListener.EndContact(c);
}
if (c.m_prev) {
c.m_prev.m_next = c.m_next;
}
if (c.m_next) {
c.m_next.m_prev = c.m_prev;
}
if (c == this.m_world.m_contactList) {
this.m_world.m_contactList = c.m_next;
}
if (c.m_nodeA.prev) {
c.m_nodeA.prev.next = c.m_nodeA.next;
}
if (c.m_nodeA.next) {
c.m_nodeA.next.prev = c.m_nodeA.prev;
}
if (c.m_nodeA == bodyA.m_contactList) {
bodyA.m_contactList = c.m_nodeA.next;
}
if (c.m_nodeB.prev) {
c.m_nodeB.prev.next = c.m_nodeB.next;
}
if (c.m_nodeB.next) {
c.m_nodeB.next.prev = c.m_nodeB.prev;
}
if (c.m_nodeB == bodyB.m_contactList) {
bodyB.m_contactList = c.m_nodeB.next;
}
this.m_contactFactory.Destroy(c);
--this.m_contactCount;
}


b2ContactFactory.prototype.Destroy = function (contact) {
    if (contact.m_manifold.m_pointCount > 0) {
        contact.m_fixtureA.m_body.SetAwake(true);
        contact.m_fixtureB.m_body.SetAwake(true);
    }
    var type1 = parseInt(contact.m_fixtureA.GetType());
    var type2 = parseInt(contact.m_fixtureB.GetType());
    var reg = this.m_registers[type1][type2];
    if (true) {
        reg.poolCount++;
        contact.m_next = reg.pool;
        reg.pool = contact;
    }
    var destroyFcn = reg.destroyFcn;
    destroyFcn(contact, this.m_allocator);
}
2 Answers
Related