Play a game on a device, put it down, pick up another device, and continue playing exactly where you left off. This is the future of games.
That future is a reality today for some games and apps (Netflix, Kindle), and I’m convinced that players will expect that in most games in the next year or so. So obviously, the next bit of new iOS tech I decided to try was iCloud. I would love to turn Flower Garden into that kind of seamless experience, independently of the device you use to access it.
As a quick spoiler, it turns out I won’t be able to make Flower Garden quite so seamless without a lot of extra work. But I learned a lot along the way and I should be able to take a small step in that direction.
iCloud promises to be the “available from everywhere” storage solution that will be a key component towards the scenario of playing on any device at any time. Unfortunately, it’s just a component of that whole scenario and still requires quite a bit of work on the part of the developer.
Since I’m just retrofitting Flower Garden to work with iCloud, I wanted a simple way to simply replicate the game state on iCloud. Then, whenever you play it from any device, you get the latest state and everything works as expected.
The big problem with that approach is that you can’t always count on having an active (or fast enough) internet connection. iPod Touches, iPads, or even iPhone in a plane or a cell tower black spot will make it so your device has no way to communicate with iCloud. So what happens if the player plays without an internet connection, then comes home, grabs another device, plays for a bit with internet connection, and finally launches the original device, this time with internet connection. You’ll end up with two conflicting game states. Not fun!
I can think of two different strategies to deal with this situation:
- Require internet connection to play. Some games do this today. It can be quite frustrating not being able to play your favorite game in the plane, but it does solve the game state conflict problem because they can always communicate with iCloud, so they usually provide seamless multi-device play.
- Detect changes on both game states and intelligently solve them. Sounds good in theory, but it’s a pain in practice. Unless you have a very simple game state (a percentage completion for example), it means not only do you have to record the state, but you have to record the events that led to that state, and be ready to examine them, compare them, and marge them in the case of a conflict. And the worst part is that even if you put lots of care into it, there will be cases where the merge is not ideal and the player will feel like he lost something in the merge.
For Flower Garden, neither solution is particularly attractive. It’s the proverbial rock and a hard place. So for the moment I’ve decided to implement just a small step in that direction: Sync things that only progress in one direction, without any danger of conflict. That includes purchased IAPs and unlocked seeds. Later on, once that’s working solidly without any problems, I can consider adding counts like Fertilizer, Color Dust, or Green Thumb points. That will be a bit trickier because there’s still chance for conflict since the count can go up and down, but it’s possible to change each amount into two different values that always go up: Earned amount, and spent amount. That way I can always grab the maximum value that I find either local or on iCloud and things should work smoothly. (Update: What I wrote there about separating them in two different values is totally wrong, and any DB programmer worth his salt would be laughing at that. Separating them in two different values won’t help at all. That’s what I get for doing brain-dump posts on the fly 🙂
Apple provides two APIs to store data in iCloud: key-value storage and file storage.
Key Value storage
Don’t be fooled into thinking you need to store data in strings or one field at the time. You can stuff whatever binary chunk of data you want with NSData, so it’s quite versatile.
It does have three pretty big drawbacks thought:
- Size. You can only store up to 64KB of data. That’s definitely a big deal if you’re planning on storing large data files. Even for Flower Garden, each pot is about 30KB, so I wouldn’t be able to save the full garden state this way.
- Syncing. The Apple docs say that “Keys and values are transferred to and from iCloud at periodic intervals.” Ouch! What does that mean? Clearly this is intended for non-crucial data (like settings), so the potential delay isn’t a big deal. In my tests, I found that data was often not available when starting the app, but would become available a few seconds later.
- No way to check state. When starting the app, there’s no way to find out if the values you’re reading are up to date. Combined with the slow syncing, it makes it less than idea for important data. On the flip side, you can be notified when the data changes, but that means having to deal with changes while the game is running (which I was hoping not to do).
File storage might be a better option then since it doesn’t have any of those drawbacks: there’s no size limit, data is synced much more aggressively, and you can check if the data is up-to-date, and wait until it is otherwise.
What’s not to like about file storage then? The cumbersome and intrusive API that Apple created around it. If you read the docs, they make it sound like you need to inherit from UIDocument and load all your data through that class. I like to keep things simple and portable, so I’d really rather not introduce UIDocument into the file-loading process if I don’t have to.
It turns out that Apple’s docs on the iCloud SDK are quite sparse and lacking details. I was able to put together a demo from those docs and some experimentation, so hopefully this will be useful to other devs as well.
The device has an iCloud storage daemon running, monitoring specific directories for changes. You get the directory assigned to your app by calling URLForUbiquityContainerIdentifier.
To add a new file, the docs recommend first creating the file somewhere else, and then calling NSFileManager:setUbiquitous:itemAtURL:destinationURL:error: to move it to iCloud. Interestingly, I accidentally skipped this step and just wrote to the iCloud directory directly, and the files were stored correctly anyway. Might as well leave it just in case that behavior changes later on, or there’s some side effect I didn’t notice.
After that, every time you write to the file, the iCloud daemon will detect the changes and push them out to iCloud. This is an important point because a) You don’t have explicit control to say “start send it now” or even “This file is ready to be sent to iCloud” (that would be my choice), and b) I don’t know what it does with partial updates, so I would be very careful about writing to those files and make sure it’s an atomic operation (save somewhere else, and them move the file in one operation).
To get the latest version of the files on iCloud, you can check whether they’re fully downloaded or not by calling NSURL:getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:. If they are up to date, you can move on, otherwise, you can initiate a download of the files by calling NSFileManager:startDownloadingUbiquitousItemAtURL:error:.
One weird thing is that the startDownloading function doesn’t have a callback (that I can see). So I had to set up a timer to check periodically if the files are synced.
Since all the syncing happens at startup, there’s no complexity involved with data changing while the app is running. If that’s a case you need to handle, you might be better off using UIDocument since it at least detects conflicts. If you do it this way, the latest one will overwrite any past changes.
Also, working this way, it seemed easier to keep the files in Application Support (or Documents), and only move them to/from iCloud when I wanted to. That has the advantage of iCloud not changing things from under you while the game is running, and the fact that, if for some reason the files in iCloud are corrupt, you always have good local files to fall back to.
The demo iCloudTest saves data both with the key value pair, and the file storage system directly like I described above. If you run it from different devices, you’ll see that it’s amazing how quickly file data gets propagated, but key-value data takes a few more seconds.
The demo project also works in the simulator (there’s no iCloud support, so it just uses local files), and it even works with iOS4 (also using local files, and it avoids using any iOS5 symbols while detecting iCloud support).
Sharing Data Between Multiple Apps
Once you’re at this point, sharing data between multiple apps is really straightforward.
First you need to make sure the app IDs of both apps start with the seam Team ID. For some unknown reason lost in the midsts of time, Flower Garden and Flower Garden Free use different prefixes, so that option is out for me. Hopefully most people having been using the Team ID only.
Then you need to decide which type of data you want to share. If you want to share files, you need to add the app ID of the other app to your iCloud entitlement, and then as for the correct app ID (including prefix!) when you call URLForUbiquityContainerIdentifier. If you just want the app’s own iCloud data, you can pass NULL as the parameter.
As far as I can tell, each application can only have one set of key-value data. So sharing it between two apps means changing one app to use the app ID of the other app in the com.snappytouch.icloudtest field of the iCloud entitlements file. As long as both apps use the same Team ID prefix, it should work fine without having to do anything else.