Thumping Moles for Fun

0
83
22 min read

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

The project is…

In this article, we will be building a mole thumping game. Inspired by mechanical games of the past, we will build molehills on the screen and randomly cause animated moles to pop their heads out. The player taps them to score. Simple in concept, but there are a few challenging design considerations in this deceptively easy game. To make this game a little unusual, we will be using a penguin instead of a mole for the graphics, but we will continue to use the mole terminology throughout, since a molehill is easier to consider than a penguin-hill.

Design approach

Before diving into the code, let’s start with a discussion of the design of the game. First, we will need to have molehills on the screen. To be aesthetically pleasing, the molehills will be in a 3 x 4 grid. Another approach would be to use random molehill positions, but that doesn’t really work well on the limited screen space of the iPhone.Moles will randomly spawn from the molehills. Each mole will rise up, pause, and drop down. We will need touch handling to detect when a mole has been touched, and that mole will need to increase the player’s score and then go away.

How do we make the mole come up from underground? If we assume the ground is a big sprite with the molehills drawn on it, we would need to determine where to make the “slot” from which the mole emerges, and somehow make the mole disappear when it is below that slot. One approach is to adjust the size of the mole’s displayed frame by clipping the bottom of the image so that the part below the ground is not visible. This needs to be done as a part of every update cycle for every mole for the entire game. From a programming standpoint this will work, but you may experience performance issues. Another consideration is that this usually means the hole in the molehill will always appear to be a straight-edged hole, if we trim the sprite with a straight line. This lacks the organic feel we want for this game.

The approach we will take is to use Z-ordering to trick the eye into seeing a flat playfield when everything is really on staggered Z-orders. We will create a “stair step” board, with multiple “sandwiches” of graphics for every row of molehills on the board.

For each “step” of the “stair step”, we have a sandwich of Z-ordered elements in this order, from back to front: molehill top, mole, ground, and molehill bottom. We need to have everything aligned so that the molehill top graphic overlaps the ground of the next “step” further towards the top of the screen. This will visually contain the mole, so it appears to be emerging from inside the molehill.

We intentionally skipped the Z value of 1, to provide an extra expansion space if we later decide that we need another element in the “sandwich”. It is easier to leave little holes like this than to worry about changing everything later, if we enhance our design. So throughout our layout, we will consider it as a sandwich of five Z values, even though we only use four elements in the sandwich.

As we said, we need this to be a “stair step” board. So for each row of molehills, from the top of the screen to the bottom, we will need to increase the Z-ordering between layers to complete the illusion. This is needed so that each mole will actually pass in front of the ground layer that is closer to the top of the screen, yet will hide completely behind the ground layer in its own sandwich of layers.

Designing the spawn

That covers the physical design of the game, but there is one additional design aspect we need to discuss: spawning moles. We need to spawn the moles whenever we need one to be put into the play. Just as we reviewed two approaches to the hiding mole problem earlier, we will touch on two approaches to mole spawning.

The first approach (and most common) is to create a new mole from scratch each time you need one. When you are done with it, you destroy it. This works fine for games with a small number of objects or games of more limited complexity, but there is a performance penalty to create and destroy a lot of objects in a short amount of time. Strictly speaking, our mole thumping game would likely work fine with this approach. Even though we will be creating and destroying quite a few moles all the time, we only have a dozen possible moles, not hundreds.

The other approach is to create a spawning pool. This is basically a set number of the objects that are created when you start up. When you need a mole, in our case, you ask the pool for an unused “blank mole”, set any parameters that are needed, and use it. When you are done with it, you reset it back to the “blank mole” state, and it goes back into the pool.

For our game the spawning pool might be a little more heavily coded than needed, as it is doubtful that we would run into any performance issues with this relatively simple game. Still, if you are willing to build the additional code as we are doing here, it does provide a strong foundation to add more performance-heavy effects later on.

To clarify our design approach, we will actually implement a variation of the traditional spawning pool. Instead of a general pool of moles, we will build our “blank mole” objects attached to their molehills. A more traditional spawning pool might have six “blnk moles” in the pool, and they are assigned to a molehill when they are needed. Both approaches are perfectly valid.

Portrait mode

The default orientation supported by cocos2d is landscape mode, which is more commonly used in games. However, we want our game to be in portrait mode. The changes are very simple to make this work. If you click once on the project name (and blue icon) in the Project Navigator pane (where all your files are listed), and then click on the name of your game under TARGETS , you will see the Summary pane. Under the Supported Interface Orientations, select Portrait, and deselect Landscape Left and Landscape Right . That will change your project to portrait. The one adjustment to the cocos2d template code we need is in the IntroLayer.m. After it sets the background to Default.png, there is a command to rotate the background. Remove, or comment out this line, and everything will work correctly.

Custom TTF fonts

In this project we will be using a custom TTF font. In cocos2d 1.x, you could simply add the font to your project and use it. Under cocos2d 2.0, which we are using, we have to approach this a little differently. We add the font to our project (we are using anudrg.ttf). Then we edit the Info.plist for our project, and add a new key to the list, like this:

This tells the project that we need to know about this font. To actually use the font, we need to call it by the proper name for the font, not the filename. To find out this name, in Finder, select the file and choose File Info . In the info box, there is an entry for Full Name . In our case, the file name is AnuDaw. Any time we create a label with CCLabelTTF, we simply need to use this as the font name, and everything works perfectly.

Defining a molehill

We have created a new subclass of CCNode to represent the MXMoleHill object. Yes, we will be using a subclass of CCNode, not a subclass of CCSprite . Even though we initially would consider the molehill to be a sprite, referring back to our design, it is actually made up of two sprites, one for the top of the hill and one for the bottom. We will use CCNode as a container that will then contain two CCSprite objects as variables inside the MXMoleHill class.

Filename: MXMoleHill.h @interface MXMoleHill : CCNode { NSInteger moleHillID; CCSprite *moleHillTop; CCSprite *moleHillBottom; NSInteger moleHillBaseZ; MXMole *hillMole; BOOL isOccupied; } @property (nonatomic, assign) NSInteger moleHillID; @property (nonatomic, retain) CCSprite *moleHillTop; @property (nonatomic, retain) CCSprite *moleHillBottom; @property (nonatomic, assign) NSInteger moleHillBaseZ; @property (nonatomic, retain) MXMole *hillMole; @property (nonatomic, assign) BOOL isOccupied; @end

If this seems rather sparse to you, it is. As we will be using this as a container for everything that defines the hill, we don’t need to override any methods from the standard CCNode class. Likewise, the @implementation file contains nothing but the @synthesize statements for these variables.

It is worth pointing out that we could have used a CCSprite object for the hillTop sprite, with the hillBottom object as a child of that sprite, and achieved the same effect. However, we prefer consistency in our object structure, so we have opted to use the structure noted previously. This allows us to refer to the two sprites in exactly the same fashion, as they are both children of the same parent.

Building the mole

When we start building the playfield, we will be creating “blank mole” objects for each hill, so we need to look at the MXMole class before we build the playfield. Following the same design decision as we did with the MXMoleHill class, the MXMole class is also a subclass of CCNode.

Filename: MXMole.h #import #import "cocos2d.h" #import "MXDefinitions.h" #import "SimpleAudioEngine.h" // Forward declaration, since we don't want to import it here @class MXMoleHill; @interface MXMole : CCNode { CCSprite *moleSprite; // The sprite for the mole MXMoleHill *parentHill; // The hill for this mole float moleGroundY; // Where "ground" is MoleState _moleState; // Current state of the mole BOOL isSpecial; // Is this a "special" mole? } @property (nonatomic, retain) MXMoleHill *parentHill; @property (nonatomic, retain) CCSprite *moleSprite; @property (nonatomic, assign) float moleGroundY; @property (nonatomic, assign) MoleState moleState; @property (nonatomic, assign) BOOL isSpecial; -(void) destroyTouchDelegate; @end

We see a forward declaration here (the @class statement). Use of forward declaration avoids creating a circular loop, because the MXMoleHill.h file needs to import MXMole.h . In our case, MXMole needs to know there is a valid class called MXMoleHill, so we can store a reference to an MXMoleHill object in the parentHill instance variable, but we don’t actually need to import the class. The @class declaration is an instruction to the compiler that there is a valid class called MXMoleHill, but doesn’t actually import the header while compiling the MXMole class. If we needed to call the methods of MXMoleHill from the MXMole class, we could then put the actual #import “MXMoleHill.h” line in the MXMole.m file. For our current project, we only need to know the class exists, so we don’t need that additional line in the MXMole.m file.

We have built a simple state machine for MoleState. Now that we have reviewed the MXMole.h file, we have a basic idea of what makes up a mole. It tracks the state of the mole (dead, alive, and so on), it keeps a reference to its parent hill, and it has CCSprite as a child where the actual mole sprite variable will be held. There are a couple of other variables (moleGroundY and isSpecial), but we will deal with these later.

Filename: MXDefinitions.h typedef enum { kMoleDead = 0, kMoleHidden, kMoleMoving, kMoleHit, kMoleAlive } MoleState; #define SND_MOLE_NORMAL @"penguin_call.caf" #define SND_MOLE_SPECIAL @"penguin_call_echo.caf" #define SND_BUTTON @"button.caf"

Unlike in the previous article, we do not have typedef enum that defines the MoleState type inside this header file. We have moved our definit ions to the MXDefinitions.h file, which helps to maintain slightly cleaner code. You can storethese “universal” definitions in a single header file, and include the header in any .h or .m files where they are needed, without needing to import classes just to gain access to these definitions. The MXDefinitions.h file only includes the definitions; there are no @interface or @implementation sections, nor a related .m file.

Making a molehill

We have our molehill class and we’ve seen the mole class, so now we can look at how we actually build the molehills in the MXPlayfieldLayer class:

Filename: MXPlayfieldLayer.m -(void) drawHills { NSInteger hillCounter = 0; NSInteger newHillZ = 6; // We want to draw a grid of 12 hills for (NSInteger row = 1; row <= 4; row++) { // Each row reduces the Z order newHillZ--; for (NSInteger col = 1; col <= 3; col++) { hillCounter++; // Build a new MXMoleHill MXMoleHill *newHill = [[MXMoleHill alloc] init]; [newHill setPosition:[self hillPositionForRow:row andColumn:col]]; [newHill setMoleHillBaseZ:newHillZ]; [newHill setMoleHillTop:[CCSprite spriteWithSpriteFrameName:@"pileTop.png"]]; [newHill setMoleHillBottom:[CCSprite spriteWithSpriteFrameName:@"pileBottom.png"]]; [newHill setMoleHillID:hillCounter]; // We position the two moleHill sprites so // the "seam" is at the edge. We use the // size of the top to position both, // because the bottom image // has some overlap to add texture [[newHill moleHillTop] setPosition: ccp(newHill.position.x, newHill.position.y + [newHill moleHillTop].contentSize.height / 2)]; [[newHill moleHillBottom] setPosition: ccp(newHill.position.x, newHill.position.y - [newHill moleHillTop].contentSize.height / 2)]; //Add the sprites to the batch node [molesheet addChild:[newHill moleHillTop] z:(2 + (newHillZ * 5))]; [molesheet addChild:[newHill moleHillBottom] z:(5 + (newHillZ * 5))]; //Set up a mole in the hill MXMole *newMole = [[MXMole alloc] init]; [newHill setHillMole:newMole]; [[newHill hillMole] setParentHill:newHill]; [newMole release]; // This flatlines the values for the new mole [self resetMole:newHill]; [moleHillsInPlay addObject:newHill]; [newHill release]; } } }

This is a pretty dense method, so we’ll walk through it one section at a time. We start by creating two nested for loops so we can iterate over every possible row and column position. For clarity, we named our loop variables as row and column, so we know what each represents. If you recall from the design, we decided to use a 3 x 4 grid, so we will have three columns and four rows of molehills. We create a new hill using an alloc/init, and then we begin filling in the variables. We set an ID number (1 through 12), and we build CCSprite objects to fill in the moleHillTop and moleHillBottom variables.

Filename: MXPlayfieldLayer.m -(CGPoint) hillPositionForRow:(NSInteger)row andColumn:(NSInteger)col { float rowPos = row * 82; float colPos = 54 + ((col - 1) * 104); return ccp(colPos,rowPos); }

We also set the position using the helper method, hillPositionForRow:andColumn:, that returns a CGPoint for each molehill. (It is important to remember that ccp is a cocos2d shorthand term for a CGPoint. They are interchangeable in your code.) These calculations are based on experimentation with the layout, to create a grid that is both easy to draw as well as being visually appealing.

The one variable that needs a little extra explaining is moleHillBaseZ . This represents which “step” of the Z-order stair-step design this hill belongs to. We use this to aid in the calculations to determine the proper Z-ordering across the entire playfield. If you recall, we used Z-orders from 2 to 5 in the illustration of the stack of elements. When we add the moleHillTop and moleHillBottom as children of the moleSheet (our CCSpriteBatchNode), we add the Z-order of the piece of the sandwich to the “base Z” times 5. We will use a “base Z” of 5 for the stack at the bottom of the screen, and a “base Z” of 2 at the top of the screen. This will be easier to understand the reason if we look at the following chart, which shows the calculations we use for each row of molehills:

As we start building our molehills at the bottom of the screen, we start with a higher Z-order first. In the preceding chart, you will see that the mole in hole 4 (second row of molehills from the bottom) will have a Z-order of 23. This will put it behind its own ground layer, which is at a Z-order of 24, but in front of the ground higher on the screen, which would be at a Z-order of 19.

It is worth calling out that since we have a grid of molehills in our design, all Z-ordering will be identical for all molehills in the same row. This is why the decrement of the baseHillZ variable occurs only when we are iterating through a new row.

If we refer back to the drawHills method itself, we also see a big calculation for the actual position of the moleHillTop and moleHillBottom sprites. We want the “seam” between these two sprites to be at the top edge of the ground image of their stack, so we set the y position based on the position of the MXMoleHill object. At first it may look like an error, because both setPosition statements use contentSize of the moleHillTop sprite as a part of the calculation. This is intentional, because we have a little jagged overlap between those two sprites to give it a more organic feel.

To wrap up the drawHills method, we allocate a new MXMole, assign it to the molehill that was just created, and set the cross-referencing hillMole and parentHill variables in the objects themselves. We add the molehill to our moleHillsInPlay array, and we clean everything up by releasing both the newHill and the newMole objects. Because the array retains a reference to the molehill, and the molehill retains a reference to the mole, we can safely release both the newHill and newMole objects in this method.

Drawing the ground

Now that we have gone over the Z-ordering “trickery”, we should look at the drawGround method to see how we accomplish the Z-ordering in a similar fashion:

Filename: MXPlayfieldLayer.m -(void) drawGround { // Randomly select a ground image NSString *groundName; NSInteger groundPick = CCRANDOM_0_1() * 2; switch (groundPick) { case 1: groundName = @"ground1.png"; break; default: // Case 2 also falls through here groundName = @"ground2.png"; break; } // Build the strips of ground from the selected image for (int i = 0; i < 5; i++) { CCSprite *groundStrip1 = [CCSprite spriteWithSpriteFrameName:groundName]; [groundStrip1 setAnchorPoint:ccp(0.5,0)]; [groundStrip1 setPosition:ccp(size.width/2,i*82)]; [molesheet addChild:groundStrip1 z:4+((5-i) * 5)]; } // Build a skybox skybox = [CCSprite spriteWithSpriteFrameName:@"skybox1.png"]; [skybox setPosition:ccp(size.width/2,5*82)]; [skybox setAnchorPoint:ccp(0.5,0)]; [molesheet addChild:skybox z:1]; }

This format should look familiar to you. We create five CCSprite objects for the five stripes of ground, tile them from the bottom of the screen to the top, and assign the Z-order as z:4+((5-i) * 5). We do include a randomizer with two different background images, and we also include a skybox image at the top of the screen, because we want some sense of a horizon line above the mole-thumping area.

anchorPoint is the point that is basically “center” for the sprite. The acceptable values are floats between 0 and 1. For the x axis, an anchorPoint of 0 is the left edge, and 1 is the right edge (0.5 is centered). For the y axis, an anchorPoint of 0 is the bottom edge, and 1 is the top edge. This anchorPoint is important here because that anchorPoint is the point on the object to which the setPosition method will refer. So in our code, the first groundStrip1 created will be anchored at the bottom center. When we call setPosition, the coordinate passed to setPosition needs to relate to that anchorPoint; the position set will be the bottom center of the sprite. If this is still fuzzy for you, it is a great exercise to change anchorPoint of your own CCSprite objects and see what happens on the screen.

Mole spawning

The only piece of the “sandwich” of elements we haven’t seen in detail is the mole itself, so let’s visit the mole spawning method to see how the mole fits in with our design:

Filename: MXPlayfieldLayer.m -(void) spawnMole:(id)sender { // Spawn a new mole from a random, unoccupied hill NSInteger newMoleHill; BOOL isApprovedHole = FALSE; NSInteger rand; if (molesInPlay == [moleHillsInPlay count] || molesInPlay == maxMoles) { // Holes full, cannot spawn a new mole } else { // Loop until we pick a hill that isn't occupied do { rand = CCRANDOM_0_1() * maxHills; if (rand > maxHills) { rand = maxHills; } MXMoleHill *testHill = [moleHillsInPlay objectAtIndex:rand]; // Look for an unoccupied hill if ([testHill isOccupied] == NO) { newMoleHill = rand; isApprovedHole = YES; [testHill setIsOccupied:YES]; } } while (isApprovedHole == NO); // Mark that we have a new mole in play molesInPlay++; // Grab a handle on the mole Hill MXMoleHill *thisHill = [moleHillsInPlay objectAtIndex:newMoleHill]; NSInteger hillZ = [thisHill moleHillBaseZ]; // Set up the mole for this hill CCSprite *newMoleSprite = [CCSprite spriteWithSpriteFrameName:@"penguin_forward.png"]; [[thisHill hillMole] setMoleSprite:newMoleSprite]; [[thisHill hillMole] setMoleState:kMoleAlive]; // We keep track of where the ground level is [[thisHill hillMole] setMoleGroundY: thisHill.position.y]; // Set the position of the mole based on the hill float newMolePosX = thisHill.position.x; float newMolePosY = thisHill.position.y - (newMoleSprite.contentSize.height/2); [newMoleSprite setPosition:ccp(newMolePosX, newMolePosY)]; // See if we need this to be a "special" mole NSInteger moleRandomizer = CCRANDOM_0_1() * 100; // If we randomized under 5, make this special if (moleRandomizer < 5) { [[thisHill hillMole] setIsSpecial:YES]; } //Trigger the new mole to raise [molesheet addChild:newMoleSprite z:(3 + (hillZ * 5))]; [self raiseMole:thisHill]; } }

The first thing we check is to make sure we don’t have active moles in every molehill, and that we haven’t reached the maximum number of simultaneous moles we want on screen at the same time (the maxMoles variable). If we have enough moles, we skip the rest of the loop. If we need a new mole, we enter a do…while loop that will randomly pick a molehill and check if it has the isOccupied variable set to NO (that is, no active mole in this molehill). If the randomizer picks a molehill that is already occupied, the do…while loop will pick another molehill and try again. When we find an unoccupied molehill, the code breaks out of the loop and starts to set up the mole.

As we saw earlier, there is already a “blank mole” attached to every molehill. At this point we build a new sprite to attach to the moleSprite variable of MXMole, change the moleState to kMoleAlive, and set up the coordinates for the mole to start. We want the mole to start from underground (hidden by the ground image), so we set the mole’s y position as the position of the molehill minus the height of the mole.

Once we have set up the mole, we assign our calculated Z-order for this mole (based on the moleHillBaseZ variable we stored earlier for each molehill), and call the raiseMole method, which controls the animation and movement of the mole.

Special moles

We have seen two references to the isSpecial variable from the MXMole class, so now is a good time to explain how it is used. In order to break the repetitive nature of the game, we have added a “special mole” feature. When a new mole is requested to spawn in the spawnMole method, we generate a random number between 1 and 100. If the resulting number is less than five, then we set the isSpecial flag for that mole. This means that roughly 5 percent of the time the player will get a special mole. Our special moles use the same graphics as the standard mole, but we will make them flash a rainbow of colors when they are in the play. It is a small difference, but enough to set up the scoring to give extra points for the “special mole”. To implement this special mole, we only need to adjust coding in three logic areas:

  • When raiseMole is setting the mole’s actions (to make it flashy)

  • When we hit the mole (to play a different sound effect)

  • When we score the mole (to score more points)

This is a very small task, but it is the small variations in the gameplay that will draw the players in further. Let’s see the game with a special mole in the play:

Moving moles

When we call the raiseMole method, we build all of the mole’s behavior. The absolute minimum we need is to raise the mole from the hill and lower it again. For our game, we want to add a little randomness to the behavior, so that we don’t see exactly the same motions for every mole. We use a combination of pre-built animations with actions to achieve our result. As we haven’t used any CCAnimate calls before, we should talk about them first.

The animation cache

Cocos2d has many useful caches to store frequently used data. When we use a CCSpriteBatchNode, we are using the CCSpriteFrameCache to store all of the sprites we need by name. There is an equally useful CCAnimationCache as well. It is simple to use. You build your animation as a CCAnimation, and then load it to the CCAnimationCache by whatever name you would like.

When you want to use your named animation, you can create a CCAnimate action that loads directly from CCAnimationCache. The only caution is that if you load two animations with the same name to the cache, they will collide in the cache, and the second one will replace the first.

For our project, we preload the animation during the init method by calling the buildAnimations method. We only use one animation here, but you could preload as many as you need to the cache ahead of time.

Filename: MXPlayfieldLayer.m -(void) buildAnimations { // Load the Animation to the CCSpriteFrameCache NSMutableArray *frameArray = [NSMutableArray array]; // Load the frames [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_forward.png"]]; [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_left.png"]]; [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_forward.png"]]; [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_right.png"]]; [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_forward.png"]]; [frameArray addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"penguin_forward.png"]]; // Build the animation CCAnimation *newAnim = [CCAnimation animationWithSpriteFrames:frameArray delay:0.4]; // Store it in the cache [[CCAnimationCache sharedAnimationCache] addAnimation:newAnim name:@"penguinAnim"]; }

We only have three unique frames of animation, but we load them multiple times into the frameArray to fit our desired animation. We create a CCAnimation object from the frameArray, and then commit it to CCAnimationCache under the name penguinAnim. Now that we have loaded it to the cache, we can reference it anywhere we want it, just by requesting it from CCAnimationCache, like this:

[[CCAnimationCache sharedAnimationCache] animationByName:@"penguinAnim"]]

LEAVE A REPLY

Please enter your comment!
Please enter your name here