Chapter 5. Creating Cool Content

In this chapter, you'll be learning how to implement the really complex, subtle game mechanics that not many developers do. This is what separates the good games from the great games. There will be many examples, tutorials, and code snippets in this chapter intended for adaption in your own projects, so feel free to come back at any time to look at something you may have either missed the first time, or are just curious to know about in general.

In this chapter, we will cover the following topics:

  • Adding a table for scores
  • Adding subtle sliding to the units
  • Creating movements on a Bézier curve instead of straight paths
  • Depth perception via device tilting (and parallax scrolling)
  • Three ways to create unit streamers or ghosts
  • Touchscreen controls versus D-pad adaptation (and why it matters so much to know this distinction)
  • A common theme for this chapter will be to show you how to take what seem like complex things and turn them into easy-to-code and easy-to-modify segments that you can implement in your own project (or projects).

Also, this chapter features things that won't go well with the book's project, and only the first two points in the preceding list are related to the game project, which has been slowly worked all this while. The rest are standalone sample projects with code designed in a modular fashion so that you can extract it for your own projects faster.

Tip

It's strongly recommended to open the Chapter 5 code before working on the first two sections. A decent amount of code has been added and/or modified since the last chapter, and it was not talked about in this book. Therefore, you may get compilation errors should you try to follow along with the book without using the Chapter 5 project code. Thanks for understanding!

Adding a table for scores

Because we want a way to show the user their past high scores, in the GameOver scene, we're going to add a table that displays the most recent high scores that are saved. For this, we're going to use CCTableView. It's still relatively new, but it works for what we're going to use it.

CCTableView versus UITableView

Although UITableView might be known to some of you who've made non-Cocos2d apps before, you should be aware of its downfalls when it comes to using it within Cocos2d. For example, if you want a BMFont in your table, you can't add LabelBMFont (you could try to convert the BMFont into a TTF font and use that within the table, but that's outside the scope of this book).

If you still wish to use a UITableView object (or any UIKit element for that matter), you can create the object like normal, and add it to the scene, like this (tblScores is the name of the UITableView object):

[[[CCDirector sharedDirector] view] addSubview:tblScores];

Saving high scores (NSUserDefaults)

Before we display any high scores, we have to make sure we save them. The easiest way to do this is by making use of Apple's built-in data preservation tool—NSUserDefaults. If you've never used it before, let me tell you that it's basically a dictionary with "save" mechanics that stores the values in the device so that the next time the user loads the device, the values are available for the app.

Also, because there are three different values we're tracking for each gameplay, let's only say a given game is better than another game when the total score is greater.

Therefore, let's create a saveHighScore method that will go through all the total scores in our saved list and see whether the current total score is greater than any of the saved scores. If so, it will insert itself and bump the rest down. In MainScene.m, add the following method:

-(NSInteger)saveHighScore
{
  //save top 20 scores
  
  //an array of Dictionaries...
  //keys in each dictionary:
  //  [DictTotalScore]
  //  [DictTurnsSurvived]
  //  [DictUnitsKilled]

//read the array of high scores saved on the user's device
  NSMutableArray *arrScores = [[[NSUserDefaults standardUserDefaults] arrayForKey:DataHighScores] mutableCopy];
  
//sentinel value of -1 (in other words, if a high score was not found on this play through)
  NSInteger index = -1;
//loop through the scores in the array
  for (NSDictionary *dictHighScore in arrScores)
  {
//if the current game's total score is greater than the score stored in the current index of the array...
    if (numTotalScore > [dictHighScore[DictTotalScore] integerValue])
    {
//then store that index and break out of the loop
      index = [arrScores indexOfObject:dictHighScore];
      break;
    }
  }
  
//if a new high score was found
  if (index > -1)
  {
//create a dictionary to store the score, turns survived, and units killed
    NSDictionary *newHighScore = @{ DictTotalScore : @(numTotalScore),
    DictTurnsSurvived : @(numTurnSurvived),
    DictUnitsKilled : @(numUnitsKilled) };
    
//then insert that dictionary into the array of high scores
    [arrScores insertObject:newHighScore atIndex:index];
    
//remove the very last object in the high score list (in other words, limit the number of scores)
    [arrScores removeLastObject];
    
//then save the array
    [[NSUserDefaults standardUserDefaults] setObject:arrScores forKey:DataHighScores];
    [[NSUserDefaults standardUserDefaults] synchronize];
  }

//finally return the index of the high score (whether it's -1 or an actual value within the array)
  return index;
}

Finally, call this method in the endGame method right before you transition to the next scene:

-(void)endGame
{
  //call the method here to save the high score, then grab the index of the high score within the array
  NSInteger hsIndex = [self saveHighScore];
  
  NSDictionary *scoreData = @{ DictTotalScore : @(numTotalScore),
  DictTurnsSurvived : @(numTurnSurvived),
  DictUnitsKilled : @(numUnitsKilled),
  DictHighScoreIndex : @(hsIndex)};
  
  [[CCDirector sharedDirector] replaceScene:[GameOverScene sceneWithScoreData:scoreData]];
  
}

Now that we have our high scores being saved, let's create the table to display them.

Creating the table

It's really simple to set up a CCTableView object. All we need to do is modify the contentSize object, and then put in a few methods that handle the size and content of each cell.

So first, open the GameOverScene.h file and set the scene as a data source for the CCTableView:

@interface GameOverScene : CCScene <CCTableViewDataSource>

Then, in the initWithScoreData method, create the header labels as well as initialize the CCTableView:

//get the high score array from the user's device
arrScores = [[NSUserDefaults standardUserDefaults] arrayForKey:DataHighScores];
    
//create labels
CCLabelBMFont *lblTableTotalScore = [CCLabelBMFont labelWithString:@"Total Score:" fntFile:@"bmFont.fnt"];

CCLabelBMFont *lblTableUnitsKilled = [CCLabelBMFont labelWithString:@"Units Killed:" fntFile:@"bmFont.fnt"];

CCLabelBMFont *lblTableTurnsSurvived = [CCLabelBMFont labelWithString:@"Turns Survived:" fntFile:@"bmFont.fnt"];
  
//position the labels
lblTableTotalScore.position = ccp(winSize.width * 0.5, winSize.height * 0.85);
lblTableUnitsKilled.position = ccp(winSize.width * 0.675, winSize.height * 0.85);
lblTableTurnsSurvived.position = ccp(winSize.width * 0.875, winSize.height * 0.85);
  
//add the labels to the scene
[self addChild:lblTableTurnsSurvived];
[self addChild:lblTableTotalScore];
[self addChild:lblTableUnitsKilled];
  
//create the tableview and add it to the scene
CCTableView * tblScores = [CCTableView node];
tblScores.contentSize = CGSizeMake(0.6, 0.4);
CGFloat ratioX = (1.0 - tblScores.contentSize.width) * 0.75;
CGFloat ratioY = (1.0 - tblScores.contentSize.height) / 2;
tblScores.position = ccp(winSize.width * ratioX, winSize.height * ratioY);
tblScores.dataSource = self;
tblScores.block = ^(CCTableView *table){
    //if the press a cell, do something here.
    //NSLog(@"Cell %ld", (long)table.selectedRow);
};
[self addChild: tblScores]; 

With the CCTableView object's data source being set to self we can add the three methods that will determine exactly how our table looks and what data goes in each cell (that is, row).

Note

Note that if we don't set the data source, the table view's method will not be called; and if we set it to anything other than self, the methods will be called on that object/class instead.

That being said, add these three methods:

-(CCTableViewCell*)tableView:(CCTableView *)tableView nodeForRowAtIndex:(NSUInteger)index
{
  CCTableViewCell* cell = [CCTableViewCell node];
  
  cell.contentSizeType = CCSizeTypeMake(CCSizeUnitNormalized, CCSizeUnitPoints);
  cell.contentSize = CGSizeMake(1, 40);
  
  // Color every other row differently
  CCNodeColor* bg;
  if (index % 2 != 0) bg = [CCNodeColor nodeWithColor:[CCColor colorWithRed:0 green:0 blue:0 alpha:0.3]];
  else bg = [CCNodeColor nodeWithColor: [CCColor colorWithRed:0 green:0 blue:0 alpha:0.2]];
  
  bg.userInteractionEnabled = NO;
  bg.contentSizeType = CCSizeTypeNormalized;
  bg.contentSize = CGSizeMake(1, 1);
  [cell addChild:bg];
  return cell;
}

-(NSUInteger)tableViewNumberOfRows:(CCTableView *)tableView
{
  return [arrScores count];
}

-(float)tableView:(CCTableView *)tableView heightForRowAtIndex:(NSUInteger)index
{
  return 40.f;
}

The first method, tableView:nodeForRowAtIndex:, will format each cell based on which index it is. For now, we're going to color each cell in one of two different colors.

The second method, tableViewNumberOfRows:, returns the number of rows, or cells, that will be in the table view. Since we know there are going to be 20, we can technically type 20, but what if we decide to change that number later? So, let's stick with using the count of the array.

The third method, tableView:heightForRowAtIndex:, is meant to return the height of the row, or cell, at the given index. Since we aren't doing anything different with any cell in particular, we can hardcode this value to a fairly reasonable height of 40.

At this point, you should be able to run the game, and when you lose, you'll be taken to the game over screen with the labels across the top as well as a table that scrolls on the right side of the screen.

Tip

It's good practice when learning Cocos2d to just mess around with stuff to see what sort of effects you can make. For example, you could try using some ScaleTo actions to scale the text up from 0, or use a MoveTo action to slide it from the bottom or the side.

Feel free to see whether you can create a cool way to display the text right now.

Now that we have the table in place, let's get the data displayed, shall we?

Showing the scores

Now that we have our table created, it's a simple addition to our code to get the proper numbers to display correctly.

In the nodeForRowAtIndex method, add the following block of code right after adding the background color to the cell:

//Create the 4 labels that will be used within the cell (row). 
CCLabelBMFont *lblScoreNumber = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d)", index+1] fntFile:@"bmFont.fnt"];
//Set the anchor point to the middle-right (default middle-middle)
lblScoreNumber.anchorPoint = ccp(1,0.5);
  
CCLabelBMFont *lblTotalScore = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictTotalScore] integerValue]] fntFile:@"bmFont.fnt"];

CCLabelBMFont *lblUnitsKilled = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictUnitsKilled] integerValue]] fntFile:@"bmFont.fnt"];

CCLabelBMFont *lblTurnsSurvived = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d", [arrScores[index][DictTurnsSurvived] integerValue]] fntFile:@"bmFont.fnt"];
  
//set the position type of each label to normalized (where (0,0) is the bottom left of its parent and (1,1) is the top right of its parent)
lblScoreNumber.positionType = lblTotalScore.positionType = lblUnitsKilled.positionType = lblTurnsSurvived.positionType = CCPositionTypeNormalized;

//position all of the labels within the cell
lblScoreNumber.position = ccp(0.15,0.5);
lblTotalScore.position = ccp(0.35,0.5);
lblUnitsKilled.position = ccp(0.6,0.5);
lblTurnsSurvived.position = ccp(0.9,0.5);
  
//if the index we're iterating through is the same index as our High Score index...
if (index == highScoreIndex)
{
//then set the color of all the labels to a golden color
    lblScoreNumber.color =
    lblTotalScore.color =
    lblUnitsKilled.color =
    lblTurnsSurvived.color = [CCColor colorWithRed:1 green:183/255.f blue:0];
}
  
//add all of the labels to the individual cell
[cell addChild:lblScoreNumber];
[cell addChild:lblTurnsSurvived];
[cell addChild:lblTotalScore];
[cell addChild:lblUnitsKilled];

And that's it! When you play the game and end up at the game over screen, you'll see the high scores being displayed (even the scores from earlier attempts, because they were saved, remember?). Notice the high score that is yellow. It's an indication that the score you got in the game you just played is on the scoreboard, and shows you where it is.

Although the CCTableView might feel a bit weird with things disappearing and reappearing as you scroll, we'll cover how to make that better in the next chapter on polishing our game. For now, let's get some Threes!—like sliding into our game.

Tip

If you're considering adding a CCTableView to your own project, the key takeaway here is to make sure you modify the contentSize and position properly. By default, the contentSize is a normalized CGSize, so from 0 to 1, and the anchor point is (0,0).

Plus, make sure you perform these two steps:

  • Set the data source of the table view
  • Add the three table view methods

With all that in mind, it should be relatively easy to implement a CCTableView.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.32.1