How to build an Asynchronous
Turn-Based Game using
App42 Cloud APIs
Contents
- Introduction
- Why Turn-based Games
- Learning through a sample game – Word Junkies
- App42 Ecosystem for Game Developers
- Conclusion
Introduction
This book has been written to make you understand the mechanics behind Turn-based Gaming.
When you are finished reading with it, you will be able to make your own Turn-based and Challenge-based Games using App42 Backend APIs.
To make it more simplified, we have taken a sample game – Word Junkies, and have explained each step in detail.
This book is a comprehensive tutorial for Game Developers to make Turn-based games in the least possible time.
Enjoy Reading!!
Turn-based Games
Turn-based games are tactical games where players play alternately in turns. They are given a timed interval or maybe unlimited time to make their move.Normally 2-playered, but some 4-player games (like our childhood favourite ‘Ludo’) also form a part of this cadre.
Benefits of a Turn-based game
- Players are able to make planned moves
- The onus of winning/loosing is completely on them as there is a minimum server interference
- Players feel challenged, and thus more addicted to the game
- For developers,Turn-based games are easier to control and manage
Asynchronous Turn-based Games
There are times when users just don’t like to sit and wait for their turn while the opponent makes his move. Here asynchronous games come into play, wherein the game goes on for a long period of time, and players can even go offline in between.
But in such games, the users need to be notified of their turn. App42 Push Notification Service takes care of that. It notifies the user by sending a pop-up, a sound, a badge or a banner message when he is offline in the game,thus keeping him addicted.
Social Turn-based Games
According to a various reports,the retention rates for Social Turn-based games are higher than most other games. Due to the competitive game-play with their friends, people feel compelled to progress in their respective game.
Social Turn-based games succeed in creating a loyal-user community by offering popular evergreen games played among friends.
From a revenue perspective, Turn-based games commonly monetize via In-App Purchase from consumers eager to go ahead in the game, and by showing ads to those not willing to pay.
NextStep
By now you must have learned that Social Turn-based games are a huge success. Understanding your need, we have provided a synopsis of our Social Gaming Features in the end pages of the book. These features can be easily added to your product to make it a highly engaging Social Turn-based Game.
Let's proceed with the Game Development Tutorial.
Words Junkie
The game-play of a word game looks very simple but is a bit tricky when it comes to development. Today we will make a word game slightly different than the games we see around.
Words Junkie is a simple turn-based word game built on iOS platform, that allows players to place one character in each turn and accumulate points based on the length of the word they form.
Features:
- It has a running timer
- Facebook connect feature
- Screenshot sharing
- Push Notifications
- Leaderboards
- Match-making Logic
We will be using UIKit as well as cocos2d v2.x as basic tools.
We can divide the game logic in different sections in terms of development:
- Game Layout
- Game Background creation
- Scroll Layer creation
- Creating a Word Board
- Alphabet Rack
- Score Display
- Menu Layer
- Core Game Logic
- Character Placement on the Word Board
- Word Selection
- Word Submission
- Database Updating
- Notify the opponent
- Session Resuming
- Leaderboard
- Score Submission
- Global Leaderboard
- Friends’Leaderboard
- Match-Making
- Facebook Friends
- Random Opponents
Game Layouts
To start off, you can download the source code from here. Now open up the Xcode project which has been downloaded and navigate to the class PWGameLogicLayer.m. This is a singleton class inherited from CCLayerColor and will behave as a game layer which consists of the basic logic of the game.
Browse the class PWGameLogicLayer.m for the method createBgLayerForIPad / createBgLayerForIPhone that creates sprites with appropriate positions to meet the background requirements of the game based on the device’s screen size. These sprites will be added to PWGameLogicLayer as it must be static.
To introduce scrolling and zoom-in/zoom-out, we will be using CCLayerPanZoom from Cocos2D-iPhone-Extensions v0.2.1. This layer will work as the base layer for the Word-Board because it can be scrolled and zoomed-in or zoomed-out. The method createScrollLayer of the class PWGameLogicLayer.m will create scroll layer.
-(void)createScrollLayer
{
CGSize s = [[CCDirector sharedDirector] winSize];
[[[CCDirector sharedDirector] view] setMultipleTouchEnabled:YES];
_panZoomLayer = [[CCLayerPanZoom node] retain];
[self addChild: _panZoomLayer z:1];
_panZoomLayer.delegate = self;
_panZoomLayer.contentSize = s;
_panZoomLayer.mode = kCCLayerPanZoomModeSheet;
if(UI_USER_INTERFACE_IDIOM()==UIUserInterfaceIdiomPhone)
{
_panZoomLayer.minScale = 1.0f;
_panZoomLayer.maxScale = 2.0f;
}
else
{
_panZoomLayer.minScale = 1.0f;
_panZoomLayer.maxScale = 2.0f;
}
_panZoomLayer.rubberEffectRatio = 0.0f;
_panZoomLayer.panBoundsRect = CGRectMake(0, 0, s.width, s.height);
_panZoomLayer.position = ccp(s.width/2, s.height/2);
}
panZoomLayer is being created globally and added to PWGameLogicLayer.
The word board is an arrangement of a fixed size of tiles in a 2 - dimensional matrix of certain rows and columns. There are three kinds of tiles which have been used in this game:
Normal Tile: Indicates an empty place.
Highlighted Tile: Indicates a selected character.
Brown Tile: Indicates the tile occupied by a character.
So, when the game starts for the very first time, the Word Board will be constructed using normal tiles as all the tiles are empty by default. As the game proceeds, the Word Board will be created as a mixture of Normal tiles as well as Brown tiles depending upon the game data saved in the database.
Let’s browse the class PWGameLogicLayer.m for the method createWordBoard:
-(void)createWordBoard
{
CGSize s = _panZoomLayer.contentSize;
if(UI_USER_INTERFACE_IDIOM()==UIUserInterfaceIdiomPhone)
{
rowWidth = TILE_WIDTH_IPHONE;
rowHeight = TILE_HEIGHT_IPHONE;
startPoint = ccp((s.width-numberOfColumns*rowWidth)/2, s.height-TILE_Y_OFFSET_IPHONE/2-((MAX_NUMBER_ROWS_ALLOWED-numberOfRows)*rowHeight/2));
}
else
{
rowWidth = TILE_WIDTH;
rowHeight = TILE_HEIGHT;
startPoint = ccp((s.width-numberOfColumns*rowWidth)/2, s.height-TILE_Y_OFFSET/2-((MAX_NUMBER_ROWS_ALLOWED-numberOfRows)*rowHeight/2));
}
for (int i=0; i<numberOfRows; i++)
{
CGPoint refPoint = CGPointMake(startPoint.x+rowWidth/2, startPoint.y-rowHeight*numberOfRows+rowHeight/2+i*rowHeight);
for (int j=0; j<numberOfColumns; j++)
{
CCSprite *tile = [CCSprite spriteWithFile:@"Tile_Normal.png"];
[tile setPosition:CGPointMake(refPoint.x+j*rowWidth, refPoint.y)];
[_panZoomLayer addChild:tile z:1];
}
}
}
Here, startPoint is the uppermost corner of the Word-Board that we will use as a reference point to calculate the indices of the tiles.
Creating the Word-Board is nothing but traversing a matrix of dimensions: (numberOfRows x numberOfColumns). Here refPoint is the position of the bottom-left tile and with the progress of the loops, it travels tile-by-tile from left-to-right as well as bottom-to-top. The tile sprites will be added on the scroll layer which we had created earlier.
Now the next step is to create a rack of alphabets from where users will drag the characters and drop on to the Word-Board. The alphabets must have the capability to swallow the touches on being touched as these need to be dragged in the game play. For that we need to differentiate between the touches on game layer and alphabets. The best practice for this is to create a separate class (e.g.PWCharacter class) which inherits CCLayerColor and implements the required touch delegate methods inside this class. This class has some properties defined like:
isMovable --> Indicates an empty place.
isPlaced --> Indicates a selected character.
pos_index --> Indicates the tile occupied by a character.
alphabet --> Indicates the tile occupied by a character.
So, let’s browse through the code that creates the rack of alphabets:
-(void)createCharacterMenuForIPad
{
CGSize size = [[CCDirector sharedDirector] winSize];
float x_pos = 0;
NSMutableArray *alphabetsArray =[[NSMutableArray alloc]initWithObjects:@"A",@"B",@"C",@"D",@"E",
@"F",@"G",@"H",@"I",@"J",@"K",@"L",@"M",@"N",@"O",@"P",@"Q",@"R",
@"S",@"T",@"U",@"V",@"W",@"X",@"Y",@"Z",nil];
int alphabetCount = [alphabetsArray count];
for (int i = 0; i<alphabetCount; i++)
{
CCSprite *tile = [CCSprite spriteWithFile:@"Tile_Small.png"];
CGSize tileSize = tile.contentSize;
if (!x_pos)
{
x_pos = (size.width-alphabetCount*(tileSize.width+1))/2;
}
PWCharacter *character = [PWCharacter layerWithColor:ccc4(25, 100, 50, 0)
width:tileSize.width height:tileSize.height];
character.pos_index = i;
character.tag = CHARACTER_MENU_CHAR_BASE_TAG+i;
character.position = ccp(x_pos,MENU_LAYER_HEIGHT_IPAD);
character.isMovable = NO;
character.alphabet = [alphabetsArray objectAtIndex:i];
[tile setPosition:CGPointMake(tileSize.width/2, tileSize.height/2)];
[tile setTag:9];
[character addChild:tile z:10];
CCLabelTTF *label = [CCLabelTTF labelWithString:[alphabetsArray objectAtIndex:i]
fontName:GLOBAL_FONT fontSize:30];
label.tag = 10;
label.position = ccp(tileSize.width/2, tileSize.height/2);
label.color = ccWHITE;
[character addChild:label z:11];
[self addChild:character z:CHARACTER_MENU];
x_pos+=tileSize.width+1;
}
[alphabetsArray release];
}
Above method creates characters from A to Z and adds them to PWGameLogicLayer above scroll layer in terms of z-order.
To display the score, we will create a separate layer and will call it as “PWHudLayer”. This layer will have six childrenas shown below:
It consists of:
All are implemented in PWMenuLayer.m class which is detailed in the later part.
Core Game Logic
As Words Junkie is a turn-based multiplayer game, it has some constants to identify the current game mode, player turn etc.
Placement Mode - Initial mode in which alphabets need to be placed on the Word-Board.
Search Mode - In this mode, word needs to be selected and submitted.
The game has three basic steps to play; alphabet placement, word selection and word submission. So, let’s start with the first step i.e. alphabet placement.
When we touch an alphabet on the rack, it calls the ccTouchBegan delegate method of the PWCharacter class
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
PWGameLogicLayer *gameLayer = [PWGameLogicLayer sharedInstance];
if(self.visible && ([gameLayer selectedChar]==NULL || isMovable) && [self containsTouchLocation:touch] && gameLayer.currentGameMode==kPlacementMode && !isPlaced && gameLayer.turnIndicator==kPlayerOneTurn)
{
[gameLayer enableScrolling:NO];
[self startNotPlaceableIndicatorAnimation:NO];
gameLayer.selectedChar = self;
[gameLayer resizeTheCharacterToTileSize];
[self changeTextureWithImage:@"Tile_Placed.png"];
CGPoint point = [touch locationInView:[[CCDirector sharedDirector] view]];
CGPoint spoint = [[CCDirector sharedDirector] convertToGL:point];
prevTouchPoint = [self convertToNodeSpace:spoint];
if (!isMovable)
{
[[[PWGameController sharedInstance] tutorialManager] setTutorialElementsVisible:NO];
[gameLayer getSelectedCharFromCharacterMenuWithLetter:alphabet withPositionINdex:pos_index];
}
self.isMovable = YES;
[self animateTheChar:YES];
gameLayer.isCharacterSelected = YES;
return YES;
}
return NO;
}
The touch will be accepted only when:
- Character is visible
- Character is movable
- Character’s rect contains the touch location
- Current game mode is placement mode
- Character is not already placed on the word-board
- And the turn is of player one
If the touch is accepted then the above method:
- disables the game layer scrolling
- sets self as a selectedChar
- resizes self to the size of tiles on the word-board and changes the texture accordingly
- if the character is selected from the rack, it creates another character with same characteristics to fill the vacant place on the rack
- starts pendulum type movement on the self
On dragging the character, ccTouchMoved changes the character position as follows:
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint point = [touch locationInView:[[CCDirector sharedDirector] view]];
CGPoint spoint = [[CCDirector sharedDirector] convertToGL:point];
spoint = [self convertToNodeSpace:spoint];
self.position = CGPointMake(self.position.x+spoint.x-prevTouchPoint.x, self.position.y+spoint.y-prevTouchPoint.y);
}
When we drop the character anywhere, PWGameLogicLayer class does the character placement calculations in its ccTouchesEnded method:
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:[[CCDirector sharedDirector] view]];
CGPoint spoint = [[CCDirector sharedDirector] convertToGL:point];
spoint = [_panZoomLayer convertToNodeSpace:spoint];
if (currentGameMode == kPlacementMode && isCharacterSelected)
{
[self placeCharacterAtCorrectPlaceOnEndTouchPoint:spoint];
isCharacterSelected = NO;
}
else if (currentGameMode == kSearchMode)
{
CGPoint index = [self getIndexForThePosition:spoint];
if (selectedIndicesArray.count<1 && ![self isCharPlaceableAtIndex:index])
{
[selectedIndicesArray addObject:NSStringFromCGPoint(index)];
[self selectTheChar:index];
if (![[PWGameController sharedInstance] tutorialStatus])
{
[[[PWGameController sharedInstance] tutorialManager] setTutorialElementsVisible:NO];
[[PWGameController sharedInstance] setNextTutorialStep:kClickOnPlay];
[[[PWGameController sharedInstance] tutorialManager] showTutorialStep:[[PWGameController sharedInstance] nextTutorialStep]];
}
}
else if([self validSelectionCheck:index] && (![selectedIndicesArray containsObject:NSStringFromCGPoint(index)]))
{
[selectedIndicesArray addObject:NSStringFromCGPoint(index)];
[self selectTheChar:index];
}
else if(![self isCharPlaceableAtIndex:index])
{
[self removeChild:selectionLine cleanup:YES];
selectionLine = nil;
[self resetSelectionParameters];
}
}
}
-(void)placeCharacterAtCorrectPlaceOnEndTouchPoint:(CGPoint)touchPoint
{
lastPlacedCharIndex = [self getIndexForThePosition:touchPoint];
[self enableScrolling:YES];
[selectedChar removeFromParent];
selectedChar.tag = [self getCharTagForIndex:lastPlacedCharIndex];
[_panZoomLayer addChild:selectedChar];
if ([self isBeyondTheBoundary:lastPlacedCharIndex])
{
[selectedChar removeFromParentAndCleanup:YES];
selectedChar = nil;
lastPlacedCharIndex = ccp(-1, -1);
}
else if ([self isCharPlaceableAtIndex:lastPlacedCharIndex])
{
[[SimpleAudioEngine sharedEngine] playEffect:CHARACTER_PLACED];
[menuLayer setButtonsEnabled:YES];
currentGameMode = kSearchMode;
CGPoint pos = [self getCorrectCharPositionForTheIndex:lastPlacedCharIndex];
[selectedChar setPosition:pos];
[self placeTheChar];
}
else
{
[menuLayer setRecallButtonEnabled:YES];
[selectedChar startNotPlaceableIndicatorAnimation:YES];
}
}
-(void)placeTheChar
{
[self reorderSelectedChar];
[[PWGameController sharedInstance].dataManager addAlphabet:selectedChar.alphabet
atIndex:NSStringFromCGPoint(lastPlacedCharIndex)];
[selectedChar setOpacity:0];
selectedChar.isMovable = NO;
selectedChar.isPlaced = YES;
self.selectedChar = nil;
lastPlacedCharIndex = ccp(-1, -1);
}
On drop, the above method:
- calculates the index for the current position of the alphabet with respect to the
startPoint
- checks for the word-board boundary conditions; if it is dropped beyond the boundary, the alphabet will be removed
- checks that the lastPlacedCharIndex is placeable or not, if the alphabet dropped inside the boundary
- places the alphabet at the lastPlacrdCharIndex if placeable, otherwise starts not placeable indicator animation
- Word selection and submission
To form a word, you have to select all the alphabets which you want to include in the word including the last placed character. When you click on the character to select, the method ccTouchesEnded of PWGameLogicLayer:
- Calculates the touch position index if the current mode is SearchMode
- Checks for the valid selection and if it is a valid one, adds it to selectedIndicesArray otherwise resets the selection parameters to its default values
- The selected word will be submitted by playClicked method of PWMenuLayer
The play Clicked method:
- checks for the game mode; if it’s placement mode, an alert pops up saying “Place a character”
- in search mode, first it checks for word selection then checks whether the selection is valid or not as per the game rule
- then checks whether the selected word is a dictionary word or not and makes sure that the selected word has not been submitted earlier
- if any condition goes false, it prompts the appropriate alert and if the selected word satisfies all the conditions, it is locked for the submission
As Words Junkie is a turn-based multiplayer game, the game session needs to be saved after every move. This is done in case whenever any of the players want to play again, the game should start from the last move played by either of them.
As we planned to make Words Junkie a multiplayer game over the internet, we had to save the data on a remote server so that any of the players could have access to the game session easily.
So, we decided to use Storage Services of our App42 Cloud API to store the game session. With this service, creating docs, updating them later on, and fetching data can be done by just one API call whenever and wherever is required.
So, when the word is ready to be saved in the database, the method
writeSessionToTheServer of the class PWDataManager will be called from the methodscontinueWithThisValidWord or continueWithThisInvalidWord of the class PWGameLogicLayer.
-(void)writeSessionToTheServer
{
NSAutoreleasePool *threadPool = [[NSAutoreleasePool alloc] init];
[sessionInfo setObject:[NSNumber numberWithInt:(++sessionTokenNumber)] forKey:@"sessionToken"];
int globalSessionToken=-1;
int i =0;
do
{
NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
NSLog(@"%s..i=%d",__FUNCTION__,++i);
[self updateGameSessionOnSever];
globalSessionToken = [self getGlobalSessionToken];
[loopPool release];
}while (globalSessionToken < sessionTokenNumber && i<=10);
if (delegate && [delegate respondsToSelector:@selector(removeSubmittingScreen)])
{
[delegate performSelectorOnMainThread:@selector(removeSubmittingScreen) withObject:nil waitUntilDone:NO];
}
if (sendPush)
{
[self notifyOpponentForRecentMove:recentWord];
self.recentWord=nil;
sendPush = NO;
}
[threadPool release];
}
-(void)updateGameSessionOnSever
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
StorageService *storageService = [servicAPI buildStorageService];
@try
{
Storage *storage = [storageService updateDocumentByDocId:DOC_NAME
collectionName:COLLECTION_NAME docId:doc_Id newJsonDoc:[sessionInfo JSONRepresentation]];
[storage release];
}
@catch (App42Exception *exception)
{
}
@finally
{
[storageService release];
[servicAPI release];
}
}
The above method will update the session of this particular game using a uniqueDocument id, which is assigned by the App42 Storage Service, whenever a player starts a new game.
-(void)createNewGameSession
{
myScore = 0;
opponentScore = 0;
self.player1Dict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:myScore],MY_SCORE,[[PWFacebookHelper sharedInstance] userName],PLAYER_NAME,[NSNumber numberWithInt:kPlayerOneTurn],TURN,[NSNumber numberWithInt:1],SKIPP_CHANCE_LEFT, nil];
self.player2Dict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:opponentScore],MY_SCORE,self.player2_name,PLAYER_NAME,[NSNumber numberWithInt:kPlayerTwoTurn],TURN,[NSNumber numberWithInt:1],SKIPP_CHANCE_LEFT, nil];
sessionInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[NSString stringWithFormat:@"%@",player1],PLAYER1,
[NSString stringWithFormat:@"%@",player2],PLAYER2,
[NSNumber numberWithInt:kPlacementMode],GAME_MODE,
[NSNumber numberWithInt:kGameRunning],GAME_STATE,
[NSNumber numberWithInt:kNormalMode],GAME_TYPE,
NSStringFromCGSize(CGSizeMake(NUMBER_OF_ROWS, NUMBER_OF_COLUMNS)),GAME_BOARD_SIZE,
player1Dict,[NSString stringWithFormat:@"%@",player1],
player2Dict,[NSString stringWithFormat:@"%@",player2],
nil];
[self performSelectorInBackground:@selector(startNewGameSessionOntheServer) withObject:nil];
}
-(void)startNewGameSessionOntheServer
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
StorageService *storageService = [servicAPI buildStorageService];
@try
{
Storage *storage = [storageService insertJSONDocument:DOC_NAME collectionName:COLLECTION_NAME json:[sessionInfo JSONRepresentation]];
self.doc_Id = [[[storage jsonDocArray] objectAtIndex:0] docId];
}
@catch (App42Exception *exception)
{ }
@finally
{
[storageService release];
[servicAPI release];
}
}
The method “createNewGameSession” of PWDataManager creates a new game session with initial default values and calls
startNewGameSessionOntheServer method of PWDataManager to insert a new document for this game.
In turn-based games, the opponents must be notified of the recent move. In this case, if the game is not running at the opponent’s end then a Push Notification must be sent to him for the same,and if the game is in running state then the screen must be refreshed to reflect the recent move.
To send Push Notifications we will be using App42 - Push Notification Service.
- Steps involved in configuring the push notification service:t
- Create a certificate fromiOS DevCenter and download it.
- Double click the .cer file, which was downloaded in the above step, to install it.
- Convert the iPhone developer certificate into a .p12 file by following the below mentioned steps:
- Open the Keychain Access application (in the Applications/Utilities folder)
- Select the Keys category in Keychain Access
- Select the private key associated with your iPhone Development Certificate
- The private key is identified by the iPhone Developer as:
public certificate that is paired with it
- Select File > Export Items
- Save your key in the Personal Information Exchange (.p12) file format
- You will be prompted to create a password which will be used when you will attempt to import this key on another computer
- Make your .p12 file compatible with APP42 server by following the below mentioned steps:
- Upload iphone_dev.p12 file to the AppHQ console
- Login to AppHQ console and select Push Notification in AppHQ Menu
- Select iOS settings in Settings
- Select your App Name from the drop down menu
- Browse your iphone_developer.p12 file by clicking ‘choose file’ option to upload the file
- Enter password given during creation of the iphone_developer.p12 file
- Select the environment from the drop down menu
- Click on the submit button and it’s done here
- Open your Xcode project, navigate to the AppDelegate.m class and change the application:didFinishLaunchingWithOptions: method to look like this:
-(BOOL)applica
tion:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{ // Let the device know we want to receive push notifications
[[UIApplicationsharedApplication]registerForRemoteNotificationTypes:
(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound |
UIRemoteNotificationTypeAlert)]; returnYES;
}
- Now you need to add the following delegate methods in your AppDelegate.m in order to receive push notification:
- After getting the device token you need to register your device with the App42 Server to set up a connection to APNS to send the push notification to this device token. Change the delegate method
application:didRegisterForRemoteNotificationsWithDeviceToken: in the AppDelegate.m class to look like this:
-
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
{
NSLog(@"My token is: %@", deviceToken);
// Prepare the Device Token for Registration (remove spaces and <>)
NSString *devToken = [[[[deviceTokendescription]
stringByReplacingOccurrencesOfString:@"<"withString:@""]
stringByReplacingOccurrencesOfString:@">"withString:@""]
stringByReplacingOccurrencesOfString: @" "withString: @""];
NSLog(@"My Device token is: %@", devToken);
/**
* Register the device token for App42 Push notification services
*/
[selfregisterUserForPushNotificationToApp42Cloud:devToken];
}
-(void)registerUserForPushNotificationToApp42Cloud:(NSString*)deviceToken
{
ServiceAPI *serviceObj = [[ServiceAPIalloc]init];
serviceObj.apiKey = APP42_APP_KEY;
serviceObj.secretKey = APP42_SECRET_KEY;
PushNotificationService *pushObj = [serviceObjbuildPushService];
@try
{
PushNotification
*pushNotification=[pushObjregisterDeviceToken:deviceTokenwithUser:@"User Name"];
[pushNotificationrelease];
}
@catch (App42Exception *exception)
{
NSLog(@"%@",exception.reason);
}
@finally
{
[serviceObjrelease];
[pushObjrelease];
}
}
- Now to send a push notification, call the following method in a commonly used class in your project so that you can call this whenever you want to:
- If you want to take any actions when you receive a push notification, then you need to add the following delegate method in the AppDelegate.m class:
-
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
{
NSLog(@"%s..userInfo=%@",__FUNCTION__,userInfo);
/**
* Dump your code here according to your requirement after receiving push
*/ }
With the above mentioned step, your App has been successfully set up to send/receive push notifications through our App42 Server using App42 Push Notification Service.
In this game, register User For Push Notification To App42 Cloud method registers the user device to the App42 Push Notification Service.
-(void)registerUserForPushNotificationToApp42Cloud
{
if (!self.deviceToken)
{
return;
}
ServiceAPI *serviceObj = [[ServiceAPI alloc]init];
serviceObj.apiKey = APP42_APP_KEY;
serviceObj.secretKey = APP42_SECRET_KEY;
PushNotificationService *pushObj = [serviceObj buildPushService];
@try
{
PushNotification *pushNotification =[pushObj registerDeviceToken:self.deviceToken withUser:player1];
[pushNotification release];
}
@catch (App42Exception *exception)
{
NSLog(@"%@",exception.reason);
}
@finally
{
[serviceObj release];
[pushObj release];
}
}
And the method notify Opponent For Recent Move notifies the opponent of your last move.
-(void)notifyOpponentForRecentMove:(NSString*)word
{
ServiceAPI *serviceObj = [[ServiceAPI alloc]init];
serviceObj.apiKey = APP42_APP_KEY;
serviceObj.secretKey = APP42_SECRET_KEY;
PushNotificationService *pushObj = [serviceObj buildPushService];
int score = word.length;
score *=score;
@try
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
NSString *message;
if ([[[PWGameController sharedInstance] alertManager] alertType]==kValidWordAlert)
{
message = [NSString stringWithFormat:@"%@ made %@ for %d points.It's your turn now!",[[PWFacebookHelper sharedInstance] userName],[word uppercaseString],score];
}
else if ([[[PWGameController sharedInstance] alertManager] alertType]==kInvalidWordAlert)
{
message = [NSString stringWithFormat:@"It's your turn against %@!",[[PWFacebookHelper sharedInstance] userName]];
}
else if([[[PWGameController sharedInstance] alertManager] alertType]==kQuitGameAlert)
{
message = [NSString stringWithFormat:@"%@ has quit the game. You won!",[[PWFacebookHelper sharedInstance] userName]];
}
else if([[[PWGameController sharedInstance] alertManager] alertType]==kGameOverAlert)
{
NSString *winnerID =[sessionInfo objectForKey:WINNER_ID];
NSString *resultString;
if ([winnerID isEqualToString:player1])
{
NSString *playerName = [[[[PWFacebookHelper sharedInstance] userName] componentsSeparatedByString:@" "] objectAtIndex:0];
resultString = [NSString stringWithFormat:@"%@ Won!",playerName];
}
else if (winnerID.length==0)
{
resultString = @"It's a Tie!";
}
else
{
resultString = @"You Won!";
}
message = [NSString stringWithFormat:@"No more tiles left. %@",resultString];
}
else
{
message = [NSString stringWithFormat:@"%@ has passed the turn.It's your turn now!",[[PWFacebookHelper sharedInstance] userName]];
}
[dictionary setObject:message forKey:@"alert"];
[dictionary setObject:@"default" forKey:@"sound"];
[dictionary setObject:@"1" forKey:@"badge"];
PushNotification *pushNotification = [pushObj sendPushMessageToUser:player2 withMessageDictionary:dictionary];
[pushNotification release];
NSLog(@"player2=%@",player2);
}
@catch (App42Exception *exception)
{
NSLog(@"%@",exception.reason);
}
@finally
{
[serviceObj release];
[pushObj release];
}
}
It’s very important in turn-based games that for every launch, game must resume itself from the last move played by either of the players. Here on every launch we check whether the game has any data to resume or not. If yes, then the method resumeGameFromSession of PWGameLogicLayer resumes the game by placing alphabets which were placed earlier.
-(void)resumeGameFromSession
{
NSMutableDictionary *gameDataDict = [PWGameController sharedInstance].dataManager.gameDataDict;
NSArray *keys = [gameDataDict allKeys];
for (NSString *key in keys)
{
[self createCharacter:[gameDataDict objectForKey:key] atIndex:key];
}
}
-(void)createCharacter:(NSString*)alphabet atIndex:(NSString*)indexString
{
CGPoint index = CGPointFromString(indexString);
PWCharacter *character = [PWCharacter layerWithColor:ccc4(25, 100, 50, 0) width:rowWidth height:rowHeight];
character.position = [self getCorrectCharPositionForTheIndex:index];
character.isMovable = NO;
character.isPlaced = YES;
character.alphabet = alphabet;
character.tag = [self getCharTagForIndex:index];
CCSprite *tile = [CCSprite spriteWithFile:@"Tile_Placed.png"];
[tile setPosition:CGPointMake(rowWidth/2, rowHeight/2)];
[tile setTag:9];
[character addChild:tile z:1];
CCLabelTTF *label = [CCLabelTTF labelWithString:alphabet fontName:GLOBAL_FONT fontSize:30*scaleFactor];
label.tag = [self getCharTagForIndex:index];
label.position = ccp(rowWidth/2, rowHeight/2);
label.color = ccWHITE;
[character addChild:label z:2];
[_panZoomLayer addChild:character z:10];
}
Leaderboard
Nowadays, global game Leaderboards and social interactions have become essential tools for keeping users engaged in mobile games. This requires game developers to build and deploy their own web services, maintain and update user score databases and integrate a social platform (Facebook) with their Leaderboards.
But we are going to use Scoreboard Services of the App42 Cloud API. It’s an open source customizable project which developers simply have to plug and play with their game to offer a rich set of features to their users. At the core, it uses our powerful App42 cloud APIs. It wraps them around a clean and customizable user interface. So let’s browse through the code for more understanding. In this game, we will be submitting the game scores to the Leaderboard services when it gets over.
The method “saveScore” of PWDataManager submit the game score to the App42 Scoreboard Service.
-(void)saveScore
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
ScoreBoardService *scoreboardService = [servicAPI buildScoreBoardService];
@try
{
NSString *userName = [NSString stringWithFormat:@"%@zz%@",[[PWFacebookHelper sharedInstance] userName],player1];
[scoreboardService saveUserScore:GAME_NAME gameUserName:userName gameScore:myScore];
}
@catch (App42Exception *exception)
{
}
@finally
{
[scoreboardService release];
[servicAPI release];
}
}
To save the score, first you should create your game on the AppHQ Console and you can start submitting the score with your user name.
Leaderboards can be broken down into several subcategories such as: Global, Friends, Today’s, etc. In this game, Leaderboard will be having two sub categories:
A Global Leaderboard shows where the player is in relation to everyone on the site. The method “getScores” of PWDataManager will fetch the top N rankers from the list, where N is the number of records wanted from the list. In this game, we will be requesting top 20 rankers’ records.
-(NSMutableArray*)getScores
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
ScoreBoardService *scoreboardService = [servicAPI buildScoreBoardService];
NSMutableArray *scoreList =nil;
@try
{
Game *game=[scoreboardService getTopNRankers:GAME_NAME max:MAX_NUMBER_OF_RECORDS_DISPLAYED_IN_LB];
scoreList = game.scoreList;
}
@catch (App42Exception *exception)
{ }
@finally
{
[scoreboardService release];
[servicAPI release];
return scoreList;
}
}
A Friends Leaderboard shows the players where they stand as compared to their friends. The method “getFriendsScores” of the class PWDataManagerwill fetch the top N rankers from the list of friend’s score, where N is the number of records needed from the list by you. In this game, we will be requesting top 20 rankers’ records.
-(NSMutableArray*)getFriendsScores
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
ScoreBoardService *scoreboardService = [servicAPI buildScoreBoardService];
NSMutableArray *scoreList=nil;
@try
{
NSLog(@"friendsList=%@",friendsList);
NSArray *arr = [self getFriendsId:friendsList];
NSLog(@"arr=%@",arr);
Game *game=[scoreboardService getTopRankersByGroup:GAME_NAME group:arr];
scoreList = game.scoreList;
}
@catch (App42Exception *exception)
{ }
@finally
{
[scoreboardService release];
[servicAPI release];
return scoreList;
}
}
Here the Scoreboard Service will take care of all the difficult activities such as finding a friend’s score from the list of scores and then will sort them in descending order. It will also return the top N rankers among the friends. You don't need to do anything except calling the API with your friends’ IDs in an array as a parameter.
So, now we have all the data needed to create a Leaderboard screen with two sub-categories as Global and Friends. Now you can browse through the PWLeaderboardViewController class to understand more of UI related work. The Friends Leaderboard will be turned on by default but you can always switch between tabs to see the Global one. The Leaderboard data will be shown in a UITableView and each cell of the UITableView will contain the player’s image, name, rank and score.
In this game, you will either deal with your Facebook friends or any random users. So we made it mandatory to login with Facebook to play this game. Now when you will launch this game for the first time, it will ask you to login with your Facebook account. Once you are logged in then the Game Home Screen will be shown which will contain active and finished games’ list and the buttons to go to other screens of the game.
- The view Did Load method of the PWHomeViewController
- checks that whether the user is logged into Facebook or not. If not, it shows login view otherwise loads the original home screen.
- asks the App42 Storage Service for the list of games played by this user, if any, bycalling getAllGames method of PWDataManager from the reloadTableView method of PWHomeViewController.
-(void)getAllGames
{
ServiceAPI *servicAPI = [[ServiceAPI alloc] init];
servicAPI.apiKey = APP42_APP_KEY;
servicAPI.secretKey = APP42_SECRET_KEY;
StorageService *storageService = [servicAPI buildStorageService];
Storage *storage;
@try
{
PWFacebookHelper *fbHelper = [PWFacebookHelper sharedInstance];
fbHelper.userInfoDict = [[NSUserDefaults standardUserDefaults] objectForKey:@"facebookUserInfo"];
if (fbHelper.userInfoDict)
{
fbHelper.userName = [[NSUserDefaults standardUserDefaults] objectForKey:@"userName"];
fbHelper.userId = [[NSUserDefaults standardUserDefaults] objectForKey:@"userId"];
[self setPlayer1:[NSString stringWithFormat:@"%@",fbHelper.userId]];
}
Query *query1_1 = [QueryBuilder buildQueryWithKey:PLAYER1 value:player1 andOperator:APP42_OP_EQUALS];
Query *query1_2 = [QueryBuilder buildQueryWithKey:PLAYER2 value:player1 andOperator:APP42_OP_EQUALS];
Query *query1_3 = [QueryBuilder combineQuery:query1_1 withQuery:query1_2 usingOperator:APP42_OP_OR];
storage = [storageService findDocumentsByQuery:query1_3 dbName:DOC_NAME collectionName:COLLECTION_NAME];
self.gamesArray = [storage jsonDocArray];
}
@catch (App42Exception *exception)
{
self.gamesArray=nil;
NSLog(@"exception=%@",exception.reason);
}
@finally
{
[storageService release];
[servicAPI release];
}
}
-(void)reloadTableView
{
[[[PWGameController sharedInstance] dataManager] getAllGames];
[self getFinishedGames];
[gamesListView reloadData];
[self removeAcitvityIndicator];
}
In get All Games method, we create a query based on App42 Storage Service query management. The query will request the game’s data in which the current logged in user is involved as player-one or player-two. After getting the response, it will reload the table view to show the list of games and by default active games will be turned on.
Match-Making
To start a new game, the basic requirement is the selection of the opponent. It can be done in two ways:
Most of the times people want to play with their friends, so we will be considering Facebook friends as one of the source of getting opponents. To play with Facebook friends, first we need to have the list of the friends who have installed this game on their devices.
The method get Friends Playing This Game of class PWFacebookHelper does this for you. It creates a query and requests the list of friends from Facebook based on this query.
-(void)getFriendsPlayingThisGame
{
// Query to fetch the active user's friends, limit to 25.
NSString *query = @"SELECT uid, name, is_app_user FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1=me()) AND is_app_user=1";
// Set up the query parameter
NSDictionary *queryParam =[NSDictionary dictionaryWithObjectsAndKeys:query, @"q", nil];
// Make the API request that uses FQL
[FBRequestConnection startWithGraphPath:@"/fql"
parameters:queryParam
HTTPMethod:@"GET"
completionHandler:^(FBRequestConnection *connection,
id result,
NSError *error)
{
if (error)
{
NSLog(@"Error: %@", [error localizedDescription]);
NSLog(@"error=%@",[error description]);
}
else
{
NSLog(@"Result: %@", result);
// Get the friend data to display
NSArray *friendInfo = (NSArray *) [result objectForKey:@"data"];
if( self.delegate && [self.delegate respondsToSelector:@selector(friendListRetrieved:)])
{
[self.delegate friendListRetrieved:friendInfo];
[self saveFriends:friendInfo];
}
}
}];
}
In return, you get the array of friends who have installed this game on their devices. The class PWFriendsListViewController will display the list of the friends after getting the data in the method friendListRetrieved.
-(void)friendListRetrieved:(NSArray*)freindsList
{
NSLog(@"Friends=%@",freindsList);
[[[PWGameController sharedInstance] dataManager] setFriendsList:freindsList];
self.friendsList = freindsList;
[friendListTableView reloadData];
[self removeAcitvityIndicator];
~
Now you can select a friend to start game with that friend.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[[SimpleAudioEngine sharedEngine] playEffect:MENU_ITEM_CLICKED];
[[PWGameController sharedInstance] dataManager].player2 = [NSString stringWithFormat:@"%@",[[friendsList objectAtIndex:indexPath.row] objectForKey:@"uid"]];
[[PWGameController sharedInstance] dataManager].player2_name = [[friendsList objectAtIndex:indexPath.row] objectForKey:@"name"];
[[[PWGameController sharedInstance] dataManager] createNewGameSession];
[[PWGameController sharedInstance] switchToLayerWithCode:kGameLayer];
~
The tableView: didiSelectRowAtIndexPath delegate method will create a new game on the App42 Storage Service and will load the game screen for you
Secondly, one can choose to play with a random opponent. The method random Player Button Action will try to find a random player for you. If a player is not found then it will register you in the Random Stack Collection on the App42 Storage prompting a message saying “We will notify you when we find an opponent!”. Next time when you user search for a random player, a new game will be created between both of you.
-(IBAction)randomPlayerButtonAction:(id)sender
{
[[SimpleAudioEngine sharedEngine] playEffect:MENU_ITEM_CLICKED];
[self findingOpponentView];
[self performSelector:@selector(checkForRandomPlayer) withObject:nil afterDelay:0.1];
~
-(void)checkForRandomPlayer
{
PWDataManager *dataManager = [[PWGameController sharedInstance] dataManager];
NSArray *idArray = [[[PWGameController sharedInstance] dataManager] checkForRandomPlayer];
if (idArray && idArray.count)
{
int randomIndex = arc4random()%idArray.count;
NSDictionary *userInfoDict = [[dataManager getDictionaryFromJSON:[[idArray objectAtIndex:randomIndex] jsonDoc]] retain];
if ([[[userInfoDict objectForKey:RANDOM_STACK] objectForKey:@"uid"] isEqualToString:dataManager.player1])
{
[self removeFindingOpponentView];
// do nothing if the id of the requeting user and id in the stack is same.
NSDictionary *alertInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"",ALERT_TITLE,@"We will notify you when we find an opponent!",ALERT_MESSAGE, nil];
[[PWGameController sharedInstance].alertManager showTurnAlertWithAlertInfo:alertInfo];
}
else
{
dataManager.player2 = [[userInfoDict objectForKey:RANDOM_STACK] objectForKey:@"uid"];
dataManager.player2_name = [[userInfoDict objectForKey:RANDOM_STACK] objectForKey:@"name"];
[dataManager createNewGameSession];
[[PWGameController sharedInstance] switchToLayerWithCode:kGameLayer];
[dataManager removeDocWithRequestId:[[idArray objectAtIndex:randomIndex] docId]];
[self removeFindingOpponentView];
}
[userInfoDict release];
userInfoDict=nil;
[idArray release];
idArray=nil;
}
else
{
[self removeFindingOpponentView];
NSDictionary *alertInfo = [NSDictionary dictionaryWithObjectsAndKeys:@"",ALERT_TITLE,@"We will notify you when we find an opponent!",ALERT_MESSAGE, nil];
[[PWGameController sharedInstance].alertManager showTurnAlertWithAlertInfo:alertInfo];
[dataManager performSelectorInBackground:@selector(addRequestToRandomStack) withObject:nil];
}
}
App42 Ecosystem for Game Developers
400+ APIs and 20+ Modules across 17+ Native SDKs
App42 - Social Gaming Features
App42 provides you a range of Social Gaming features to help increase engagement and collaboration and make your App go viral.
Users can create and manage their custom Avatars or import existing Avatars from Facebook, Twitter and LinkedIn
Users can create their own Buddy Circles through Facebook Friend List or Geo-location search
- Share files and send messages
- Invite and Challenge users
- Have Buddy-based Leaderboards
- Global/Daily/Weekly Scores and Rankings
- Global Scoreboard
- Friends Leaderboard
- Allows users to unlock levels and purchase In-App items.
- Collectable rewards like virtual currencies, discount offers and goods, hugely increase your users’ active playtime.
Enables you to link your users’ social accounts with the game.
- Facebook
- Twitter
- LinkedIn
- Push Notification Service
- Used to notify users of any event or alert.
- Greatly helps to increase User Engagement.
- App42 Push Notification has the following special features:
- It is a Cross-Platform service
- Supports Android, iOS and Windows.
HTML5 and Blackberry – coming soon
- Supports Channel-Subscription Mode
- Push Notification Scheduling
- Push Analytics
AppWarp – Multiplayer Gaming Engine
Words Junkie is a simple Turn-based Game developed using App42 Cloud APIs. In case you wish to develop a more complex game, like a Real-time Massive Multiplayer Game, you must use our Gaming Engine – AppWarp.
- Customizable Room/Lobby Logic
- Match-making API - for quick playing mode
- Custom Binary Protocol – for real-time communication
- In-Game Chat
- Locking/Unlocking Properties for Client-side Arbitration
- Connection Resiliency – to tackle intermittent network errors
In-Game Analytics
Get real-time data and insights to make important business-related decisions for your game.
- Filter, sort and analyse user data through a Pivot table
- Generate charts and publish reports
- Track your App Installations
- Get daily, weekly, monthly dose of your API Hits
- Track User Activity – Analyse User Retention
- Assess your Push Notification Campaign
- Get Geo-density graphs
Conclusion
By now you must have learnt how to make a simple Turn-based Game using App42 Cloud APIs.
The App42 Ecosystem provides you with all features you may need for any sort of development. We are a cross-platform server that supports almost all popular platforms and languages.
- Our Product portfolio includes:
- App42 Cloud API – The REST Server
- AppWarp – Multiplayer Gaming Engine
- AppHQ – The Management Console
- AppHawk–Project Management and Collaboration Tool
- AppClay – The App Builder
- App42 PaaS – Shared Cloud Containers (Upcoming Release – October)
- AppHype – Ad-Wrapper and Ad-Server(Upcoming Release – October)
In future if you need any help, App42 Ecosystem is there for you.
Have a nice day!!