http://framework.realtime.co/messaging

Building a multi-player mobile game with Realtime Framework

Taking into account the emerging market for mobile apps, the need to consider the development of software for this market grows every day. With this trend in mind Realtime is delighted to present the new Realtime Framework mobile app example, a real-time multi-player iOS game.

Summary

The Realtime Framework is a set of tools and technologies that facilitate the development and deployment of software, applications, websites using real time. 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 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 multi-player game

The Realtime Angry Admin is a game developed with the purpose of demonstrating that you can use Realtime Framework to develop real-time collaborative games.

In this game we use both Realtime Framework services: the Realtime Cloud Storage for data persistence, for example the game's scores, and Realtime Cloud Messaging, for syncing the game in real-time between players. Mobile Push Notifications are used to notify users of new game challenges by other players.

Before you dive into your preferred SDK please take a few minutes to read the starting guides. For your convenience they are divided into small chapters that will guide you through the main concepts and best practices for optimal use of Realtime Framework.

Storage Starting Guide and Documentation

Messaging Starting Guide and Documentation

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.

We also use Cocos2D (cocos2d-iphone is a framework for building 2D games) and Sprite Builder

This tutorial will walk you through the process of creating a simple game for your iPhone or iPad using Realtime Framework and Cocos2D.


Our main goal is to demonstrate how everyone can build a multi-player game without the need to deploy servers, only by using the Realtime Framework cloud services.

For this example we thought it would be interesting to build a game that would be a good example for most game developers. So we decided to use a framework for building games with a physics engine. With this guidelines our first step was to follow the example set by Sprite Builder. The game was set to have two players, they shoot at a rock and the first to achieve the stone falling on the opponent cannon becomes the winner.

You play by starting the game in two possible ways: by inviting someone to play with you or being invited by other player. Each player has a game Id which is used for identifying the game and player itself. If you enter someone else's game id, you will invite that player to play with you.

The other player will be notified through a mobile push notification that an opponent wants to challenge him. The other way is by starting the game and waiting some player to join your game id (naturally you'll have to let your friends know your game Id).

First approach to sync the game

The initial idea was to create a game in which players had the same experience, that is, a synchronized game. The game would be synchronized by messages sent between players. In a first approach both players would have a physics engine, only actions like shooting and game over collisions would be synchronized. The idea was to let the physics engine do the rest.

Taking this into account only the shoots of the opponent of each player were synchronized. For example if player on right side shoots, he will send a message that he is shooting, the player on the left will handle a shooting message from the player on the right side, and simulate it on his own game, and vice-versa.

This works, and very nicely. But we end up on a situation that was not desirable. As you know, physics has many variables that matter, like gravity, collisions, friction among other factors. Thereby sometimes the ending result was not a fully synchronized game, the objects on the scene didn't draw the same way on both games (devices). Not good.

Sync all things!

The second and definitely approach, focused on the goal of a fully synchronized game. So it was decided that only one player will be responsible for the physics engine and the game will entirely synchronized through messages. This way the player on the left side plays with physics and sends the objects positions, the player on the right handles the messages and redraws the scene accordingly. This way we can guarantee that in both games the same action is happening.

Realtime Cloud Messaging uses the Pub/Sub (publish/subscribe) messaging pattern. The game's Id will be the name of game's channel. Both players will subscribe the same channel and both, as subscribers, will receive the messages published in the channel. As we know the side of each player (left/right; with physics/without physics), each player will only handle the essential messages for his scenario.

The game will also have another messaging channel. Messaging has a great feature that allows to subscribe a channel with APNs push notifications. This allows to receive notifications even when the application isn't running, hence some messages will be sent through this second channel (in the form of: "GameID":Notifications, which is a sub channel of the main one) allowing to notify players that they have an invitation to play.

The development process

After setting up the game environment, with all the necessary objects, obstacles and physics engine, it's time to put Messaging to work.

To add the Realtime Framework SDK to your existing project, just add it like you do with any other framework or library. You may just drag and drop it to your project. Be aware that you will need to add others frameworks to your project in order to working with the SDK.

Since we will also work with Cloud Storage, it's sufficient to use the Cloud Storage SDK, since it includes the Cloud Messaging SDK (Storage will be discussed later).

In this example we have chosen to instantiate the messaging client in a singleton class, MessagingManager (also applies to Storage, StorageManager). This class must implement OrtcClientDelegate. Since the client is a property, it is instantiated and configured like this:

                    _messagingClient = [OrtcClient ortcClientWithConfig:self];
                    [_messagingClient setConnectionMetadata:@"CONNECTION_METADATA"];
					[_messagingClient setConnectionTimeout:10];
					
					if (ISCLUSTER) {
						[_messagingClient setClusterUrl:@"SERVER"];
					}
					else {
						[_messagingClient setUrl:@"SERVER"];
					}
				

After the messaging client is properly instantiated and configured, don't forget to connect it:

                    [_messagingClient connect:@"APP_KEY" authenticationToken:@"AUTH_TOKEN"];
                

When _messagingClient gets connected, OrtcClientDelegate method: - (void) onConnected:(OrtcClient*) ortc gets called. At this moment messaging client is ready to work, and you can subscribe a channel, like this:

                    [_messagingClient subscribe:[[StorageManager sharedManager] myGameID] subscribeOnReconnected:YES onMessage:onMessage];
                

It is also subscribed to the notifications channel:

                    NSString *myGameNotificationsChannel = [NSString stringWithFormat:@"%@:Notifications", [[StorageManager sharedManager] myGameID]];
					[_messagingClient subscribeWithNotifications:myGameNotificationsChannel subscribeOnReconnected:YES onMessage:onMessage];
                

Where onMessage is a block declaring the callback function for the received messages. In this particular case the block is an ivar defined previously like this:

                    id weakSelf = self;
					onMessage = ^(OrtcClient* ortc, NSString* channel, NSString* message) {
						[weakSelf receivedMSG:message onChannel:channel];
					};
                

That sets the method - (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel a callback for every incoming message on [[StorageManager sharedManager] myGameID] channel.

The messaging client will be instantiated when the application is launched. At this moment we also generate your game ID or read it, in case you already had one. Doing this the player will be able, as soon as he launches the app, to receive messages and notifications.

Now its time to send and receive messages.

Since it is not to the messaging client that messages matter, it is implemented a protocol MessagingDelegate which forwards the received messages to the game class, so each game class will be a MessagingDelegate, which also implement the method - (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel

On MessagingManager class implementation:

                	- (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel {
                		if (_msgDelegate && [_msgDelegate respondsToSelector:@selector(receivedMSG:onChannel:)]) {
                			[_msgDelegate receivedMSG:message onChannel:channel];
                		}
                	}
                

Sending a message is equally easy. We only have to call a singleton method:

                	[[MessagingManager sharedManager] sendMessage:msg ToChannel:[[StorageManager sharedManager] gameID]];
                

MessagingManager will be responsible to publish the messages to the channel:

                	- (void) sendMessage:(NSString *)msg ToChannel:(NSString *) channel {
                		[_messagingClient send:channel message:msg];
                	}
                

 

The game workflow

Let's go back to the game. It will be now explained how the messages and storage actions control the game flow. Don't forget each player receives the messages, but we may choose the messages the player wants to handle.

Let's assume that someone wants to invite you to play and your game id is "Abcd". The inviting player inserts your game id and press the "Join Game ID = Abcd" button. At this moment the inserted game id will be validated (will assume that is valid, exists, it's not his own, and there's no opponent for that game id) and the game scene will be loaded. Immediately afterwards the storage opponents table is updated for "Abcd" game id.

                	[[CCDirector sharedDirector] replaceScene:[CCBReader sceneWithNodeGraphFromFile:@"GameSceneNoPh"]]; // load the scene
                	[[StorageManager sharedManager] setGameIdOpponent:[[StorageManager sharedManager] gameID] OnTable:YES];
                

Meanwhile the other player will subscribe the game channel, set himself as MessagingDelegate (to be able to receive messages) and when the game scene is fully loaded he will send a message asking if you are ready to play:

					[[MessagingManager sharedManager] setMsgDelegate:self];
					[[MessagingManager sharedManager] subscribeChannelWithName:[[StorageManager sharedManager] gameID]];
                
                	[super sendMessage:[NSString stringWithFormat:@"ARUTHERE_%@", RIGHT_SIDE]];
                

on super:

                	- (void) sendMessage:(NSString *)msg {
						if ([msg isEqualToString:[NSString stringWithFormat:@"ARUTHERE_%@", RIGHT_SIDE]] || [msg isEqualToString:@"JOINRESTART"]) {
							NSString *notificationsGameID = [NSString stringWithFormat:@"%@:Notifications", [[StorageManager sharedManager] gameID]];
							[[MessagingManager sharedManager] sendMessage:msg ToChannel:notificationsGameID];
						}
						else {
							[[MessagingManager sharedManager] sendMessage:msg ToChannel:[[StorageManager sharedManager] gameID]];
						}
					}
				

On the other hand, there are several states in which you may be. The main ones:

  • * Firstly if you're not running your app you will receive a notification that someone is inviting you to play;

  • * If you're running your app on the main screen, you will receive a Storage notification. That will alert you that you have an opponent;

  • * If you're running your app on the game scene, you will receive the message, handle it and answer it;

You will always receive a message or a notification (depending if you are running your application or not), that an opponent is challenging you, once your application is always subscribing "Abcd" and "Abcd:Notifications" channels.

Regardless if you have an opponent, you will start your game by pressing "Start Game ID = Abcd". At this point, the scene will start loading, and when is finished will check if you have any opponent, if so, you will ask if opponent is ready to play by sending him a message. Is also set a "_waitingTimer", in case your opponent abandons the game and doesn't reply to your message.

                	[[StorageManager sharedManager] checkGameIdOpponent:[[StorageManager sharedManager] gameID] OnTableOnCompletion:^(BOOL completion) {
						// if completion block defined, call it
						if (completion) {
							[super sendMessage:[NSString stringWithFormat:@"ARUTHERE_%@", LEFT_SIDE]];
							_waitingTimer = [NSTimer scheduledTimerWithTimeInterval:60.0 target:self
																		   selector:@selector(cancelGame) userInfo:nil repeats:NO];
						}
					}];
				

Once your opponent is subscribing the channel and waiting for you, he will receive the message and will answer:

                	[super sendMessage:[NSString stringWithFormat:@"IMHERE_%@", RIGHT_SIDE]];
				
                	- (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel {
						if (super.gameStatus == kGameRunning) {
							(...)
						}
						else {
							(...)
							if (super.gameStatus == kGamePaused) {
								if ([message isEqualToString:@"STARTGAME"]) {
									[_waitingTimer invalidate];
									_waitingTimer = nil;
									[self setScene];
								}
								(...)
								else {
									NSArray *messageChunks = [message componentsSeparatedByString:@"_"];
									if ([messageChunks count] > 1) {
										
										if ([[messageChunks objectAtIndex:0] isEqualToString:@"ARUTHERE"] && [[messageChunks objectAtIndex:1] isEqualToString:LEFT_SIDE]) {
											[super sendMessage:[NSString stringWithFormat:@"IMHERE_%@", RIGHT_SIDE]];
										}
										else if ([[messageChunks objectAtIndex:0] isEqualToString:@"IMHERE"] && [[messageChunks objectAtIndex:1] isEqualToString:LEFT_SIDE]) {
											[super sendMessage:@"STARTGAME"];
										}
									}
								}
								(...)
							}
							(...)
						}
					}
				

You, the left side player, will handle the "IMHERE" message from the right side, and just answer that you are ready to start the game:

                	- (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel {
	                	if (super.gameStatus == kGameRunning) {
							(...)
						}
						else {
							if (super.gameStatus == kGamePaused) {
								if ([message isEqualToString:@"STARTGAME"]) {
									[_waitingTimer invalidate];
									_waitingTimer = nil;
									[self setScene];
								}
								(...)
								else {
									NSArray *messageChunks = [message componentsSeparatedByString:@"_"];
									if ([messageChunks count] > 1) {
										
										if ([[messageChunks objectAtIndex:0] isEqualToString:@"ARUTHERE"] && [[messageChunks objectAtIndex:1] isEqualToString:RIGHT_SIDE]) {
											[super sendMessage:[NSString stringWithFormat:@"IMHERE_%@", LEFT_SIDE]];
											
										}
										else if ([[messageChunks objectAtIndex:0] isEqualToString:@"IMHERE"] && [[messageChunks objectAtIndex:1] isEqualToString:RIGHT_SIDE]) {
											[super sendMessage:@"STARTGAME"];
										}
									}
								}
							}
							(...)
						}
					}
				

As soon as you send "STARTGAME":

                	[super sendMessage:@"STARTGAME"];
				

Each player (see above) will handle the message, and both will start the game.

With the game running the player should tap the cannon's base to shoot and the firing action will send a message:

                	NSString *message = [NSString stringWithFormat:@"SHOOT_%@_%d_%f", LEFT_SIDE, [super shootLeft], rotationRadians];
					[super sendMessage:message];
				

Where "LEFT_SIDE" is the side of player who fires (your shot), "[super shootLeft]" is the count of shots and "rotationRadians" the cannon's rotation. The players will process the messages and depending on player's side and where the message came from, an angry admin ball will be added to the game scene.

As mentioned before, the player on the left side (game id owner), will be responsible for managing the physics engine. He will send a message with the updated positions for each moving object. The other player, on the right side, will handle those messages and update each object's position.

                	// left side player; Game with physics
					- (void) sendPosition:(CCTime) deltaTime {
						
						_timer = _timer + deltaTime;
						_timerLabel.string = [NSString stringWithFormat:@"TIME: %.1f", _timer];
						
						NSString *message = [NSString stringWithFormat:@"POS_B|%f|%f|%f_LB|%d|%f|%f|%f_RB|%d|%f|%f|%f_%f",
											 _block.position.x, _block.position.y, _block.rotation,
											 [super shootLeft], _ball_left.position.x, _ball_left.position.y, _ball_left.rotation,
											 [super shootRight], _ball_right.position.x, _ball_right.position.y, _ball_right.rotation,
											 _timer];

						[super sendMessage:message];
					}
				
                	// right side player; Game without physics
                	// handle messages for POSITION and SHOOT
					- (void) receivedMSG:(NSString *) message onChannel:(NSString *) channel {

						if (super.gameStatus == kGameRunning) {
							
							NSArray *messageChunks = [message componentsSeparatedByString:@"_"];
							if ([messageChunks count] > 1) {
								
								if ([[messageChunks objectAtIndex:0] isEqualToString:@"POS"]) {
									
									NSArray *blockPos = [[messageChunks objectAtIndex:1] componentsSeparatedByString:@"|"];
									NSArray *lbPos = [[messageChunks objectAtIndex:2] componentsSeparatedByString:@"|"];
									NSArray *rbPos = [[messageChunks objectAtIndex:3] componentsSeparatedByString:@"|"];
									
									[_block setPosition:CGPointMake([[blockPos objectAtIndex:1] floatValue], [[blockPos objectAtIndex:2] floatValue])];
									[_block setRotation:[[blockPos objectAtIndex:3] floatValue]];
									
									if (_ball_left && [super shootLeft] == [[lbPos objectAtIndex:1] intValue]) {
										[_ball_left setPosition:CGPointMake([[lbPos objectAtIndex:2] floatValue], [[lbPos objectAtIndex:3] floatValue])];
										[_ball_left setRotation:[[lbPos objectAtIndex:4] floatValue]];
									}
									
									if (_ball_right && [super shootRight] == [[rbPos objectAtIndex:1] intValue]) {
										[_ball_right setPosition:CGPointMake([[rbPos objectAtIndex:2] floatValue], [[rbPos objectAtIndex:3] floatValue])];
										[_ball_right setRotation:[[rbPos objectAtIndex:4] floatValue]];
									}
									_timerLabel.string = [NSString stringWithFormat:@"TIME: %.1f", [[messageChunks objectAtIndex:4] floatValue]];
								}
								
								else if ([[messageChunks objectAtIndex:0] isEqualToString:@"SHOOT"]) {
									
									NSString *playerSide = [messageChunks objectAtIndex:1];
									int shoot = [[messageChunks objectAtIndex:2] intValue];
									NSString *angle = [messageChunks objectAtIndex:3];
									
									[self player:playerSide DidShoot:shoot WithAngle:[angle floatValue]];
								}
								(...)
							}
						}
						else {
							(...)
						}
					}
				

Being responsible for the physics, left player will detect the collision between the stone and both player's cannon. When a collision is detected a game over message is sent.

					// colision detection on physics game
					- (void) detectCollision {
						if (CGRectIntersectsRect(_leftBtt.boundingBox, _block.boundingBox))
						{
							[self unschedule:@selector(sendPosition:)];
							NSString *message = [NSString stringWithFormat:@"GMOV_%@_%d", RIGHT_SIDE, [super calcScoreForTimeElapsed:_timer]];
							[super sendMessage:message];
						}
						if (CGRectIntersectsRect(_rightBtt.boundingBox, _block.boundingBox))
						{
							[self unschedule:@selector(sendPosition:)];
							NSString *message = [NSString stringWithFormat:@"GMOV_%@_%d", LEFT_SIDE, [super calcScoreForTimeElapsed:_timer]];
							[super sendMessage:message];
						}
					}
				

Now that the game is over it's time to save the winner's score at the Cloud Storage table if the player desired to do so.

Saving the scores

As mentioned before the Angry Admin game uses Realtime Cloud Storage to save the game scores. In a nutshell, a table named BirdGame was created in cloud storage to keep the scores for each game id with the following key-schema:

gameID (primary key): identifies the game to which the scores belongs to;
score (secondary key): identifies the actual score;

An attribute named nickName is also added to save the nick name of the player. This table schema is useful to retrieve the scores of a given game by it's game Id. To keep the overall scores there's another table named StorageGames where the game name (in this case BirdGame) is the primary key and the score the secondary key. This is the code to save the score in both tables:


					- (IBAction) submitScore:(id)sender {
   
					    __block UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
					    activityView.center = CGPointMake(_scrollView.center.x, _scrollView.center.y * 0.85);
					    activityView.hidesWhenStopped = YES;
					    [activityView startAnimating];
					    [self.view addSubview:activityView];
					   
					    _submitBtt.enabled = NO;
					    _nickNameTextField.enabled = NO;

					    __block NSDictionary *itemDict = [[NSDictionary alloc] initWithObjectsAndKeys:
					                              [[StorageManager sharedManager] gameID], PKEY_BIRDGAME,
					                              [NSNumber numberWithInt:_score], SKEY_BIRDGAME,
					                              _nickNameTextField.text, @"nickName", nil];
					   
					    TableRef *tableRef = [[[StorageManager sharedManager] storageRef] table:TAB_BIRDGAME_SCORES];
					    [tableRef push:itemDict
					           success:^(ItemSnapshot *item) {//define block for a success callback
					               itemDict = nil;
					               itemDict = [[NSDictionary alloc] initWithObjectsAndKeys:
					                           PKEY_GAMES_VALUE, PKEY_GAMES,
					                           [NSNumber numberWithInt:_score], SKEY_GAMES,
					                           [[StorageManager sharedManager] gameID], @"gameID",
					                           _nickNameTextField.text, @"nickName", nil];
					              
					               TableRef *tableRef = [[[StorageManager sharedManager] storageRef] table:TAB_GAMES_SCORES];
					               [tableRef push:itemDict
					                      success:^(ItemSnapshot *item) {//define block for a success callback
					                         
					                          [_closeBtt sendActionsForControlEvents:UIControlEventTouchUpInside];
					                          [activityView stopAnimating];
					                          [activityView removeFromSuperview];
					                      }
					                        error:^(NSError* e){ //define block for an error callback
					                            NSLog(@"### Error: %@", [e localizedDescription]);
					                        }];
					           }
					             error:^(NSError* e){ //define block for an error callback
					                 NSLog(@"### Error: %@", [e localizedDescription]);
					             }];
					}
				

More details about working with Cloud Storage tables can be found here.

Retrieving the scores

To indulge the egos of our players we allow them to check the scores of their games and even the global scores across all games. Pressing the little cup image will bring the top 10 scores for both the current player game and overall scores (across all games).

Both lists are retrieved from the appropriate Cloud Storage table with the following code:


					- (void) getScores
					{
					    NSLog(@"GET SCORES %@", [_scoresControl titleForSegmentAtIndex: [_scoresControl selectedSegmentIndex]]);
					   
					    void (^cbSuccess)(ItemSnapshot*) = ^(ItemSnapshot *item) {
					       
					        if(item!=nil) {
					            NSDictionary *dic = [item val];
					            NSLog(@"Score Item: %@", dic);
					            [_scores addObject:dic];
					        }
					        else {
					            //we got all items
					            [self showScores];
					           
					            [_activityIndicator stopAnimating];
					        }
					    };
					   
					    void (^cbError)(NSError*) = ^(NSError* e){ //define block for an error callback
					        NSLog(@"### Error: %@", [e localizedDescription]);
					    };
					   
					    [_activityIndicator startAnimating];
					   
					    // clear scores - remove all scores
					    [_scores removeAllObjects];
					    TableRef *tableRef = nil;
					   
					    // my Game Id highScores
					    if (_scoresControl.selectedSegmentIndex == 0) {
					       
					        tableRef = [[[StorageManager sharedManager] storageRef] table:TAB_BIRDGAME_SCORES];
					           
					        [tableRef equalsString:PKEY_BIRDGAME value:[[StorageManager sharedManager] myGameID]];
					        [tableRef greaterEqualNumber:SKEY_BIRDGAME value:[NSNumber numberWithInt:0]];
					        [tableRef limit:10];
					        [tableRef desc];
					       
					        [tableRef getItems:cbSuccess error:cbError];
					       
					    }

					    // global highScores
					    else if (_scoresControl.selectedSegmentIndex == 1) {
					        tableRef = [[[StorageManager sharedManager] storageRef] table:TAB_GAMES_SCORES];
					        [tableRef equalsString:PKEY_GAMES value:PKEY_GAMES_VALUE];
					        [tableRef greaterEqualNumber:SKEY_GAMES value:[NSNumber numberWithInt:0]];
					        [tableRef limit:10];
					        [tableRef desc];
					        [tableRef getItems:cbSuccess error:cbError];
					    }
					}

				

More details about retrieving items from a Cloud Storage table can be found here.

Feeling inspired to code your own game?

We hope this guide helps you understand the main parts of the Angry Admin 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/Storage/tree/master/multi-player-mobile-game

The GitHub sample uses a public Realtime Framework demo license and you are welcome to use it. But if you want to keep your data private don't forget to get your own Realtime free license and replace the application key.

Have fun and code strong!

If you find this interesting please share: