Sorry for the delay between posts, but it's been busy elsewhere!
In this possibly final part of the series, we discuss some specific Box2D subclasses that represent concrete game objects.
Since it's getting concrete, you will notice some model-related bookkeeping.
In this game, a Ball is just that, a circular object that bounces around the play field, and is under force via the device's accelerometer.
public class Ball extends Box2DBody implements UnderForce {
static final float MODEL_RADIUS = 1f;
public static final class Property {
public static final int RADIUS = Constants.Property.USER_DEFINED_START;
public static final int PHYSICAL = Constants.Property.USER_DEFINED_START + 1;
}
CircleDef sd;
public Ball(String name, Geometry model, int depth) {
super(name, model, depth);
set(Property.RADIUS, Float.valueOf(1f));
}
Here is the bookkeeping for the transform, which takes the model's scale into account, in case it is at a different scale than unit size.
@Override
protected void notifyPropertyChanged(int propertyId) {
if(propertyId == Property.RADIUS) {
final Float rx = getAs(Property.RADIUS);
if(rx != null) {
final Transform sx = getAs(Constants.Property.TRANSFORM);
if(sx != null) {
sx.sx = rx.floatValue()/MODEL_RADIUS;
sx.sy = rx.floatValue()/MODEL_RADIUS;
sx.sz = rx.floatValue()/MODEL_RADIUS;
set(Constants.Property.TRANSFORM, sx);
}
else {
final Transform ssx = new Transform(0f, 0f, 0f, rx.floatValue()/MODEL_RADIUS);
set(Constants.Property.TRANSFORM, ssx);
}
}
}
else {
super.notifyPropertyChanged(propertyId);
}
}
Here is where the forces are applied to Box2D objects.
@Override
public void applyForce(Vec2 force) {
if(passive) return;
if(body == null) return;
//Log.d("Force", force.toString());
body.applyImpulse(force, body.getWorldCenter());
}
The next 2 methods configure the Box2D resources for being a ball with specific size and physical properties.
@Override
protected void configureBodyDef(BodyDef bd) {
final Transform p3d = getAs(Constants.Property.TRANSFORM);
if(p3d != null) {
final Vec2 p2d = new Vec2(p3d.tx, p3d.ty);
bd.position = p2d;
}
bd.isBullet = true;
}
@Override
protected void configureBody(Body bd) {
final PhysicalProps pp = getAs(Property.PHYSICAL);
sd = new CircleDef();
final Float fx = getAs(Property.RADIUS);
sd.radius = fx != null ? fx.floatValue() : 10f;
if(pp != null) {
sd.density = pp.density;
sd.restitution = pp.restitution;
sd.friction = pp.friction;
}
else {
sd.density = .1f;
sd.restitution = .6f;
sd.friction = .2f;
}
sd.filter.categoryBits = 1;
sd.filter.groupIndex = 1;
bd.createShape(sd);
bd.setMassFromShapes();
final Vec2 vx = getAs(Constants.Property.VELOCITY);
if(vx != null) {
bd.setLinearVelocity(vx);
}
}
}
The Ground piece is slightly more involved; it contains a regularly-changing action that lasts for a fixed amount of time, and performs interesting things in the game, e.g. score points, add time, adjust gravity, etc. This is called SpawnAction. These are assigned by another task called Spawner at regular intervals. A new action is accepted by sending it to EventCallback.
Contacts are handled the same way, via the EventCallback. The contact is passed to the current SpawnAction (if any) for processing.
SpawnAction uses the Install and Uninstall pipelines with Ground as the target, to resolve bookkeeping. So a Ground starts a SpawnAction by installing, and when its timer goes off, the SpawnAction uninstalls itself.
This piece has similar bookkeeping for the model, so we will not repeat any comments there.
public class Ground extends Box2DBody implements LoadedCallback, UnloadedCallback, EventCallback {
public static final class Property {
public static final int HALF_DIMENSION = Constants.Property.USER_DEFINED_START;
public static final int PHYSICAL = Constants.Property.USER_DEFINED_START + 1;
}
PolygonDef sd;
final FilterData fd = new FilterData();
SpawnAction sa;
public Ground(String name, Geometry model, int depth) {
super(name, model, depth);
set(Property.HALF_DIMENSION, new Vec2(.5f, .5f));
set(Property.PHYSICAL, new PhysicalProps(0f, .618f, .206f));
}
@Override
protected void notifyPropertyChanged(int propertyId) {
if(propertyId == Ground.Property.HALF_DIMENSION) {
final Vec2 hd = getAs(Property.HALF_DIMENSION);
final Transform sx = getAs(Constants.Property.TRANSFORM);
if(sx != null) {
sx.sx = hd.x * 2f;
sx.sy = hd.y * 2f;
set(Constants.Property.TRANSFORM, sx);
}
else {
final Transform ssx = new Transform();
ssx.sx = hd.x * 2f;
ssx.sy = hd.y * 2f;
set(Constants.Property.TRANSFORM, ssx);
}
}
else {
super.notifyPropertyChanged(propertyId);
}
}
@Override
protected void configureBody(Body bd) {
sd = new PolygonDef();
final Vec2 hd = getAs(Property.HALF_DIMENSION);
sd.setAsBox(hd.x, hd.y);
final PhysicalProps pp = getAs(Property.PHYSICAL);
sd.friction = pp.friction;
sd.restitution = pp.restitution;
sd.density = pp.density;
sd.filter.categoryBits = 1;
sd.filter.groupIndex = 1;
bd.createShape(sd);
if(pp.density > 0f)
bd.setMassFromShapes();
}
@Override
protected void configureBodyDef(BodyDef bd) {
final Transform p3d = getAs(Constants.Property.TRANSFORM);
if(p3d != null) {
final Vec2 p2d = new Vec2(p3d.tx, p3d.ty);
bd.position = p2d;
}
}
protected void internalSetAction(SpawnAction sa) {
if (sa instanceof CreateHole) {
fd.categoryBits = 1;
fd.groupIndex = -1;
} else {
fd.categoryBits = 1;
fd.groupIndex = 1;
}
if(sa == null) {
// reset visibility
setVisible(true);
}
if(body != null) {
body.getShapeList().setFilterData(fd);
}
}
void setAction() {
if (sa != null) {
internalSetAction(sa);
setVisible(sa.isVisible());
}
}
SpawnAction resetAction() {
final SpawnAction prev = this.sa;
internalSetAction(null);
this.sa = null;
return prev;
}
void handleContact(Locator lc, Installer in, Contact cx) {
if(sa instanceof ContactTrigger) {
((ContactTrigger)sa).contactTrigger(lc, in, cx);
}
else if(sa instanceof NonContactTrigger) {
((NonContactTrigger)sa).nonContactTrigger(lc, in);
}
}
public boolean isAvailable() { return sa == null; }
Of special note below is that the accepted SpawnAction is passed to the Install Pipeline if it is accepted.
@Override
public void event(GameObject go, Locator lc, Installer in) {
if(go instanceof SpawnAction) {
// see if we can accept this action
if(sa == null) {
// yes we can
try {
in.install(go, new String[] { name });
// set this so we are tracking it now
sa = (SpawnAction)go;
sa.setTarget(name);
} catch (Exception e) {
Log.e(name, "event.install", e);
}
}
else {
// nothing yet; recycle later on
}
}
else if(go instanceof Contact) {
handleContact(lc, in, (Contact)go);
}
}
@Override
public void loaded(GameObject go, Exception ex, Locator lc) {
if(go == sa) {
// we were expecting you....
if(ex == null)
setAction();
else
resetAction();
}
}
@Override
public void unloaded(GameObject go, Exception ex, Locator lc) {
if(go == sa) {
resetAction();
}
}
}
Finally there is the SpawnAction which uses TimerCallback to do its thing. The only interesting thing here is when the timer expires, it runs the Uninstall Pipeline and uses the target given to it via setTarget().
As seen above, there are 2 general kinds of trigger: contact and non-contact, the only difference is one requires the contact information.
public abstract class SpawnAction extends GameObject implements TimerCallback {
public int loc;
public int duration;
protected String target;
protected SpawnAction(String name) { super(name, false); }
protected SpawnAction(String name, int loc, int dur) {
super(name, false);
this.loc = loc;
this.duration = dur;
}
protected void release(Installer in) {
try {
in.uninstall(name, new String[] { target });
} catch (Exception e) {
}
}
public void setTarget(String tx) { target = tx; }
public boolean isVisible() { return true; }
@Override
public void execute(long arg0, long arg1, boolean last, Locator lc, Installer in) {
if(last) {
release(in);
}
}
@Override
public boolean getRegisterOnInstall() {
return true;
}
@Override
public void setConfig(TimerConfig tc, int tbt) {
tc.autoRepeat = false;
tc.continuous = false;
tc.durationMS = duration;
}
}
There's a little more to it, but it's outside the scope of Box2D, so maybe that is for another post!