Asynchronous Games in Firebase; pt II
Last post I looked at the motivation for writing asynchronous multiplayer games on top of Firebase. This post I’ll sketch an outline on how to actually do it.
The first thing to consider is that we now have two notions: The Firebase concept of area (called “table” for legacy reasons) and the overall concept of a “game”. Firebase tables are associated with games, but whereas in a synchronous game the table probably would be the game such that if the table is closed, the game is over, in asynchronous games the table may come and go, but the game itself would survive.
Oh by the way, I’ll use Java in this post, but remember you can write it in script languages Ruby, Python and Groovy as well. 🙂
Let’s step it through:
- The game is created. This is probably done on a website somewhere and does not involve Firebase at all.
- Client connects. When game client is opened (someone wants to make a move), the client connects to Firebase and does the following:
- Search the lobby for any table with the correct “gameId” set in the attributes.
- If a table is found, attempt a “join” command.
- If a table is not found, or the join fails, send a table creation request and make sure to include the “gameId”.
- Activator creates table. When a client cannot find a table for the game, it send a table creation request, the activator then reads the game and its state from database, and creates a new table.
- Game play. The player makes its moves and actions normally via Firebase. The game in Firebase either saves the state on each action, or delegates to the time when the table is being closed (see below).
- Table closes. When the activator finds table which has not been accessed for some time. If needed it should also save the game state at this point (see above).
I’ll focus on points no. 3, 4 and 5 above for the rest of this post. And I’ll use code examples from the Kalaha game I’m currently involved in writing.
Activator / Table creation
The game activator in Firebase is the components that know how to create and destroy tables in the system. We’ll follow best practises (but we’ll skip the “init” state for now) as it helps us save the state to database.
The activator should first make sure to implement RequestAwareActivator to make sure it gets the requests:
public class ActivatorImpl implements GameActivator, RequestAwareActivator { [...]
When a client wants a table to join for a specific game, it’ll send a table creation request, and the activator should read the game state from the database and create a new table. Somewhat compressed, it may look like this:
@Override public RequestCreationParticipant getParticipantForRequest( int pid, int seats, Attribute[] atts) throws CreationRequestDeniedException { // find the game id in the parameters int gameId = getKalahaGameId(atts); if(gameId == -1) { // you may want to handle this as a special form of "new game" } else { log.debug("Ressurecting game " + gameId + " for player id " + pid); // read the game from the database Game game = gameManager.getGame(gameId); if(game == null) { // code 1 for "no such game" throw new CreationRequestDeniedException(1); } return new Participant(game); } }
The Participant is an inner class for handling the request, like so:
private static class Participant implements RequestCreationParticipant { private final Game game; public Participant(Game game) { this.game = game; } @Override public void tableCreated(Table table, LobbyTableAttributeAccessor atts) { table.getGameState().setState(new net.kalaha.game.action.State(game.getState())); atts.setStringAttribute(TABLE_STATE_ATTRIBUTE, "OPEN"); atts.setIntAttribute("gameId", game.getId()); } @Override public LobbyPath getLobbyPathForTable(Table table) { return new LobbyPath(table.getMetaData().getGameId(), "", table.getId()); } [...] }
As you can see above the creation participant sets the Game object on the table when it is created. It also sets a lobby attribute with the game ID which is important for the client to find the table. The lobby path above is kept simple for this example and the TABLE_STATE_ATTRIBUTE is a constant you can define yourself.
Now we need to close the table when it isn’t used. This is somewhat outside the scope of this post, but I’ll post some pseudo code here to demonstrate table destruction:
public void checkTables() { TableFactory fact = context.getTableFactory(); for (LobbyTable table : fact.listTables()) { int tableId = table.getTableId(); long lastModified = getLastModifiedFromAttributes(table); int seated = getSeatedFromAttributes(table); if(seated == 0 && isOld(lastModified)) { checkClose(tableId, table); } } }
The above should be called regularly from a scheduled task. The “get from attributes” method are trivial, for example:
private long getLastModifiedFromAttributes(LobbyTable table) { Map map = table.getAttributes(); AttributeValue a = map.get(DefaultTableAttributes._LAST_MODIFIED); return a.getDateValue().getTime(); }
The attributes “last modified” and “seated” are standard attributes and always available. The “table state” attribute is not and you’d have to set and get it yourself. In “check close” we’ll check the table state, destroy it if it is closed and if not, send an action to the table in order to clsoe it:
private void checkClose(int tableId, LobbyTable table) { TableFactory fact = context.getTableFactory(); String state = getTableStateFromAttributes(table); if(state.equals("CLOSED")) { // the table is closed, so destroy fact.destroyTable(tableId, true); } else { sendCloseActionToTable(tableId); } }
And finally, the method to send a “close youself” action to the table would look something like this:
private void sendCloseActionToTable(int tableId) { ActivatorRouter router = context.getActivatorRouter(); CloseTableAction action = // create your action here byte[] actionBytes = // convert action to bytes GameDataAction wrap = new GameDataAction(-1, tableId); wrap.setData(ByteBuffer.wrap(actionBytes)); router.dispatchToGame(tableId, wrap); }
Which should be more or less self-explainable. The action is of course whatever type of object and encoding you use in your game, it could be standard Java objects and Serialization for example.
Table Play / Closing
The table should work as usual, the only thing we’ll add is to save the game state on the “close table command”. We need to translate the action byte data to an object, then differ between internal actions and client actions and process. Something like this perhaps (again from my Kalaha game):
public void handle(GameDataAction action, Table table) { Object act = // translate to action object log.debug("Got action: " + act); if(act instanceof KalahaAction) { // here's where you'll handle the actual game state } if(act instanceof CloseTableAction) { setTableClosedAttribute(table); saveTableStateToDb(table); } else { log.warn("Unknown action: " + act); } }
In the above all kalaha actions are treated separately as client actions and the “close table” action simple sets the table state attribute to “CLOSED” to mark for the activator that the table is safe to remove, and then saves the game state to database.
Conclusion
As you can see, the actual code to manage asynchronous games in Firebase is minimal, you’re going to spend infinitely more time on game logic than state handling. All the code I’ve omitted is trivial. In fact, writing this article took me longer than implementing the feature in my game!
Which ends our discussion about asynchronous games in Firebase. Last post we looked at motivation and background, and in this post we’ve seen how to actually program it in Firebase. Now go and try it yourself!
Sorry, the comment form is closed at this time.
eyelash grower
Thats a definitely fine article with required Data. Thanks for your help.