This is the fourth installment of our Cocos2D tutorial series on cloning Centipede for iOS. Today we will be writing the basic AI needed for the Caterpillar object. Let's get started!
Where We Left Off
This is the fourth installment of our Cocos2D tutorial series on cloning Centipede for iOS. Make sure you have completed the previous parts before beginning.
In the previous tutorial, I showed you how to create the caterpillar and segment objects. You learned how to move the head of the caterpillar and have each of the segments follow behind.
Today, we will be discussing the basic AI of the caterpillar. I will show you how the caterpillar can interact with the environment and respond to obstacles such as walls and sprouts. By the end of this tutorial, your caterpillar will be able to fully navigate any environment that was generated in part 2.
Step 1: Remove Our Test Code
In the last tutorial, we put in some simple code to drive the caterpillar forward. This was just temporary and is no longer needed. So, open up Caterpillar.m and remove the following line from the update:
method:
x += kGridCellSize;
We will be modifying the x and y variables shortly.
Step 2: The Collision Method
It turns out there is a block of code that gets reused every time a collision occurs. A collision is any time the caterpillar's head node intercepts the right wall, left wall, top wall, bottom wall, sprout, or player. The goal of the collision method is to adjust the caterpillars new state accordingly.
Before I show you the collision method, let's discuss the caterpillar's rules based on the original game.
- The caterpillar continues in its current direction until an obstacle is in its way.
- If the caterpillar is moving down and collides with a wall or sprout, its next moves are one space down and one space in the opposite direction.
- If the caterpillar is moving up and collides with a wall or sprout, its next moves are one space up and one space in the opposite direction.
- If the caterpillar collides with the bottom row, its overall direction gets changed to up
- If the caterpillar collides with the top row, its overall direction gets changed to down.
With these rules in mind, let's implement the collision:
method. Open Caterpillar.m file and add the following method declaration to our private class interface as well as add the implementation:
@interface Caterpillar(Private) - (void) collision; @end
And the implementation...
- (void) collision { // 1 BOOL down = self.currentState == CSDownLeft || self.currentState == CSDownRight || self.previousState == CSDownLeft || self.previousState == CSDownRight; // 2 self.previousState = self.currentState; // 3 if(down) { if(self.currentState == CSRight) { self.currentState = CSDownLeft; } else { self.currentState = CSDownRight; } } else { if(self.currentState == CSRight) { self.currentState = CSUpLeft; } else { self.currentState = CSUpRight; } } }
- The first step here is to determine which direction the caterpillar is traveling overall (up or down). We can determine this if the caterpillar's current or previous state is one of the "Down" states.
- The next step is to set the previous state to the current state. This will be useful when we start moving the caterpillar according to its states.
- Finally, we determine if we should be in an Up Left/Right state or a Down Left/Right state and set it.
Now that we know how to change state when a collision has occurred, let's dive into the actual collision detection and logic to move the caterpillar.
Step 3: Caterpillar Movement And Wall Collisions
Since we have basically implemented our Caterpillar as one big state machine, this part will end up being one large switch statement. The cases of the statement will be each of the possible states of the caterpillar. In the code below, we are going to start with the most common states: CSRight
and CSLeft
. Add the following code to the update:
method inside of your Caterpillar.m class. Make sure you add it before the line self.position = ccp(x,y);
.
switch (self.currentState) { case CSRight: // 1 if(x + kGridCellSize >= kGameAreaStartX + kGameAreaWidth) { // 2 if(y - kGridCellSize <= kGameAreaStartY) { self.previousState = CSUpLeft; self.currentState = CSRight; } else if(y >= kGameAreaStartY + kGameAreaHeight - kGridCellSize) { // 3 self.previousState = CSDownRight; self.currentState = CSRight; } else { // 4 if(self.previousState == CSDownRight || self.previousState == CSDownLeft) { self.currentState = CSDownLeft; } else { self.currentState = CSUpLeft; } self.previousState = CSRight; [self update:4.0]; return; } // 5 [self collision]; } else { // 6 x = x + kGridCellSize; } break; case CSLeft: // Check for a wall collision if(x <= kGameAreaStartX) { if(y - kGridCellSize <= kGameAreaStartY) { self.previousState = CSUpRight; self.currentState = CSLeft; } else if(y >= kGameAreaStartY + kGameAreaHeight) { // Top collision self.previousState = CSDownLeft; self.currentState = CSLeft; } else { // Left wall collision if(self.previousState == CSDownRight || self.previousState == CSDownLeft) { self.currentState = CSDownRight; } else { self.currentState = CSUpRight; } self.previousState = CSLeft; [self update:4.0]; return; } [self collision]; } else { x = x - kGridCellSize; } break; }
Since these cases are symmetrical, I'm only going to explain CSRight.
- We first check to see if the caterpillar collided with the right edge. If so, we need to see where it's at in order to determine what to do next.
- This first check determines if we are at the bottom of the screen, if so we simply head the other direction and set the previous position to an "up" direction to send the caterpillar back up.
- This does the opposite of #2 and checks if the caterpillar is at the top of the screen.
- This is the base state of this case. It triggers when you hit the side wall and are neither at the top nor the bottom. If so, we set the previous state and call this method again. The reason we call the method again is we don't want to continue moving the caterpillar in the current direction (otherwise it would go one segment too far through the wall).
- Call the collision method to set up our next/previous positions.
- Finally, if there is no collision, move the the caterpillar forward.
If you run the code at this point, the caterpillar will move forward until it hits the right wall. It will stop and the rest of the body will follow. Let's implement the remaining states in order to respond to the other state changes.
// ... case CSDownLeft: // 1 if(self.moveCount == 0) { y = y - kGridCellSize; self.moveCount++; } else { // 2 x = x - kGridCellSize; self.moveCount = 0; self.currentState = CSLeft; self.previousState = CSDownLeft; } break; case CSDownRight: if(self.moveCount == 0) { y = y - kGridCellSize; self.moveCount++; } else { x = x + kGridCellSize; self.moveCount = 0; self.currentState = CSRight; self.previousState = CSDownRight; } break; case CSUpRight: if(self.moveCount == 0) { y = y + kGridCellSize; self.moveCount++; } else { x = x + kGridCellSize; self.moveCount = 0; self.currentState = CSRight; self.previousState = CSUpRight; } break; case CSUpLeft: if(self.moveCount == 0) { y = y + kGridCellSize; self.moveCount++; } else { x = x - kGridCellSize; self.moveCount = 0; self.currentState = CSLeft; self.previousState = CSUpLeft; } break; default: break;
Like we did with the last section, I'm only going to walk you through one of the cases as they are all similar.
- All of these states require 2 steps in order to operate. The first step is to go down, the next is to switch direction. So the
moveCount
property will either be 0 or 1. A value of 0 will cause the state to move down and a value of 1 will cause it to move forward. - At this stage we reset to either left or right and then reset the
moveCount
property. Basically, continuing as usual.
Now run the application. The caterpillar should now traverse the entire level going up and down, however it completely ignores the sprouts at this stage.
Step 4: Collision With Sprouts
Since we already have our state machine in place, sprout collision becomes very simple. It's only a matter of checking the head segment's bounds against the bounds of each of the sprouts on the screen. Open Caterpillar.m and add the following code right before the switch statement:
// 1 CGRect caterpillarBounds = [self getBounds]; // 2 [self.gameLayer.sprouts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { Sprout *sprout = (Sprout *)obj; CGRect sproutBounds = [sprout getBounds]; // 3 if(self.currentState == CSRight) { CGRect rightBounds = caterpillarBounds; rightBounds.origin.x = rightBounds.origin.x + kGridCellSize; if(CGRectIntersectsRect(rightBounds, sproutBounds)) { [self collision]; *stop = YES; } } // 4 if(self.currentState == CSLeft) { CGRect leftBounds = caterpillarBounds; leftBounds.origin.x = leftBounds.origin.x - kGridCellSize; if(CGRectIntersectsRect(leftBounds, sproutBounds)) { [self collision]; *stop = YES; } } }];
- We start by getting the caterpillar bounds. In this case our caterpillar position is based on the head segment so this method will just return the bounds of the head segment.
- On every
update:
call for the caterpillar we enumerate all of the sprout objects to check if there was a collision. - Check to see if the caterpillar collided with a sprout on the right.
- Check to see if the caterpillar collided with a sprout on the left.
If there was a collision, we call the collision method to update the state and set the *stop
variable to true to discontinue the enumeration.
Now, when you run the application the caterpillar should completely navigate the sprout field from top to bottom and back yet again.
Conclusion
Now that we have a fully functional caterpillar, the next step will be to add the player interaction. In the next tutorial in the series, I will show you how to add a new interaction layer that will control the player object and move its square within a confined area. We will also see how the player can fire a constant stream of missiles.
Happy Coding!
Comments