Juggling Cloud Data On and Offline
So, here's the problem. You're making a game where you have your players able to collect Coins (or whatever), and you want to enable an option for them to submit their Coin Count online to share with their friends, or to be able to play from anywhere and pick up from where they left off.
Sounds easy enough, but you also want to make sure they can play offline when they want and although the counts may not match temporarily, when they do go online again, everything will jive - they won't lose any of their coins.
Add in the problem where the player can start a new game on another computer (with no local data) and after collecting some coins remembers to go online and would expect (logically), that their newly earned coins should be added to their online total.
To make matters worse, in-game, they can spend their coins to earn rewards which means the coin count # will need to be able to adjust positively and negatively... which means, if they log in today and have x coins, then go offline and spend y coins, the server needs to know how to update to have x-y coins.
This is the exact problem I was looking at recently, and I came up with a simple, and pretty effective solution.
Sure, you could try to track a log or history of the coins as they are earned and spent, and just keep a running total using all that data, but that would complicate things a lot more... and take a lot of work to set up.
For this solution, we're going to assume that the Server-side code is predetermined and really simple: you can Send the data - which simply replaces the currently stored value with the new value, and you can Load the data which just returns the currently stored value.
So if the server shows 100 coins. If you call Load, you get 100, if you call Send 120, the server will now have 120, and if you call Load again, you get 120. Simple.
The trick here is that we need to know which location has the most recent version of the data.
The local version of the saved data will need to have 3 things: the coin count, the coin count the last time we sent to the server, and a simple flag to store the Saved State.
Now, the Saved State Flag will have 3 possible values:
NEW = -1
UNSENT = 0
SENT = 1
We're going to update that value depending on what happens with the data.
When a brand new game is first loaded - the first time on this machine, or maybe they cleared their data or something - the State will be set to NEW. Your Coin Count will also be 0.
Now the player can either just start playing, or they can Log In.
If they just start playing, each time we flush out their coin count to the disk (however you want to do that - each time they pick up a coin, or when they beat a level, or whatever), we're going to see that they are not online, so we're not even going to try to update the server, and we're going to leave the State as NEW.
Now lets say they played for awhile, and they quit the game. They come back to it later, and the game loads their data from the disk immediately. It's going to load their coin-count (say... 50 coins), and the state which is still NEW.
Now the player decides to log in for the first time - they registered or whatever and now they put in their credentials for the first time. We connect to the server and do all that stuff, and then we're going to call our SyncWithServer routine.
This routine is going to first grab the value from the server, check to see what our State is, and then depending on the State, do something different.
So in this scenario:
Server.CoinCount = 0
Local.CoinCount = 50
State = NEW
Because the State = NEW, we're going to COMBINE the two counts. We're going to do:
Local.CoinCount += Server.CoinCount
Now, because we have non-NEW data that has not been flushed to the server yet, we set our State to UNSENT and then we call our Send routine to update the server with the current value (50), and after that succeeds, we set our State to SENT.
So far, our logic would look something like this:
function SyncWithServer()
Server.CoinCount = Load()
if State == NEW
{
Local.CoinCount += Server.CoinCount
State = UNSENT
if Send(Local.CoinCount)
State = SENT
}
Once the player is logged in and this Sync is done, we don't need to check the numbers again during this session - they play and collect more coins, spend coins, whatever, and we'll just do something like this:
function FlushCoinCount()
Local.CoinCount = CoinCount
if State != NEW State = UNSENT
if OnLine
{
if Send(Local.CoinCoint)
State = SENT
}
So when they are finally done playing for the day, lets say they have 100 coins, things will look like this:
Server.CoinCount = 100
Local.CoinCount = 100
State = SENT
Now, the next time they bring up the game, lets say they log in right away. As soon as the game starts, it will grab the local data, and then we do the SyncWithServer Routine - but this time State = SENT and not NEW, so we need to do something different. See - for all we know at this point in time, when the player stopped playing on this computer he had 100 coins, and we had sent that # to the server. But we have NO IDEA if he played on another computer and logged in and sent a different number - so we're pretty much going to throw away our Local Coin Count, and just use what the server has. So our Sync Function would look something like this:
function SyncWithServer()
Server.CoinCount = Load()
if State == NEW
{
Local.CoinCount += Server.CoinCount
State = UNSENT
if Send(Local.CoinCount)
State = SENT
}
else if State = SENT
{
Local.CoinCount = Server.CoinCount
}
That's easy enough - we're basically saying once the SENT state is sent, that we're relinquishing responsibility to the Server.
But we've got one more State value: UNSENT. This is the tricky one. This situation would occur if the player logged in and played and SENT their data, and then played again later on without logging in, and then eventually logged back in. We really wouldn't know what value 'rules', which is where our LastSentValue variable comes into play.
Anytime we update our value to the server and we set our State to SENT, we want to record our Coin Value at that time - so if later on, we find the UNSENT state when we're getting data from the Server, we're going to adjust the value coming from the server by the difference between the Local coin count and the Last Coin Count.
Here's what both of our pseudo functions will look like:
function SyncWithServer()
Server.CoinCount = Load()
if State == NEW
{
Local.CoinCount += Server.CoinCount
State = UNSENT
if Send(Local.CoinCount)
{
State = SENT
LastCoinCount = Local.CoinCount
}
}
else if State = SENT
{
Local.CoinCount = Server.CoinCount
LastCoinCount = Local.CoinCount
}
else if State = UNSENT
{
LocalCoinCount = Server.CoinCount + (LastCoinCount - Local.CoinCount)
if Send(Local.CoinCount)
{
State = SENT
LastCoinCount = Local.CoinCount
}
}
function FlushCoinCount()
Local.CoinCount = CoinCount
if State != NEW State = UNSENT
if OnLine
{
if Send(Local.CoinCoint)
{
State = SENT
LastCoinCount = Local.CoinCount
}
}
And there you have it! No matter what your user does - play on different computers, forget to login, wipe their local data, etc - once they finally do log in, everything should be up to date with each other!
Written by Tim I Hely
Related protips
4 Responses
lets say i have 100 coins synced on the server and two pcs with status 'sent'. then i go offline on both pcs and buy items worth 100 coins on both. then i go online again. will i have 0 coins and items worth 200 coins?
@pvinis I would require the user to be online to buy items. They should have to authenticate with the server before purchasing anything, just like a real credit card.
@pvinis - Trying to wrap my head around this again, but I think you could end up with that situation... I don't know of a good way to defend against that unless you force the user to be online to buy items, like @gagege said, but the whole idea behind this is to not force the user to be online and to be as transparent as possible...
i guess that its either you have to be online to purchase an item, or maybe you could suggest to the player to connect to the server before trying to play the game on another computer. this way the player can play offline, progress on the game, make purchases etc, and when he comes back from the trip that he had no internet access, he can go online with that computer so that the data is uploaded, before he plays again on another computer. does this make sense?