Yesterday @GavinBowman asked me about my experience using Game Center for the multiplayer mode in Swivel.
So here is a brain dump of my thoughts on the subject. I apologize in advance for the somewhat rambling nature of it. If you have any specific questions beyond what I cover, feel free to ask in the comments. I won't go into any code but if there is interest it can be the subject of a future post.
Four player multiplayer is a very compelling feature for a puzzle game. It's not something you see very often, but it is actually not nearly as difficult to create as one might think. Game Center / GameKit makes it really easy to take care of the boilerplate — setting the number of players, sending invitations, random matching, etc. All you have to do is launch the Matchmaker and wait until it gets back to you with a match. There are a few minor gotchas like needing to wait for all the players to connect before starting but the process is simple overall:
- Player taps multiplayer button
- Launch built-in Game Center matchmaker
- Get back a match (or cancel)
- Create a game using the match data, and hook up delegates/callbacks
- Wait for all players to connect
- Start and run the game
- End the game when someone wins (or everyone else loses)
If you've used the old Peer Picker then the process will be mostly familiar to you. Of course the hard part is what goes on during the game. So let's talk about that.
Swivel multiplayer works a lot like the single player except that for every match a number of blocks is sent to one of your opponents. The number and type of blocks depends on how big the match is, the combo multiplier, etc. The game also progressively gets harder, but unlike the single player mode there are no explicit level changes.
I decided early on that perfect synchronization was not necessary. As long as the game starts roughly at the same time for everyone and things progress at the same rate, the illusion of synchronization will be kept and play testing proved this out. As an indie developer, you've got to pick your battles. Keeping players perfectly synchronized is a tough problem and even if I had gone to all the trouble chances are most players would never notice a difference. For a small puzzle game… eh… not worth it.
Actually, had I tried to synchronize perfectly I might very well have made the game unplayable over the phone's wireless data connection (WWAN). It turns out WWAN is extremely unreliable. It's not just that players drop out unexpectedly but, frustratingly, it tends to buffer up packets and then send them in one giant burst. In Swivel this means that instead of the constant flow of bricks from other players, you might see a trickle for 30 seconds and then get slammed with a screenful. Because I knew users would be annoyed by having multiplayer disabled without Wi-Fi I kept it enabled, but I do present them with a warning message when they launch multiplayer on a WWAN connection recommending they switch to Wi-Fi if available.
Realize and accept that, unless your game is turn based, Wi-Fi is going to be the only data connection on which the game has a chance of working well. Honestly, I might have gone with the Peer Picker and supported bluetooth and Wi-Fi connections only if it had four player matchmaking out of the box.
I also discovered that GameKit is not perfectly reliable about telling you when a player drops out. So I make sure to keep track of when I last heard from a player and drop them after some period of idleness.
Another decision I made early on was not to transmit full game state or touch events to each player. Not performing game logic for each player is often considered a big no-no. Yes, it does introduce the potential for cheating via packet forging but honestly I'm just not worried about someone going to all that trouble to cheat at my little iPhone puzzler. Again, pick your battles. View your game realistically. Were I writing a bigger budget game I might well have done differently.
Instead I only transmit a snapshot of their grid. I don't include animation information or anything else. It's just an array of block types with 0 meaning no block. In Swivel, each player sees a small view of their opponents' grid at the top of the screen. But that grid isn't smoothly animated — and it doesn't need to be. Typically you only glance at it occasionally to see how you're doing. I experimented with how often to transmit that state and ended up with about once every half a second.
I added a second visual indicator of how other players are doing in the form of lines behind the player grid depicting how high opponents' grids are. Though that information may only come in every so often, I make sure to animate smoothly between levels to help maintain an illusion of smooth synchronization.
The illusion of synchronization is important, and simply using visual cues to maintain that illusion can get you a lot of mileage.
With GameKit you can send data reliably or unreliably. Reliable sending can clog up your pipeline because packets are guaranteed to be in order. Unreliable sending won't do that and tends to be faster, but sometimes the data just won't get there. Choosing which bits to send which way is important. I send the grid state unreliably. I can get away with that because the grid state has no direct bearing on the opponent — it simply serves as a visual — and if one gets lost, another update is right behind it.
Game commands must get through. I depend on them. They include things like attacking blocks sent from other players and “oh noes, I gameovered!” notifications. I send those reliably.
Another simplification is I opted to have no host. It's a two-edged sword really. On one hand the logic is simpler and if one player drops the game can go on without trying to renegotiate a host. On the other hand there is no-one to be authoritative. For Swivel it didn't matter too much. The win condition is essentially “last man standing” and is determined by each player for themselves. Yes, that does mean a dropped player sees themself winning, but from their perspective everyone else dropped out and I'm okay with that.
All in all, it took me only a two or three days to get the multiplayer mode operational. Then it took another two or three weeks to get the balancing right and iron out some bugs.
The balancing turned out to be the hardest part by far. It really is important to get this right because the level of fun a player experiences is directly dependent on it.
Originally, my aim was to have the average multiplayer game take no more than 2 or 3 minutes. I had not considered a minimum time though. At one point the game seemed really playable but my wife brought up the point that it was very demoralizing to lose in 30 seconds. I received similar feedback from other testers so I made it another goal to have the game last at least a minute unless there was a huge skill mismatch. Getting that right was difficult and took a lot of tweaking and testing.
Remember that the game should still be fun for losing players. Even when there is a reasonable skill mismatch you still want victory to feel within reach so that they'll want to play again. This is an important principle for single player games — don't disregard it for multiplayer! Additionally, crushing victories become stale quickly for winning players. If you make them work for their win they'll derive greater satisfaction from it.
One balancing problem I had early on was that each player's attacks went to all other players. This turned out to make 4 player chaotic while 2 player games were just right. To solve this I had each player's attacks go to other active players in round-robin fashion — a much easier solution than trying to balance the game individually for 2, 3 and 4 players and have to rebalance if a player drops or loses.
In the end, I'm very happy with how the multiplayer turned out. It is a lot of fun to play and really was not all that much trouble to add.
The takeaway for me is that, although there are a lot of “right” ways out there to do multiplayer and plenty of best practices, I broke pretty much all of them and everything turned out just fine. This is in part because Swivel is a puzzle game, in part due to design, and in part because I decided it was okay to cut corners on this one. I am usually somewhat of a perfectionist so this was a tough decision to make.
Regrets? I should have spent more time thinking through the multiplayer implementation early on. The file containing most of the multiplayer code is some of the messiest in my project. I'm sure it is in no small part due to being written last and not really getting all the refactoring the rest of my code did, but really it could have just been better architected from the start. It gets cluttered fast!
I'm glad I got my feet wet on Swivel though. My next title will likely be more action oriented and multiplayer will be a real challenge.
If you've been on the fence about doing multiplayer, I suggest you take a snapshot of your project and try to hack in some multiplayer over the course of a few days. Just see how it goes. I can tell you this: it is hugely satisfying when you get it working!
This post is part of iDevBlogADay, a group of blogs by indie iPhone developers featuring two posts per day. You can subscribe to iDevBlogADay through RSS or follow the #iDevBlogADay hash tag or @idevblogaday on Twitter.