Box2D - b2body GetUserData always returns null

2019-05-24 06:16发布

问题:

I am trying to adjust the position and rotation of a sprite based on that of a b2body in box2d.

After I have created the body im setting the userData property to that of my body object that holds the sprite and position etc. The problem is that in the tick method b->GetUserData never retrieves the object I put in there. Can you see anything wrong with the following?

Here is my add player method:

-(void) addPlayerShip
{
    CCSpriteSheet *sheet = [CCSpriteSheet spriteSheetWithFile:@"PlayerShip.png" capacity:1];

    //Get sprite sheet
    CCSprite *sprite = [CCSprite spriteWithSpriteSheet:sheet rect:CGRectMake(0,0,64,64)];
    [sheet addChild:sprite];

    CGSize screenSize = [CCDirector sharedDirector].winSize;

    sprite.position = ccp( screenSize.width/2, screenSize.height/2);

    [self addChild:sheet];

    // Define the dynamic body.
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position.Set(sprite.position.x/PTM_RATIO, sprite.position.y /PTM_RATIO);
    bodyDef.angularVelocity=0;

    b2Body *body = world->CreateBody(&bodyDef);

    b2PolygonShape dynamicTriangle;

    dynamicTriangle.m_vertexCount=3;
    dynamicTriangle.m_vertices[0].Set(0,-1);
    dynamicTriangle.m_vertices[1].Set(1,1);
    dynamicTriangle.m_vertices[2].Set(-1,1);

    // Define the dynamic body fixture.
    b2FixtureDef fixtureDef;
    fixtureDef.shape = &dynamicTriangle;
    fixtureDef.density = 1.0f;
    fixtureDef.friction = 0.0f;
    fixtureDef.restitution=0.0f;

    b2Fixture *fixture = body->CreateFixture(&fixtureDef);

    b2Vec2 *vector = new b2Vec2;

    BodyObject *bodyObject = [[BodyObject alloc] initWithBody:body 
                                                   andFixture:fixture 
                                                  andVelocity:vector 
                                                    andSprite:sprite];


    bodyDef.userData = bodyObject;
}

and the tick method that is always returning null on b->GetUserData

-(void) tick: (ccTime) dt
{
    int32 velocityIterations = 8;
    int32 positionIterations = 1;

    world->Step(dt, velocityIterations, positionIterations);

    //Iterate over the bodies in the physics world
    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
    {
        if (b->GetUserData() != NULL) {
            //Synchronize the Sprites position and rotation with the corresponding body
            GameObject *myObject = (GameObject*)b->GetUserData();
            myObject.Sprite.position = CGPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO);
            myObject.Sprite.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
        }   
    }
}

and finally the bodyObject

#import "BodyObject.h"


@implementation BodyObject

@synthesize Body;
@synthesize Fixture;

-(id) initWithBody:(b2Body*) body 
        andFixture:(b2Fixture*) fixture 
       andVelocity:(b2Vec2*) velocity 
         andSprite:(CCSprite*) sprite
{
    self = [super initWithSprite:(CCSprite*)sprite 
                     andVelocity:(b2Vec2*)velocity];

    self.Body=body;
    self.Fixture=fixture;

    return self;
}

@end

im new to objective c so if im doing anything majorly wrong please let me know!

EDIT: I have tried another bit of code here and i seem to be able to get a bit further.

if I set bodyDef.UserData = sprite; 

just after the line

bodyDef.position.Set(sprite.position.x/PTM_RATIO, sprite.position.y /PTM_RATIO); 

then GetUserData returns the sprite. if however I set the user data to the sprite at the end of the addPlayerShip method then it returnns null again. Is the sprite / body object being disposed of somewhere?

回答1:

You are assigning the userdata to the bodyDef AFTER you call CreateBody(), so it is never being added to the actual body.

b2BodyDef is a struct that holds settings to create a body. Once you've called CreateBody(), changes to this struct have no effect.

You found the solution yourself by moving the bodyDef.UserData = sprite; line somewhere before CreateBody() is called. This is the correct place to put it. If you think you need it at the end of the function for some reason (other variables?), then you need to refactor your code so that this is not the case.

I would change this line (and the BodyObject class):

BodyObject *bodyObject = [[BodyObject alloc] initWithBody:body 
                                               andFixture:fixture 
                                              andVelocity:vector 
                                                andSprite:sprite];

so that you can instantiate it without needing the box2d elements (i.e. before they have been defined), then use the properties to assign these values after they have been created. This way you can pass this object as the body's userdata, create the body, then assign the b2Body* using [bodyObject setBody:body];

Don't forget to ensure your Body and Fixture properties are not readonly.

The final code might look like this:

-(void) addPlayerShip
{
    CCSpriteSheet *sheet = [CCSpriteSheet spriteSheetWithFile:@"PlayerShip.png" capacity:1];

    //Get sprite sheet
    CCSprite *sprite = [CCSprite spriteWithSpriteSheet:sheet rect:CGRectMake(0,0,64,64)];
    [sheet addChild:sprite];

    CGSize screenSize = [CCDirector sharedDirector].winSize;

    sprite.position = ccp( screenSize.width/2, screenSize.height/2);

    [self addChild:sheet];

    b2Vec2 *vector = new b2Vec2;

    BodyObject *bodyObject = [[BodyObject alloc] initWithSprite:sprite 
                                                   andVelocity:vector];

    //MOVES THE BODY DEFINITION AFTER THE BODYOBJECT DEFINITION
    // Define the dynamic body.
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position.Set(sprite.position.x/PTM_RATIO, sprite.position.y /PTM_RATIO);
    bodyDef.angularVelocity=0;
    bodyDef.userData = bodyObject;

    b2Body *body = world->CreateBody(&bodyDef);

    b2PolygonShape dynamicTriangle;

    dynamicTriangle.m_vertexCount=3;
    dynamicTriangle.m_vertices[0].Set(0,-1);
    dynamicTriangle.m_vertices[1].Set(1,1);
    dynamicTriangle.m_vertices[2].Set(-1,1);

    // Define the dynamic body fixture.
    b2FixtureDef fixtureDef;
    fixtureDef.shape = &dynamicTriangle;
    fixtureDef.density = 1.0f;
    fixtureDef.friction = 0.0f;
    fixtureDef.restitution=0.0f;

    b2Fixture *fixture = body->CreateFixture(&fixtureDef);

    [bodyObject setBody:body];
    [bodyObject setFixture:fixture];
}

The bodyObject class would then simply look like this:

#import "BodyObject.h"


@implementation BodyObject

@synthesize Body;
@synthesize Fixture;

-(id) initWithSprite:(CCSprite*) sprite 
       andVelocity:(b2Vec2*) velocity 
{
    self = [super initWithSprite:(CCSprite*)sprite 
                     andVelocity:(b2Vec2*)velocity];

    return self;
}

@end


回答2:

If anyone has the same problem that GetUserData() returns null, even though you SetUserData. Then don't make my mistake and assume than b2World.QueryShape() gives bodies in callback. Nope, it gives you Fixtures. You have to fixture.GetBody().GetUserData() (my case was: box2dweb in javascript; this is the question I googled for answer, I assume others will come this way)