How to properly delete a box2d body in version: Bo

2019-02-07 07:38发布

问题:

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.

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

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:

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);
}

回答1:

I have the same problem, but I think I find out from where it comes.

Instead of m_* try functions, like GetFixtureA() instead of m_fixtureA.



回答2:

Totti did you ever figure this out? It looks like box2dweb requires manual destruction and memory management.

I think I have found your leaks, un-implemented ( static class ) destruction functions:

b2Joint.Destroy = function (joint, allocator) {}
b2CircleContact.Destroy = function (contact, allocator) {}< 
b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}<
b2PolyAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}
[UPDATE...]     
b2DestructionListener.b2DestructionListener = function () {};
b2DestructionListener.prototype.SayGoodbyeJoint = function (joint) {}
b2DestructionListener.prototype.SayGoodbyeFixture = function (fixture) {}


b2Contact.prototype.Reset(fixtureA, fixtureB)

called with with one/both fixture arguments resets passed in fixture/s BUT ALSO pass in NO arguments and it 'nulls' all the the b2Contact properties! (UNTESTED:) but I suggest set your YOURcontactListener class up to handle all contact callbacks EVERY call with Reset(??) dynamically configureable as logic requies EVERY call (there are more than you'd imagine each and every world step).

Also take Colt McAnlis clever advice and strategically pre allocate all the memory the life of your game will need (by creating game and box2d object pools now you know objects can be reset) so the garbage collector NEVER runs until, you destroy object pools at times of your own convenience.... i.e when you close the tab, or your device needs recharging! ;D
[...UPDATE]

// you can define and assign your own contact listener ...via...

YOUR.b2world.b2ContactManager.m_world.m_contactList = new YOURcontactlistener();<br>[edit]...if you dont it actually does have Box2D.Dynamics.b2ContactListener.b2_defaultListener.

// box2d in the worldStep calls YOURcontactlistener.update() via: this.b2world.b2ContactManager.m_world.m_contactList.Update(this.m_contactListener)
// this.m_contactListener being YOURS || b2_defaultListener;

// which instantiates ALL your listed leaking object like so: {b2Contact which instantiates {b2ContactEdge} and {b2Manifold which instantiates {b2ManifoldPoint{which instantiates m_id.key == ContactID{which instantiates Features}}}} along with {B2Vec2} are instantiated in b2ContactResult ...which I can not actually find but assume it must be instantiated in the Solver.

// There is a Contacts.destroyFcn callback is CREATED in....

b2ContactFactory.prototype.Destroy = function (contact) {...}

// then Contacts.destroyFcn callback(s) are privately REGISTERED in....

b2ContactFactory.prototype.InitializeRegisters() {...}

...via...

this.AddType = function (createFcn, destroyFcn, type1, type2) {...}

...BUT... THOSE privately registered ARE four of the un-implimented static class function from above...

b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndCircleContact.Destroy = function (contact, allocator) {} 
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}

So I havn't tested it yet but it looks like box2dweb just gives you the Destroy callback/handler functions and you have to read the source to find all the properties you need to null. [Edit] In combination with b2Contact.prototype.Reset(fixtureA, fixtureB)

But either way pretty confident the functions above(possibly incomplete) are callback/handlers, and can be used to null your way back to performance for anyone else who stumbles across this problem. Pretty sure Totti's moved on
(dont forget to handle your 'this' scope in callbacks).