25 min read

In this article by Alex Ogorek, author of the book Mastering Cocos2d Game Development 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 article 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 article, 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

(For more resources related to this topic, see here.)

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 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.

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, let’s get some Threes!—like sliding into our game.

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.

Adding subtle sliding to the units

If you’ve ever played Threes! (or if you haven’t, check out the trailer at http://asherv.com/threes/, and maybe even download the game on your phone), you would be aware of the sliding feature when a user begins to make “their move but hasn’t yet completed the move. At the speed of the dragging finger, the units slide in the direction they’re going to move, showing the user where each unit will go and how each unit will combine with another.

This is useful as it not only adds that extra layer of “cool factor” but also provides a preview of the future for the user if they want to revert their decision ahead of time and make a different, more calculated move.

Here’s a side note: if you want your game to go really viral, you have to make the user believe it was their fault that they lost, and not your “stupid game mechanics” (as some players might say).

Think Angry Birds, Smash Hit, Crossy Road, Threes!, Tiny Wings… the list goes on and on with more games that became popular, and all had one underlying theme: when the user loses, it was entirely in their control to win or lose, and they made the wrong move.

This” unseen mechanic pushes players to play again with a better strategy in mind. And this is exactly why we want our users to see their move before it gets made. It’s a win-win situation for both the developers and the players.

Sliding one unit

If we can get one unit to slide, we can surely get the rest of the units to slide by simply looping through them, modularizing the code, or some other form of generalization.

That being said, we need to set up the Unit class so that it can detect how far “the finger has dragged. Thus, we can determine how far to move the unit. So, “open Unit.h and add the following variable. It will track the distance from the previous touch position:

@property (nonatomic, assign) CGPoint previousTouchPos;

Then, in the touchMoved method of Unit.m, add the following assignment to previousTouchPos. It sets the previous touch position to the touch-down position, but only after the distance is greater than 20 units:

if (!self.isBeingDragged && ccpDistance(touchPos, self.touchDownPos) > 20)
{
self.isBeingDragged = YES;
//add it here:
self.previousTouchPos = self.touchDownPos;

Once that’s in place, we can begin calculating the distance while the finger is being dragged. To do that, we’ll do a simple check. Add the following block of code at the end of touchMoved, after the end of the initial if block:

//only if the unit is currently being dragged
if (self.isBeingDragged)
{
   CGFloat dist = 0;
   //if the direction the unit is being dragged is either UP or "     DOWN
   if (self.dragDirection == DirUp || self.dragDirection == DirDown)
   //then subtract the current touch position's Y-value from the "     
previously-recorded Y-value to determine the distance to "     move
     dist = touchPos.y - self.previousTouchPos.y;
     //else if the direction the unit is being dragged is either "       
LEFT or RIGHT
   else if (self.dragDirection == DirLeft ||
       self.dragDirection == DirRight)
       //then subtract the current touch position's Y-value from "         
the previously-recorded Y-value to determine the "         distance to move
     dist = touchPos.x - self.previousTouchPos.x;
 
//then assign the touch position for the next iteration of touchMoved to work properly
self.previousTouchPos = touchPos;
 
}

The “assignment of previousTouchPos at the end will ensure that while the unit is being dragged, we continue to update the touch position so that we can determine the distance. Plus, the distance is calculated in only the direction in which the unit is being dragged (up and down are denoted by Y, and left and right are denoted by X).

Now that we have the distance between finger drags being calculated, let’s push “this into a function that will move our unit based on which direction it’s being dragged in. So, right after you’ve calculated dist in the previous code block, “call the following method to move our unit based on the amount dragged:

dist /= 2; //optional
[self slideUnitWithDistance:dist "withDragDirection:self.dragDirection];

Dividing the distance by 2 is optional. You may think the squares are too small, and want the user to be able to see their square. So note that dividing by 2, or a larger number, will mean that for every 1 point the finger moves, the unit will move by 1/2 (or less) points.

With that method call being ready, we need to implement it, so add the following method body for now. Since this method is rather complicated, it’s going to be “added in parts:

-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir
{
}

The first thing “we need to do is set up a variable to calculate the new x and y positions of the unit. We’ll call these newX and newY, and set them to the unit’s current position:

CGFloat newX = self.position.x, newY = self.position.y;

Next, we want to grab the position that the unit starts at, that is, the position the “unit would be at if it was positioned at its current grid coordinate. To do that, “we’re going to call the getPositionForGridCoordinate method from MainScene, (since that’s where the positions are being calculated anyway, we might as well use that function):

CGPoint originalPos = [MainScene "getPositionForGridCoord:self.gridPos];

Next, we’re going to move the newX or newY based on the direction in which the unit is being dragged. For now, let’s just add the up direction:

if (self.dragDirection == DirUp)
{
   newY += dist;
   if (newY > originalPos.y + self.gridWidth)
     newY = originalPos.y + self.gridWidth;
   else if (newY < originalPos.y)
     newY = originalPos.y;
}

In this if block, we’re first going to add the distance to the newY variable “(because we’re going up, we’re adding to Y instead of X). Then, we want to “make sure the position is at most 1 square up. We’re going to use the gridWidth (which is essentially the width of the square, assigned in the initCommon method). Also, we need to make sure that if they’re bringing the square back to its original position, it doesn’t go into the square beneath it.

So let’s add the rest of the directions as else if statements:

else if (self.dragDirection == DirDown)
{
   newY += dist;
   if (newY < originalPos.y - self.gridWidth)
     newY = originalPos.y - self.gridWidth;
   else if (newY > originalPos.y)
     newY = originalPos.y;
}
else if (self.dragDirection == DirLeft)
{
   newX += dist;
   if (newX < originalPos.x - self.gridWidth)
     newX = originalPos.x - self.gridWidth;
   else if (newX > originalPos.x)
     newX = originalPos.x;
}
else if (self.dragDirection == DirRight)
{
   newX += dist;
   if (newX > originalPos.x + self.gridWidth)
     newX = originalPos.x + self.gridWidth;
   else if (newX < originalPos.x)
     newX = originalPos.x;
}

Finally, we “will set the position of the unit based on the newly calculated “x and y positions:

self.position = ccp(newX, newY);

Running the game at this point should cause the unit you drag to slide along “with your finger. Nice, huh? Since we have a function that moves one unit, “we can very easily alter it so that every unit can be moved like this.

But first, there’s something you’ve probably noticed a while ago (or maybe just recently), and that’s the unit movement being canceled only when you bring your finger back to the original touch down position. Because we’re dragging the unit itself, we can “cancel” the move by dragging the unit back to where it started. However, the finger might be in a completely different position, so we need to modify how the cancelling gets determined.

To do that, in your touchEnded method of Unit.m, locate this if statement:

if (ccpDistance(touchPos, self.touchDownPos) > "self.boundingBox.size.width/2)

Change it to the following, which will determine the unit’s distance, and not the finger’s distance:

CGPoint oldSelfPos = [MainScene "getPositionForGridCoord:self.gridPos];
 
CGFloat dist = ccpDistance(oldSelfPos, self.position);
if (dist > self.gridWidth/2)

Yes, this means you no longer need the touchPos variable in touchEnded if “you’re getting that “warning and wish to get rid of it. But that’s it for sliding 1 unit. Now we’re ready to slide all the units, so let’s do it!

Sliding all units

Now “that we have the dragging unit being slid, let’s continue and make all the units slide (even the enemy units so that we can better predict our troops’ movement).

First, we need a way to move all the units on the screen. However, since the Unit class only contains information about the individual unit (which is a good thing), “we need to call a method in MainScene, since that’s where the arrays of units are.

Moreover, we cannot simply call [MainScene method], since the arrays are “instance variables, and instance variables must be accessed through an instance “of the object itself.

That being said, because we know that our unit will be added to the scene as “a child, we can use Cocos2d to our advantage, and call an instance method on the MainScene class via the parent parameter. So, in touchMoved of Unit.m, make the following change:

[(MainScene*)self.parent slideAllUnitsWithDistance:dist "withDragDirection:self.dragDirection];
//[self slideUnitWithDistance:dist "withDragDirection:self.dragDirection];

Basically we’ve commented out (or deleted) the old method call here, and instead called it on our parent object (which we cast as a MainScene so that we know “which functions it has).

But we don’t have that method created yet, so in MainScene.h, add the following method declaration:

-(void)slideAllUnitsWithDistance:(CGFloat)dist "withDragDirection:(enum UnitDirection)dir;

Just in case you haven’t noticed, the enum UnitDirection is declared in Unit.h, which is why MainScene.h imports Unit.h—so that we can make use of that enum in this class, and the function to be more specific.

Then in MainScene.m, we’re going to loop through both the friendly and enemy arrays, and call the slideUnitWithDistance function on each individual unit:

-(void)slideAllUnitsWithDistance:(CGFloat)dist "withDragDirection:(enum UnitDirection)dir
{
for (Unit *u in arrFriendlies)
   [u slideUnitWithDistance:dist withDragDirection:dir];
for (Unit *u in arrEnemies)
   [u slideUnitWithDistance:dist withDragDirection:dir];
}

However, that” still isn’t functional, as we haven’t declared that function in the “header file for the Unit class. So go ahead and do that now. Declare the function header in Unit.h:

-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum "UnitDirection)dir;

We’re almost done.

We initially set up our slideUnitWithDistance method with a drag direction in mind. However, only the unit that’s currently being dragged will have a drag direction. Every other unit will need to use the direction it’s currently facing “(that is, the direction in which it’s already going).

To do that, we just need to modify how the slideUnitWithDistance method does its checking to determine which direction to modify the distance by.

But first, we need to handle the negatives. What does that mean? Well, if you’re dragging a unit to the left and a unit being moved is supposed to be moving to the left, it will work properly, as x-10 (for example) will still be less than the grid’s width. However, if you’re dragging left and a unit being moved is supposed to be moving right, it won’t be moving at all, as it tries to add a negative value x -10, but because it needs to be moving to the right, it’ll encounter the left-bound right away (of less than the original position), and stay still.

The following diagram should help explain what is meant by “handling negatives.” As you can see, in the top section, when the non-dragged unit is supposed to be going left by 10 (in other words, negative 10 in the x direction), it works. But when the non-dragged unit is going the opposite sign (in other words, positive 10 in the x direction), it doesn’t.

Mastering Cocos2d Game Development

To” handle this, we set up a pretty complicated if statement. It checks when the drag direction and the unit’s own direction are opposite (positive versus negative), and multiplies the distance by -1 (flips it).

Add this to the top of the slideUnitWithDistance method, right after you grab the newX and the original position:

-(void)slideUnitWithDistance:(CGFloat)dist withDragDirection:(enum UnitDirection)dir
{
CGFloat newX = self.position.x, newY = self.position.y;
CGPoint originalPos = [MainScene getPositionForGridCoord:self.gridPos];
if (!self.isBeingDragged &&
 
(((self.direction == DirUp || self.direction == DirRight) &&
(dir == DirDown || dir == DirLeft)) ||
 
((self.direction == DirDown || self.direction == DirLeft) &&
(dir == DirUp || dir == DirRight))))
{
   dist *= -1;
}
}

The logic of this if statement works is as follows:

Suppose the unit is not being dragged. Also suppose that either the direction is positive and the drag direction is negative, or the direction is negative and the drag direction is positive. Then multiply by -1.

Finally, as mentioned earlier, we just need to handle the non-dragged units. So, in every if statement, add an “or” portion that will check for the same direction, but only if the unit is not currently being dragged. In other words, in the slideUnitWithDistance method, modify your if statements to look like this:

if (self.dragDirection == DirUp || (!self.isBeingDragged && self.direction == DirUp))
{}
else if (self.dragDirection == DirDown || (!self.isBeingDragged && self.direction == DirDown))
{}
else if (self.dragDirection == DirLeft || (!self.isBeingDragged && self.direction == DirLeft))
{}
else if (self.dragDirection == DirRight || (!self.isBeingDragged && self.direction == DirRight))
{}

Finally, we can run the game. Bam! All the units go gliding across the screen with our drag. Isn’t it lovely? Now the player can better choose their move.

That’s it for the sliding portion.

The key to unit sliding is to loop through the arrays to ensure that all the units get moved by an equal amount, hence passing the distance to the move function.

Creating movements on a Bézier curve

If you don’t know what a Bézier curve is, it’s basically a line that goes from point A to point B over a curve. Instead of being a straight line with two points, it uses a second set of points called control points that bend the line in a smooth way. When you want to apply movement with animations in Cocos2d, it’s very tempting to queue up a bunch of MoveTo actions in a sequence. However, it’s going to look a lot nicer ( in both the game and the code) if you use a smoother Bézier curve animation.

Here’s a good example of what a Bézier curve looks like:

Mastering Cocos2d Game Development

As you can see, the red line goes from point P0 to P3. However, the line is influenced in the direction of the control points, P1 and P2.

Examples of using a Bézier curve

Let’s list” a few examples where it would be a good choice to use a Bézier curve instead of just the regular MoveTo or MoveBy actions:

  • A character that will perform a jumping animation, for example, in Super Mario Bros
  • A boomerang as a weapon that the player throws
  • Launching a missile or rocket and giving it a parabolic curve
  • A tutorial hand that indicates a curved path the user must make with their finger
  • A skateboarder on a half-pipe ramp (if not done with Chipmunk)

There are obviously a lot of other examples that could use a Bézier curve for their movement. But let’s actually code one, shall we?

Sample project – Bézier map route

First, to make things go a lot faster—as this isn’t going to be part of the book’s project—simply download the project from the code repository or the website.

If you open the project and run it on your device or a simulator, you will notice a blue screen and a square in the bottom-left corner. If you tap anywhere on the screen, you’ll see the blue square make an M shape ending in the bottom-right corner. If you hold your finger, it will repeat. Tap again and the animation will reset.

Imagine the path this square takes is over a map, and indicates what route a player will travel with their character. This is a very choppy, very sharp path. Generally, paths are curved, so let’s make one that is!

Here is a screenshot that shows a very straight path of the blue square:

Mastering Cocos2d Game Development

The following screenshot shows the Bézier path of the yellow square:

Mastering Cocos2d Game Development

Curved M-shape

Open MainScene.h and add another CCNodeColor variable, named unitBezier:

CCNodeColor *unitBezier;

Then open MainScene.m and add the following code to the init method so that your yellow block shows up on the screen:

unitBezier = [[CCNodeColor alloc] initWithColor:[CCColor colorWithRed:1 green:1 blue:0] width:50 height:50];
[self addChild:unitBezier];
CCNodeColor *shadow2 = [[CCNodeColor alloc] initWithColor:[CCColor blackColor] width:50 height:50];
shadow2.anchorPoint = ccp(0.5,0.5);
shadow2.position = ccp(26,24);
shadow2.opacity = 0.5;
[unitBezier addChild:shadow2 z:-1];

Then, in the sendFirstUnit method, add the lines of code that will reset the yellow block’s position as well as queue up the method to move the yellow block:

-(void)sendFirstUnit
{
unitRegular.position = ccp(0,0);
//Add these 2 lines
unitBezier.position = ccp(0,0);
[self scheduleOnce:@selector(sendSecondUnit) delay:2];
CCActionMoveTo *move1 = [CCActionMoveTo actionWithDuration:0.5 
"position:ccp(winSize.width/4, winSize.height * 0.75)];
CCActionMoveTo *move2 = [CCActionMoveTo actionWithDuration:0.5 
"position:ccp(winSize.width/2, winSize.height/4)];
CCActionMoveTo *move3 = [CCActionMoveTo actionWithDuration:0.5 
"position:ccp(winSize.width*3/4, winSize.height * 0.75)];
CCActionMoveTo *move4 = [CCActionMoveTo actionWithDuration:0.5 
"position:ccp(winSize.width - 50, 0)];
[unitRegular runAction:[CCActionSequence actions:move1, move2, "move3, move4, nil]];
}

After this, you’ll need to actually create the sendSecondUnit method, like this:

-(void)sendSecondUnit
{
ccBezierConfig bezConfig1;
bezConfig1.controlPoint_1 = ccp(0, winSize.height);
bezConfig1.controlPoint_2 = ccp(winSize.width*3/8, "winSize.height);
bezConfig1.endPosition = ccp(winSize.width*3/8, "winSize.height/2);
CCActionBezierTo *bez1 = [CCActionBezierTo "actionWithDuration:1.0 bezier:bezConfig1];
ccBezierConfig bezConfig2;
bezConfig2.controlPoint_1 = ccp(winSize.width*3/8, 0);
bezConfig2.controlPoint_2 = ccp(winSize.width*5/8, 0);
bezConfig2.endPosition = ccp(winSize.width*5/8, winSize.height/2);
CCActionBezierBy *bez2 = [CCActionBezierTo "actionWithDuration:1.0 bezier:bezConfig2];
ccBezierConfig bezConfig3;
bezConfig3.controlPoint_1 = ccp(winSize.width*5/8, "winSize.height);
bezConfig3.controlPoint_2 = ccp(winSize.width, winSize.height);
bezConfig3.endPosition = ccp(winSize.width - 50, 0);
CCActionBezierTo *bez3 = [CCActionBezierTo "actionWithDuration:1.0 bezier:bezConfig3];
[unitBezier runAction:[CCActionSequence actions:bez1, bez2,
bez3, nil]];
}

The preceding method creates three Bézier configurations and attaches them to a MoveTo command that takes a Bézier configuration. The reason for this is that each Bézier configuration can take only two control points. As you can see in this marked-up screenshot, where each white and red square represents a control point, you can make only a U-shaped parabola with a single Bézier configuration.

Thus, to make three U-shapes, you need three Bézier configurations.

Mastering Cocos2d Game Development

Finally, make sure that in the touchBegan method, you make the unitBezier stop all its actions (that is, stop on reset):

[unitBezier stopAllActions];

And that’s it! When you run the project and tap on the screen (or tap and hold), you’ll see the blue square M-shape its way across, followed by the yellow square in its squiggly M-shape.

If you” want to adapt the Bézier MoveTo or MoveBy actions for your own project, you should know that you can create only one U-shape with each Bézier configuration. They’re fairly easy to implement and can quickly be copied and pasted, as shown in the sendSecondUnit function.

Plus, as the control points and end position are just CGPoint values, they can be relative (that is, relative to the unit’s current position, the world’s position, or an enemy’s position), and as a regular CCAction, they can be run with any CCNode object quite easily.

Summary

In this article, you learned how to do a variety of things, from making a score table and previewing the next move, to making use of Bézier curves.

The code was built with a copy-paste mindset, so it can be adapted for any project without much reworking (if it is required at all).

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here