How to build an Asynchronous
Turn-Based Game using
App42 Cloud APIs
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 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.
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.
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.
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.
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:
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:
All are implemented in PWMenuLayer.m class which is detailed in the later part.
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:
If the touch is accepted then the above method:
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:
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:
The play Clicked method:
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.
openssl x509 -in developer_identity.cer -inform DER -out developer_identity.pem -outform PEM
openssl pkcs12 -nocerts -in yourPrivateKey.p12 -out yourPrivateKey.pem
openssl pkcs12 -export -inkeyyourPrivateKey.pem -in developer_identity.pem -out iphone_dev.p12
Where,
- developer_identity.cer <= certificate you downloaded from Apple
- yourPrivateKey.p12 <= your private key
-(BOOL)applica
tion:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{ // Let the device know we want to receive push notifications
[[UIApplicationsharedApplication]registerForRemoteNotificationTypes:
(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound |
UIRemoteNotificationTypeAlert)]; returnYES;
}
(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); } - (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { NSLog(@"Failed to get token, error: %@", error); }
When your App registers for remote notifications, it tries to obtain a “device token”. This is a 32-byte number that uniquely identifies your device. A device token is the address to which a push notification will be delivered.
- (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]; } }
-(void)sendPush:(NSString*)message { ServiceAPI *serviceObj = [[ServiceAPIalloc]init]; serviceObj.apiKey = APP42_APP_KEY; serviceObj.secretKey = APP42_SECRET_KEY; PushNotificationService *pushObj = [serviceObjbuildPushService]; @try { NSMutableDictionary *pushDictionary = [NSMutableDictionarydictionary]; [pushDictionarysetObject:messageforKey:@"alert"]; [pushDictionarysetObject:@"default"forKey:@"sound"]; [pushDictionarysetObject:@"1"forKey:@"badge"]; PushNotification *pushNotification = [pushObjsendPushMessageToUser:@"User
Name"withMessageDictionary: pushDictionary]; [pushNotificationrelease]; } @catch (App42Exception *exception) { NSLog(@"%@",exception.reason); } @finally { [serviceObj release]; [pushObj release]; } }
The pushDictionary in the above code should always follow the same structure as mentioned above to deliver the push notification successfully using App42 Server. You can remove or add items to the pushDictionary if needed as per Apple guidelines.
- (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]; }
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.
-(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.
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]; } }
400+ APIs and 20+ Modules across 17+ Native SDKs
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
Enables you to link your users’ social accounts with the game.
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.
Get real-time data and insights to make important business-related decisions for your game.
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.
In future if you need any help, App42 Ecosystem is there for you.
Have a nice day!!