Menu

Box2D Game Objects Part 1

As promised, this starts the series on how to create custom Game Objects (GOs) in AGE.

Our example uses the box2d framework, for creating physics-based action with AGE. We assume you are somewhat familiar with box2d and won't elaborate too much on its internals.

box2d is made up of a few distinct components, and we will concentrate on the World and Body components, as these will get you started. This first part will concentrate on the internals of the World component implemented for AGE.

First, some high-level details:

  • The world updates at regular intervals, so TimerCallback is implemented to handle that task.
  • Body components come-and-go as dictated by game logic, so LoadedCallback and UnloadedCallback are implemented to handle that.
  • There are some box2d callbacks that must be implemented for a meaningful component, namely ContactListener and BoundaryListener. The Event Pipeline is implemented to handle sending those callbacks to the rest of the GO ecosystem. This works as a benefit, because you cannot alter the box2d environment from within these callbacks.
  • We set up a small number of GO properties to configure the box2d World component.
  • We create the concept of filter flags so you can easily "categorize" and query the Body components according to their use in the game. Since they are flags you can set multiple on each Body.

Let's get started!

public class Box2DWorld extends GameObjectWithProperties implements RequireResourceLoader, TimerCallback, LoadedCallback, UnloadedCallback {
public static final String NAME = "World";
public static class Property {
    public static int LOWER = Constants.Property.USER_DEFINED_START;
    public static int UPPER = Constants.Property.USER_DEFINED_START + 1;
    public static int INITIAL_GRAVITY = Constants.Property.USER_DEFINED_START + 2;
}
World m_world;
AABB m_worldAABB;
final Installer in;
final String[] route;
final ArrayList<Box2DBody> list;
final HashMap<Body, Box2DBody> bodymap;
final int dur;
final Vec2 force;

Here we set up the fixed parameters for the World GO, the most important of which is the Route for events.

public Box2DWorld(int dur, Installer in, String[] route) {
    super(NAME, true);
    this.dur = dur;
    this.force = new Vec2();
    this.in = in;
    this.route = route;
    this.list = new ArrayList<Box2DBody>();
    this.bodymap = new HashMap<Body, Box2DBody>();
    this.set(Property.LOWER, new Vec2(-10f, -10f));
    this.set(Property.UPPER, new Vec2(10f, 10f));
    this.set(Property.INITIAL_GRAVITY, new Vec2(0f, -9f));
}

Here we implement the box2d callbacks we will be sending over the Event Pipeline. We are forwarding every contact and bounds event.

final class GameContactListener implements ContactListener {
    @Override
    public void add(ContactPoint point) {
    }
    @Override
    public void persist(ContactPoint point) {
    }
    @Override
    public void remove(ContactPoint point) {
    }
    @Override
    public void result(ContactResult point) {
        final Box2DBody part1 = (Box2DBody) point.shape1.m_body.getUserData();
        final Box2DBody part2 = (Box2DBody) point.shape2.m_body.getUserData();
        try {
            in.event(new Contact(part1, part2, point), route);
        } catch (Exception e) {
        }
    }
}

The boundary listener is important, because box2d invalidates the Body resources immediately after returning from violation(). Because of this, the bookkeeping is done immediately, and because of the update lock there is no fear of "messing up" the state.

final class GameBoundsListener implements BoundaryListener {
    @Override
    public void violation(Body body) {
        try {
            // box2d body is going away after this call; do bookkeeping
            final Box2DBody bb = bodymap.get(body);
            if(bb != null) {
                bb.unload(Box2DWorld.this, bodymap);
                in.event(new BoundsViolation(bb), route);
            }
        } catch (Exception e) {
        }
    }
}
protected void getForce(Vec2 force) {
    GlobalForces.get(force);
    force.x *= GlobalForces.X_FACTOR;
    force.y *= GlobalForces.Y_FACTOR;
}

Here we use RequireResourceLoader to defer some initialization until the Install Pipeline. This gives the "owner" a chance to initialize properties before they take effect.

@Override
public void load(ResourceLoader rl, Services svc) {
    m_worldAABB = new AABB((Vec2)getAs(Property.LOWER), (Vec2)getAs(Property.UPPER));
    final boolean doSleep = true;
    m_world = new World(m_worldAABB, (Vec2)getAs(Property.INITIAL_GRAVITY), doSleep);
    m_world.setContactListener(new GameContactListener());
    m_world.setBoundaryListener(new GameBoundsListener());
}

These are convenience methods to get access to creating the other box2d components, and to get relevant data directly from the World. These are intended for use during the Install/Uninstall pipelines (see below).

public Body createBody(BodyDef bd) { return m_world.createBody(bd); }
public Joint createJoint(JointDef jd) { return m_world.createJoint(jd); }
public void destroyBody(Body bd) { m_world.destroyBody(bd); }
public void destroyJoint(Joint jd) { m_world.destroyJoint(jd); }
public int getBodyCount() { return m_world.getBodyCount(); }
public Vec2 getGravity() { return m_world.getGravity(); }
public void resetGravity() {
    m_world.setGravity((Vec2)getAs(Property.INITIAL_GRAVITY));
}
public void adjustGravity(Vec2 gravity) {
    final Vec2 gv = m_world.getGravity();
    final Vec2 ng = new Vec2(gv.add(gravity));
    m_world.setGravity(ng);
}
public Shape[] query(AABB box, int maxc) { return m_world.query(box, maxc); }

These next two methods return the bodies that match the given filter flags. This is convenient for selecting Body components by "category", e.g. everything that is a "ball" or everything that is an "obstacle".

These methods get heavy use in the game logic, to select Body components for modification.

@SuppressWarnings("unchecked")
public <T extends Box2DBody> void filterAll(int flags, ArrayList<T> list) {
    for(int ix = 0; ix < list.size(); ix++) {
        final Box2DBody bb = list.get(ix);
        if((bb.filterFlags & flags) == flags) {
            list.add((T)bb);
        }
    }
}
@SuppressWarnings("unchecked")
public <T extends Box2DBody> void filterAny(int flags, ArrayList<T> list) {
    for(int ix = 0; ix < list.size(); ix++) {
        final Box2DBody bb = list.get(ix);
        if((bb.filterFlags & flags) != 0) {
            list.add((T)bb);
        }
    }
}

The Install Pipeline receives incoming Body GOs, and connects them to the box2d world. At this point, the incoming GO can create any box2d resources required, e.g. BodyDef etc.

@Override
public void loaded(GameObject arg0, Exception arg1, Locator lc) {
    if(arg1 == null && arg0 instanceof Box2DBody) {
        final Box2DBody bb = (Box2DBody)arg0;
        list.add(bb);
        bb.load(this, bodymap);
    }
}

The Uninstall Pipeline receives outgoing Body GOs, and stops tracking. The GO can be installed at a later time, and will rebuild any box2d resources that were invalidated by BoundaryListener.

@Override
public void unloaded(GameObject arg0, Exception arg1, Locator arg2) {
    if(arg1 == null && arg0 instanceof Box2DBody) {
        final Box2DBody bb = (Box2DBody)arg0;
        list.remove(bb);
    }
}

Finally, there is the world-update logic, which handles applying forces, invoking the box2d update itself, and synchronizing GO position and orientation with the results. There is an interface UnderForce bodies can implement if they want forces applied, otherwise they are "passive" bodies that are affected by gravity and other bodies from contact.

Note the use of explicit for loops, to avoid creating iterator garbage.

@Override
public void execute(long delta, long elapsed, boolean last, Locator lc, Installer in) {
    if (delta > 0) {
        if(list.size() > 0) {
            // apply forces
            getForce(force);
            for(int ix = 0; ix < list.size(); ix++) {
                final Box2DBody bx = list.get(ix);
                if(bx instanceof UnderForce) {
                    ((UnderForce)bx).applyForce(force);
                }
            }
        }
        m_world.step((float)delta/1000f, 6);
        if(list.size() > 0) {
            // sync new positions
            for(int ix = 0; ix < list.size(); ix++) {
                final Box2DBody bx = list.get(ix);
                bx.sync();
            }
        }
    }
}
@Override
public boolean getRegisterOnInstall() {
    return true;
}
@Override
public void setConfig(TimerConfig tc, int tbt) {
    tc.durationMS = dur;
    tc.continuous = true;
    tc.autoRepeat = true;
}
}
Posted by g-dollar 2013-06-04

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.