2
votes

If a circle stretches during the phases of launch, and becomes elongated (gaining height and losing width) how to dynamically adjust the collision shape (in Box2D) as it's scaling? Can this be done through each frame of all the steps to full stretch and back to a circle shape?

Similarly, when bouncing, it squashes out a little, and needs to detect more width and less height.

enter image description here

Imagine this is a Hero character in a game, and he's a bit of a jumper, even able to do midair 'double jumps'. But most of the elongation and squishing happens during ground interactions. Something like this:

enter image description here

1
Hello, please notice that implementation of solution for this can differ in different game engines - if you are using any specific technology (like Unity, pyGame, libGDX...) update your tags - m.antkowicz

1 Answers

3
votes

We have two parts of the issue:

  1. Changing fixture shape dynamically

    Box2D supports various types of shapes like:

    • CircleShape (simple)
    • PolygonShape (simple)
    • ChainShape, EdgeShape (complex)

    The situation here depends on what type of shape you need here. If "simple" type of shape is enough the solution is very easy and it will be (for PolygonShape) something like

    //! I'm using LibGDX in code examples but you can translate it to other engines
    
    Body body;
    
    ...
    
    //assuming that body has one fixture - if not just keep it's reference and call it directly
    ((PolygonShape) body.getFixtureList().first().getShape() ).setAsBox(hx, hy);
    

    notice here that for CircleShape you do not even need to cast it because Shape class contains setRadius method

    If you need "complex" type of shape the situation is more complicated but still kind of easy. The way you handle it is to destroy current fixture and recreate it with another shape like

    Body body;
    FixtureDef fixtureDef;
    
    ...
    
    //creating fixturDef - for further usage (setting friction, density etc...)
    
    ...
    
    //again assuming that body has one fixture - but also can keep reference
    body.destroyFixture( bb1.getFixtureList().first() );
    
    ChainShape shape = new ChainShape();
    //creating shape...
    
    fixtureDef.shape = shape;
    
    body.createFixture(fixtureDef);
    
    //you must dispose shape after fixture creating!
    shape.dispose();
    
  2. Creating ellipse

    Although Box2D supports various kinds of shapes there's no ellipse type and you need to use some technique to generate it on our own. In the internet there are many ways described - I will show you how to generate ellipse as ChainShape instance.

    I will be using parametric equation for ellipse with 0 <= t < 2π (in radians). The method looks like:

    //overriding method to handle default STEPS value
    ChainShape createEllipse(float width, float height)
    {
        return createEllipse(width, height, 64);
    }
    
    ChainShape createEllipse(float width, float height, int STEPS)
    {
        ChainShape ellipse = new ChainShape();      
        Vector2[] verts = new Vector2[STEPS];
    
        for(int i = 0; i < STEPS; i++)
        {
            float t = (float)(i*2*Math.PI)/STEPS;
            verts[i] = new Vector2(width * (float)Math.cos(t), height * (float)Math.sin(t));
        }
    
        ellipse.createLoop(verts);
        return ellipse;
    } 
    

    where:

    • width - width of the ellipse
    • height - height of the ellipse
    • STEPS - count of points that creating ellipse (bigger STEPS = smoother ellipse but more expensive to generate)

    So to sum up - assuming that you know what size of body is during simulation (because if you do not know it is big topic bigger than this question) the scenario would be:

    • check if I should change fixture (has body's size changed?)
    • if yes:
      • destroy old fixture
      • generate new shape with new size
      • create new fixture
      • dispose created shape

    in a code it would be something like:

    //in render method
    if( bodySizeChanged() ) //this method is checking somehow if you should change fixture
    {
        //again assuming that body has one fixture - but also can keep reference           
        body.destroyFixture( body.getFixtureList().first() );
    
        ChainShape ellipse = createEllipse(newWidth, newHeight);
        fixtureDef.shape = ellipse;
    
        body.createFixture(fixtureDef);
        ellipse.dispose();
    }
    

    Of course creating new shape every time is not very efficient and better way would be to create some collection of shapes Array<ChainShape> or even FixtureDef on loading (e.g. in show() method) and just get the desired shape/fixtureDef during render method.


EDIT - for example if you have a collection of sprites for your animation you can generate another collection of shapes for each of them - but only if previous animation frame has another size to not to change shape to the same shape - it would be something like:

    Sprite[] frames = new Sprite[N]; //N sprites for animation
    ChainShape[] shapes = new ChainShape[N]; //N shapes

    ...

    Vector2 lastSize = new Vector2(0,0); //or another initial value - depends on what sizes your sprites have

    for(int i = 0; i < frames.length; i++)
    {
        Sprite s = frames[i];

        if(lastSize.x != s.getWidth() || lastSize.y != s.getHeight())
        {
            shapes[i] = createEllipse(s.getWidth(), s.getHeight());
            lastSize.set(s.getWidth(), s.getHeight());
        }
        else
        {
            shapes[i] = null;
        }
    }

and then in the render method

    //render method
    int index = getCurrentAnimationFrameIndex(animation); //some function to get index of current sprite of character

    if(shapes[index] != null)
        //change fixture

Of course you should remember to dispose all not-null shapes at the end of your application (at least if it is Libgdx)!


One interesting thing more that I have discovered here is that ChainShape is not rotating during collision what was a kind of weird for me. Actually it could be good for you (I guess that the ball is some kind of character and it should not has a rotation - maybe you will even set some setFixedRotation on its body) but if you want the body to rotate the way to do it is to define MassData with I (inertia) set to some value and then attach this MassData to body

    MassData m = new MassData();

    m.center.set(0, 0);
    m.I = 100; //example value
    body.setMassData(m); 

Unfortunately I cannot tell you what exactly value should be set there but you can read more on Wikipedia site.