** Originally written in Fall 2011. **
This semester, I worked on Corpionage, a Facebook game for people who like to mess with their friends. I thought it turned out pretty well, but there are a few key take-aways.
Our general architecture:
The server is Google App Engine, the UI is GWT, and the Engine is PlayN. (More detail)
-
Automated testing is non-negotiable. In the last few weeks of the project, a bug fix was just trading a bug we knew about for one or more that we didn't. We had automated testing for the backend, and it was the most stable part of our codebase.
-
Tools add potential for error. Maven and GWT gave us many issues. A great deal of engineering time was wasted trying to fix dev environments. We ended up dropping maven, which drastically increased engineering time available to actually write code.
-
People over-estimate their own skill. Bad coders produce negative work and drag down the team's morale. If possible, make repo write privileges predicated on some level of code quality.
-
Events are pretty nice. Our engine, UI, and server communicate with each other largely through events. Particularly in a game where you can have many things happening that different parts of the code are interested in, it's really great to just publish an event that anyone can consume.
Our events were identified by a string key. This allowed us to avoid making a new class for each distinct event:
public class PositionEvent extends LevelEvent {
// ...
private int x;
private int y;
private Direction direction;
public PositionEvent(String key, int x, int y, Direction direction, int levelIndex,
long owningPlayerId) {
super(key, levelIndex, owningPlayerId);
// ...
}
// ...
}
To fire an event:
PositionEvent showInvader =
new PositionEvent(DefendController.SHOW_INVADER, x,
y, direction, levelIndex, currentPlayerId)
GameHost.getInstance().dispatchEvent(showInvader);
DefendController.SHOW_INVADER
is the key that identifies this specific event. There are other keys that also use PositionEvent, which save us the pain of making a bunch of empty classes:
public class ShowInvader extends PositionEvent { }
public class CameraCaught extends PositionEvent { }
public class WorkerCaught extends PositionEvent { }
Then, to register for an event, you’d do:
dispatcher.addEventListener(this, SHOW_INVADER);
@Override
public void eventRecieved(GameEvent event) {
// cast event to PositionEvent and handle appropriately
}
One possible source of error is that it’s possible that an event other than PositionEvent
will be created with key SHOW_INVADER
, and code that depends on this for casting will crash.
Another issue is that if you don’t want to make a new anonymous inner class for each event type, you frequently end up with handlers something like this:
@Override
public void onEvent(GameEvent event) {
PlayerDataEntity player = PlayerDataEntityUtils.get().getCurrentPlayer();
if (event.getKey().equals(BuildResponseEvent.BUILD_RESPONSE)) {
handleBuildResponse(event, player);
} else if (event.getKey().equals(DeleteResponseEvent.DELETE_RESPONSE)) {
handleDeleteResponse(event, player);
} else if (event.getKey().equals(HarvestDeskEvent.HARVEST_DESK)) {
handleHarvestDesk(event);
} else if (event.getKey().equals(PlayerEntity.FILE_STOLEN)) {
handleFileStolen(event);
}
}
Anonymous inner classes are a bit bulky, but are an unfortunate necessity because Java doesn’t allow functions as first class members of the language.
Corpionage was a lot of fun to make, but there were definitely many "learning experiences" along the way.