I'm in the process of refactoring some code for Go Home Dinosaurs and I've run into an interesting problem.
Because GHD was originally designed to be a microtransaction based game, and because we wanted to have (essentially) cloud saves, it's using web services to keep track of player progress, coin totals, purchases, inventory, etc. Now, we want portions of this to be pushed into local save data. Because the schedule on GHD was so tight, there's a ton of code that just assumes that the server will be there, and it also (correctly) assumes that we want to update player information incrementally on the server. It's therefore filled with calls like this:
GameDataServer.SendServerMessage("updateField_request", data); // in some other portion of code void HandleMessage(Message msg) { switch(msg.Type) { case "updateField_response": // actually update the field with the new value field = msg.Data.NewValue; break; } }
The problem is that this doesn't really fit with an offline mode. In order to combat this, one of my colleges wrote a "fake response" method, which, if there's no server to talk to, queues a fake "success" message for every call. While this was certainly the fastest, most straight forward way to accomplish an "offline" mode quickly, I'm not a huge fan. It's too coupled to the thought that there's a server between us and the persistence store. It also means that the server's responses and the "fake" responses need to be kept in sync, and I'm never a fan of "if you change this, make sure to change this" code. It's way too likely to break.
But how to combat this? I'm not sure I have an answer for this, given the current design. All I can do is outline what might work, and what I want.
Generally what I want is transparent persistence. I don't what to have to understand *how* the platform wants to persist things. I want to edit my data and then say "persist" and have the platform figure out the best way to perform this.
The first step here, I think, is to take a lesson from MMOs, and ignore trying to cache unconfirmed changes. Most MMOs make the change locally and send a message to the server, requesting that the change be made. For persistence, successful calls are generally ignored, because we know that what the server says and what the client says are now in sync (or are in sync from when we sent the message). It's only in the case of an error that things have to be handled. Generally, the correct response is then to rollback whatever transaction was made on the client, and hope the player doesn't notice (or present an error message). Since the server can (and should) return it's version of the variable in the error response, it means that you can remove a lot of special case caching code that can easily get mucked up (or start performing double duty, which is the case with at least one of our caching variables). It also means that platforms that always succeed in changing data don't have to send fake "success" messages. Only the (hopefully rare) errors in server calls need to be handled.
Second step is to start keeping track of "dirty" fields, and updating them as needed (whenever persist gets called). However, our current server code (which I didn't write, and some of which I'm trying to keep intact for a bit) assumes (again, correctly) updates for every single transaction. It's hard to say "the player has x coins." Instead, it asks to told how many coins the player gained. You can't send the entire inventory, you have to send "I'm adding / removing the following item from the inventory." Or "I'm purchasing the following item and it should be added to the inventory." These are common client – server interaction things, even in MMOs because it means that the server can actually confirm that the logic you're performing matches what it expects, but it does make doing transparent persistence very difficult.
The only way I can think to do this even remotely well is to push each update to the player to a platform intermediary. Platforms that persist the data locally ignore incremental updates and just save all the data once a full "persist" call is made. Platforms that persist the data remotely do the opposite, pushing each update to the server, rolling back errors, and ignoring requests for full persistence, or sending full persistence requests and to confirm that the local and remote versions are still in sync.