Building a multi-player Flappy Bird game for iOS

  • May 08, 2014
  • Mobile games

The Realtime Framework is a platform that facilitates the development and deployment of mobile and web apps using real-time communication features. The benefits of using the Realtime Framework include a high-performance, high-scalability server solution, easy and fast development of products and applications using real time, the ability to use the technology on several platforms and reduced bandwidth usage.

Realtime is for everyone — from the beginner to the advanced developer. Using your favorite programming language you'll be able to broadcast messages to millions of users, reliably and securely. It's all in the cloud so you don't need to manage servers.

The Realtime Framework is presented to you in two main flavors: Realtime Cloud Storage and Realtime Cloud Messaging.

Realtime Cloud Storage is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability. You can use Realtime Cloud Storage to create database tables that can store and retrieve any amount of data, and serve any level of request traffic.

Realtime Cloud Messaging uses the Pub/Sub (publish/subscribe) messaging pattern, where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers. Published messages are published to topic channels, without knowledge of what, if any, subscribers there may be. Similarly, subscribers express interest in one or more topic channels, and only receive messages that are of interest, without knowledge of what, if any, publishers there are.

The two services combined enable the development of multi-player mobile games, turning what would be difficult and even expensive into something easy and with controlled costs. Oh … and with no servers to manage, just client-side code.

To get started with the Realtime Framework you just need to register and get your free application key. Then just download the desired SDK and start building your awesome real-time application.

From Flappy Bird to Flying Brands: monetization in real-time

We all agree that Flappy Bird was a great game. At Realtime we thought it would be even greater if it allowed two players to play against each other, so we decided to give it a go.

In a couple of days part of our Realtime Framework team got together to use the Realtime SDKs along with the standard iOS libraries to build a multi-player Flappy Bird style game. In partnership with WebSpectator’s Ad Exchange Network we set the goal to integrate their real-time advertisement protocol into the game, in a way where instead of the “bird”, players would see known brands. These known brands would monetize the game by paying their 20 seconds GTS’s (guaranteed time slots, see more GTS at http://webspectator.com). It’s not intrusive advertisement and in the end it added a lot of coolness to the game (personally we love to play with OREO cookie or the Red Bull can). And Flying Brands was born!

For the physics part of the game we’ve just used the native iOS Sprite Kit library and nathanborror’s FlapFlap project (https://github.com/nathanborror/FlapFlap).

The multi-player game workflow

Flying Brands is a peer-to-peer game, where two players play the same flappy game in-sync. Each one controls its own brand and sees in real-time how the other player is playing.

To start the game one player challenges the other player, which has the opportunity to accept or deny.

Push Notifications are used to alert an offline player that a challenge is on.

As soon as the challenge is accepted the game begins. The first player to hit a pipe loses and the current score is added to the winner’s global score.

As simple as that!

Handling variable latency

For this example we decided to follow the approach where each player only sends the taps to the other player, not the actual positions of the brand logo. These taps will be “rendered” on the other’s player device using the same physics methods that the local player is using. We just had to come up with a way to keep things in-sync.

This seems easy but since there’s always some variable latency in each message (we’re using TCP/IP and the internet), this latency even though it can be really small (less than 50ms in some cases) would easily allow the game to be out-of-sync. A simple 1ms delay on a tap is enough to allow the “bird” to collide with a pipe on the remote device when that never happened at the local device. Not nice.

To solve this issue we’ve decided to use a time-relative algorithm, where the local player actions are performed at the remote player device in the same exact order and with the same time delta between them, but delayed with a fixed amount of time, large enough to accommodate the latency variations.

In our case and since Realtime is a low latency system we’ve decided to use one second as the fixed amount of time. To make this work the challenged player starts its game one second after the challenger opponent, meaning that we’ll allow up to one second of latency for each message (it’s more than enough with the Realtime platform, even if a player is using 3G or 4G networks).

With this algorithm in place each player experiences the other player actions exactly one second after they occurred in the remote device (independently from the communication latency) but with the same cadence. Placing the remote player exactly one second behind the local player makes it right for each player (this is easy to calculate since the velocity of the pipes is constant).

To implement this algorithm, both players exchange their timestamps in the beginning of the game and send their current timestamp with each tap. Upon reception of a new tap message each local player calculates the remote action time delta (to understand at what time relative to the start of the game the action occurred) and schedules it to happen exactly at that time (the message latency doesn’t matter since it’s the timestamp in the message payload that is used for the time relative calculation, not the message reception timestamp that’s affected by the network latency).

		                    - (void) scheduleTap {
							    if(player2Taps.count > 0){
							        NSDictionary* tap = [player2Taps objectAtIndex:0];
							        long long tapTime = [[tap objectForKey:@"time"] longLongValue];
							        long long deltaTime = tapTime - [GameData currentGame].opponentStartTime;
							        long long finalMoment = [GameData currentGame].localStartTime + deltaTime + MAX_LATENCY;
							        long long currentDate = [GameData getCurrentDate];
							        long long deltaTimeFinal = finalMoment - currentDate;
							        float timerTime = ((float)deltaTimeFinal)/1000;
							        
							        [NSTimer scheduledTimerWithTimeInterval:timerTime target:self selector:@selector(executeRemoteTap:) userInfo:tap repeats:NO];
							    }
							}
						
		                    - (void) executeRemoteTap:(NSTimer*) timer {
								    [player2Taps removeObjectAtIndex:0];
								    
								    Player* player = [players objectAtIndex:1];
								    
								    NSDictionary *tap = [[NSMutableDictionary alloc] initWithDictionary:[timer userInfo]];
								    
								    float x =  self.size.width/2 - playersDistance;
								    float y = [[tap objectForKey:@"y"] floatValue];
								    CGPoint position = CGPointMake(x, y);
								    [player setPosition:position];
								    
								    [self tapPlayer:[players objectAtIndex:1]];
								    
								    if(player2Taps.count > 1){
								        NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];
								        NSArray *sortedArray = [player2Taps sortedArrayUsingDescriptors:[NSArray arrayWithObject:sort]];
								        
								        [player2Taps removeAllObjects];
								        for (NSDictionary *dic in sortedArray) {
								            [player2Taps addObject:dic];
								        }
								    }
								    
								    [self scheduleTap];
							}
						

Since the other player is one second behind in the screen when the scheduled tap event occurs the “bird” will be exactly at the same position as it was in the opponent game when the tap occurred, causing the exact same effect on the physics engine of the remote player.

In case anything goes south we also send the “bird” (x,y) position with the tap message so the remote bird position could be corrected in case of failure.

Confused? If you’re a developer it’s probably easier to understand what is going on looking at the code, right?

Persisting the game data

For persistence of player data, scores and challenges workflow, we’ve decided to use the Realtime Cloud Storage NoSQL database and its real-time notifications.

The usage for scores it’s pretty straightforward, we just keep a table indexed by the playerID and a numeric attribute holds the current score. At the end of each game we just add the game score to the winner’s current score.

		                    - (void) incrementPlayerScore:(NSDictionary *) playerScore WhitScore:(NSNumber *) score OnCompletion:(void (^)(BOOL finished)) completion {

								NSNumber *totalScore = [NSNumber numberWithInt:([[playerScore objectForKey:SK_SCORES] intValue] + [score intValue])];
								ItemRef *itemRef = [[_storageRef table:TAB_SCORES] item:[playerScore objectForKey:PK_SCORES] secondaryKey:[playerScore objectForKey:SK_SCORES]];
								
								[itemRef del:^(ItemSnapshot *success) {
									
									NSDictionary *newScore = [NSDictionary dictionaryWithObjectsAndKeys:
										  [playerScore objectForKey:PK_SCORES], PK_SCORES,
										  totalScore, SK_SCORES,
										  [playerScore objectForKey:PK_PLAYERS], @"nickName",
										  [playerScore objectForKey:SK_STATUS], @"gameId", nil];
									
									[[_storageRef table:TAB_SCORES] push:newScore
										 success:^(ItemSnapshot *item) {
											 ItemRef *itemRef = [[_storageRef table:TAB_STATUS] item:[playerScore objectForKey:PK_STATUS] 
											 secondaryKey:[playerScore objectForKey:SK_STATUS]];
											 
											 [itemRef incr:@"score" withValue:[score intValue] success:^(ItemSnapshot *success) {					
												 completion(YES);
											 } error:^(NSError *error) {
												 //NSLog(@"Error INCREMENTING SCORE on TAB_STATUS\nERROR: %@", [error description]);
											 }];
										 }
										 
									   error:^(NSError *error) {
										   //NSLog(@"Error Writing item\nERROR: %@", [error description]);
										   completion(NO);
									   }];
									   
								} error:^(NSError *error) {
									//NSLog(@"Error DELETING SCORE\nERROR: %@", [error description]);
								}];
							}
						

For the challenges workflow we used the real-time notifications of Cloud Storage to keep player in-sync. Users subscribe their playerID key from the challenges table (to get notified of new challenges from other users) and they push an item to the same table with their opponent’s playerID when they want to challenge them (this will trigger the notification at the opponent that we’ve mentioned before).

		                    - (void) writeChallenge:(NSDictionary *) challenge OnCompletion:(void (^)(BOOL finished)) completion {

								TableRef *tableRef = [_storageRef table:TAB_CHALLENGES];
								ItemRef *chItem = [tableRef item:[challenge objectForKey:PK_CHALLENGES] secondaryKey:[challenge objectForKey:SK_CHALLENGES]];
								
								[chItem get:^(ItemSnapshot *item) {
									if ([[item val] objectForKey:PK_CHALLENGES] != nil) {
										completion(NO);
									}
									else {
										//NSLog(@"chItem NIL");
										[tableRef push:challenge success:^(ItemSnapshot *success) {
											//NSLog(@"\nITEM:\n%@ WRITEN", [success val]);
											completion(YES);
											
										} error:^(NSError *error) {
											//NSLog(@"Error WRITING CH on TAB_CHALLENGES writeChallenge\nERROR: %@", [error description]);
											completion(NO);
										}];
									}
								} error:^(NSError *error) {
									//NSLog(@"Error Getting Item TAB_CHALLENGES writeChallenge\nERROR: %@", [error description]);
								}];
							}
						

For Push Notifications we’ve just used the automatic integration of Cloud Storage with APNS. When users are challenged and their phones are locked, instead of receiving a Cloud Storage real-time message with the challenge through the Realtime TCP socket, they’ll receive an APNS Push Notification alerting them that someone is challenging them.

If they respond to the notification and open the app, the app will read the current challenges from the Cloud Storage table and get all the data about the user opponents, allowing them to accept or quit.

Since Realtime Cloud Storage is highly-scalable it doesn’t matter if we have 2 players or 2 million, data will be sharded by playerID across as many database servers as necessary to keep things going smoothly.

Feeling inspired to code your own game?

We hope this guide helps you understand the main parts of the multi-player Flappy Bird style game development process. Since the techniques described here can be applied to any real-time multi-player game we hope to have inspired you to code your own game.

You’ll be able to find the complete source in GitHub at the following repository: https://github.com/realtime-framework/multiplayer-flappy-bird

Just get your own Realtime free license and replace the application key.

Have fun and don’t get the Flappy addiction!

If you find this interesting please share: