April 2, 2013

Box2D via Libgdx in a top-down Action Scroller Game

Recently I wanted to let the sprites of Diabolus Ex Machina bounce whenever they collide with each other or hit any kind of wall tile. As Libgdx is already used and comes with a native binding to the physics library Box2D, it was an easy decision to make use of Box2D instead of doing it all by myself and reinvent the wheel.



Game physics, movement, collision detection, bouncing

Initially, the tile based game scenes needs to be set up as Box2D world. There are basically three kinds of tile groups:
  • floor tiles, always passable 
  • wall tiles, never passable 
  • door tiles, passable when open 
For all wall and door tiles are corresponding Box2D bodies created and added to the world. Note that bodies are positioned based on their centers instead of corners.

World settings:

Setting Value Comments
world gravity 0 rather useful in a sidescroller
pixelPerMeter 64 pixel matches the tile size of 64x64


From inside the game loop, the following three steps are executed.

1. Calculating the steering force for each sprite:

Each sprite holds its desired heading as unit vector, a current velocity vector, the maximum velocity as single float field, its acceleration as well as the corresponding Box2D body. The required force to speed up, slow down or to keep the maximum velocity is calculated as follows for each sprite: 

// input from sprite
Vector2 desiredHeading = sprite.getDesiredHeading();
Vector2 velocity = sprite.getVelocity();

float maxVelocity = sprite.getMaxVelocity()
float acceleration = sprite.getAcceleration();

// force to reach and keep desired velocity
steeringForce.x = desiredHeading.getX();
steeringForce.y = desiredHeading.getY();
steeringForce.mul(
maxVelocity);
steeringForce.sub(velocity.getX(), velocity.getY());
steeringForce.mul(
acceleration );

steeringForce.div(pixelPerMeter);

// apply steering force
Body body = sprite.getBody();
if (body.isAwake() || !desiredHeading.isZero()) {
    steeringCenter.x = body.getWorldCenter().x;
    steeringCenter.y = body.getWorldCenter().y;
    body.applyForce(steeringForce, steeringCenter);

}

The steering force derives from the delta vector between maximum (= desired velocity) and current velocity, multiplied by an arbitrary acceleration factor.
steeringForce and steeringCenter are vectors created once and reused for all calculations.
Because the game operates with pixels while Box2d prefers meters, all values must be converted accordingly.

2. After applying all forces to all sprite bodies, the world gets updated once for each game frame:

World world = getWorld();
world.step(deltaInSeconds, velocityIterations, positionIterations);
world.clearForces();


3. Finally, the new body velocities and locations can be retrieved from Box2D and used for rendering the scene:

Vector2 worldVelocity = sprite.getBody().getLinearVelocity();
Vector2 pixelVelocity = new Vector2(worldVelocity.x / pixelPerMeter, worldVelocity.y / pixelPerMeter);


Vector2 worldPosition = body.getPosition();
Vector2 pixelPosition = new Vector2(worldPosition.x / pixelPerMeter, worldPosition.y / pixelPerMeter);


The overall acceleration behaviour depends upon body density and shape, acceleration, damping and friction.
The following settings were found to give a nice movement behaviour for the Bionic sprites:

Setting Value Comments
linear damping 0 the higher the damping the lower the reachable velocity
density 60 heavier sprites accelerate slower
friction 0.2 sliding between sprites and walls
restitution 0.7 lets sprites bounce with each other as well as with solid walls
acceleration 150 gives a nice acceleration effect
if too high, the velocity will start to jitter
shape CircleShape
radius: 32 pixel = 0.5 meter
bigger sprites accelerate slower

And the settings for fast moving bullets:

Setting Value Comments
linear damping 0 no damping for flying bullets
density 10 lighter in relation to Bionics
friction 0
restitution 0 bullets do not need to bounce on collisions
acceleration 50 appropriate to nearly instantly get on maximum velocity
shape CircleShape
radius: 10 pixel ~ 0.15 meter
smaller in relation to Bionics


Field of View

Another nice use case of Box2d is for detecting the field-of-view of game actors. This is often necessary for implementing the game AI.
With Box2d this can be done with raycasting. Basically, for each game actor a ray is casted to all other actors. If the ray hits obstacles like a wall the other actor is not visible.

All we need is to call
World#rayCast(RayCastCallback callback, Vector2 point1, Vector2 point2);

with an appropriate callback interface like this:

public class VisibilityCallback implements RayCastCallback {

    private Actor targetActor;

    private boolean visible;

    @Override
    public float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction) {
        if (fixture.getBody().getUserData().equals(targetActor)) {
            return fraction;
        }

        if (fixture.getBody().getUserData() instanceof Wall) {       
                visible = false;
                return fraction;           
        }

        return -1;
    }

 

    public boolean isVisible() {
        return visible;
    }
}


Beforehand, all body fixtures are equipped with game actors respectively environment objects like walls.
After each ray casting the visibility state can simply be queried by invoking isVisible().

To reduce the number of ray casts, only ray cast
  • once between each pair of actors
  • to actors relevant for the game AI
  • to actors inside the general range of view

No comments:

Post a Comment