• In the past few lectures we’ve covered a lot of ground.
We learned how to create animation blocks, and move objects around in the view based on variable values generated by a random number generator. We learned how to load sounds. We learned how to populate an array of images using a for loop and to associate that array with an image object. We also learned how to move an object based on a touch event, and how to detect if the object’s bounding box intersected with another object.
• The purpose of this demo is to put some these skills together to make a simple animated game.
• In honor of Halloween I decided to make a demo that would involve a character moving through a graveyard and jumping over tombstones, and maybe dodging flying bats. In other words, a simple “dodging” game.
• From this basic structure more complex games could be built, where, say, the character moves back and forth and collects tokens as well as avoids hazards.
• For my demo, I envisioned a character that would stay in place, while the background moved behind him, and the obstacles moved toward him. In the film industry, if the camera travels along with the actor, it is called a
“trucking shot”. The character does not move in the frame, but the background appears to move. In the game industry this is common in traditional “side-scrolling” games.
• I had to think of the elements I would need
– A spooky sky background picture
– Background elements like a scary tree or a mound of tombstones
– Obstacles and hazards to avoid, like a tombstone to jump over, or an animated bat to duck
• I needed a character who could
– Walk (the default behavior)
– Jump (once, based on some event, then return to the default)
– Duck (once, based on some event, then return to the default)
– Maybe fall down (based on a collision with an obstacle)
• The game would have 2 views in landscape mode
– The game view
– The game over view
• Although the game would use 2D sprites, I wanted it to have a 3D look
(sometimes called 2 ½ D).
• The character’s different behaviors were animated using the Poser 3D animation tool, and rendered as a sequence of .png images with transparent (alpha) backgrounds. They were rendered at much larger scale than needed for the game
(about 200%). The image sequences were further cropped, edited, and scaled using the video layer tools in Photoshop.
• The other elements were also created in Photoshop, and I set up a mock view of the game to help me get the proper scale for the elements. The view was 460 X
320 (the aspect ratio of the iPhone screen in landscape mode.)
Frame from the jumping sequence Mock view of the game Frame from the bat animation
• I chose the Utility Application project template because it provides two views – the main view (for the game) and the flipside view (for the game over screen).
• I imported the folders of my graphics assets by dragging and dropping them on the Resources folder in the Xcode project window.
• I also imported a theme music file (.aif format) by dropping it on the project icon.
• I also had to import the AVFoundation framework by control-clicking on the frameworks folder, and by then choosing Add > Existing Frameworks… >
AVFoundation.framework.
• The header file for the main view (MainViewController.h) is where the objects I needed were created, their properties declared, and custom methods were declared. This was a necessary first step so that I could make connections between objects and methods in
Interface Builder.
• I also had to import the AVFoundation class, so that I could use its methods to play the theme music for the game.
• Although I have not implemented all the features of the game as of now, I included all the ones I could think of, but commented some of them out. The significant code is on the next couple of slides, the project itself is on the file server in the Xcode exercises folder of the
Mobile Applications group folder.
@interface MainViewController : UIViewController <FlipsideViewControllerDelegate> {
IBOutlet UIImageView * bones ; //the container for the skeleton animations
NSMutableArray * walkAnim ;
NSMutableArray * jumpAnim ;
//NSMutableArray *duckAnim;
//NSMutableArray *dieAnim;//all the different things the character does
//IBOutlet UIImageView *bat;//the window for the bat animation
//NSMutableArray *batAnim;
IBOutlet UIImageView * tombs ; //will switch between a couple of images
IBOutlet UIImageView * bkgd ; //will switch between a couple of background elements
NSTimer * bkgdTimer ; //moves the background image across the view
NSTimer * tombsTimer ; //moves the tombstones across the view
//NSTimer *batTimer;//moves the bat across the screen
NSTimer * tombsCollisionTimer ; //checks for collision with tombs
//NSTimer *batCollisionTimer;//checks for collision with bat
}
AVAudioPlayer * themePlayer ; //will play various theme songs
//AVAudioPlayer *gameOverPlayer;//plays a sound when a collision is detected
// custom methods
( IBAction ) jump;
( void ) startSound;
( void ) setupArrays;
( void ) startTimers;
( void ) moveBkgd;
( void ) moveTombs;
//- (void) moveBat;
( void ) checkTombsCollision;
//- (void) checkBatCollision;
( void ) moveUp;
- ( void ) moveDown;
- ( void ) showWalk;
• In the MainViewController.m file I had to find the code block beginning with:
( BOOL )shouldAutorotateToInterfaceOrientation:
• And then I added a few lines of code which we already discussed in the Sprite Animation lecture and demo.
• Then I launched Interface Builder by clicking on the
MainView.xib file.
• The View window was rotated to the landscape mode, and its Background color was set to black in the Attributes tab of the Inspector window. The info button in the View (which is used to flip to the second the view) was deleted, because that action will be determined by detecting a collision between the obstacle and the character.
• Several Image Views were added – for the sky, for the background element (a tree), for the obstacle (a tombstone), and for the character. Those graphics were added using the dropdown list for the Image property.
• Because the frames of the character animations are not the same aspect ratio (e.g. the jump animation is taller) that image view’s Mode property was changed from the default Scale to Fill to Bottom Right in the Attributes tab of the Inspector window.
• To catch the “tap” event that triggers the jump animation a Rounded Rect button was dropped over the character image object and scaled to cover the character completely (if I had wanted the entire screen to respond to a touch I could have scaled it to fill the whole view). To make the button invisible its Type was set to
Custom in the Attributes tab.
• In the Document window File’s Owner was selected. In the Connections tab of the Inspector window there were 3 Outlets: bones (for the character), background (for the tree), and tombs
(for the headstone). A connector was dragged to each of their respective Image Views in the View window.
• There was one Received Action, jump, which was connected to the button.
• The IB file was saved and I went back to Xcode to implement my methods.
• In the MainViewController.m file the first thing to do was to synthesize all the properties I declared in the .h file.
• Then I added some code to the viewDidLoad method so I could invoke some methods when the app starts. They were:
[ self startSound ]; //loads the theme music and other game sounds
[ self setupArrays ]; //loads the images into the arrays
[ self startTimers ]; //gets the animation and collision detection timers going
Right after the viewDidLoad method, I set up the sound to play. The first line of the method provides the path to the sound resource, the third associates that resource with an instance of the AVAudioPlayer, and gives it the instance name themePlayer. The fourth line starts the song at its beginning, and the last line tells it to play. The code is below:
( void ) startSound{
NSString *themeSongPath = [ NSString stringWithFormat : @"%@%@" , [[ NSBundle mainBundle ] resourcePath ], @"/gravehop.aif" ];
NSError *err; themePlayer = [[ AVAudioPlayer alloc ] initWithContentsOfURL : [ NSURL fileURLWithPath :themeSongPath] error :&err]; themePlayer .
currentTime = 0 ;
}
[ themePlayer play ];
Right after the startSound method, I implemented the 3 timers I needed. bkgdTimer animates the background image across the screen every 10 seconds. Its selector (the method called when the timer expires) is moveBkgd, a method defined further down in the code. tombsTimer animates the obstacle, but it fires off every .01 seconds, because it needs to move the object incrementally (which
I’ll explain a bit later). Its selector is moveTombs.
tombsCollisionTimer fires off almost as fast, every .03 seconds, and it is polling for an overlap (collision) of the image object of the obstacle and the character. Its selector is checkTombsCollision. All the timers were set to repeat indefinitely (repeats:YES). The background and the obstacle are then set to startAnimating. The code is on the next slide.
( void ) startTimers{ bkgdTimer = [[ NSTimer scheduledTimerWithTimeInterval : 10 target : self selector : @selector ( moveBkgd ) userInfo : nil repeats : YES ] retain ];
[ self moveBkgd ];
[ bkgd startAnimating ]; tombsTimer = [[ NSTimer scheduledTimerWithTimeInterval : 0.01
target : self selector : @selector ( moveTombs ) userInfo : nil repeats : YES ] retain ]; tombsCollisionTimer = [[ NSTimer scheduledTimerWithTimeInterval : 0.03
target : self selector : @selector ( checkTombsCollision ) userInfo : nil repeats : YES ] retain ];
}
[ tombs startAnimating ];
• Next, I populated two image arrays, walkAnim and jumpAnim. First, the arrays were created by allocating memory for them and initializing them.
A for loop was used to iterate through all the images, setting the value of i to 1, incrementing i, and stopping when the value of i reached the last image. The value of i was then substituted for the format specifier %d and concatenated with text of the image file’s name (as in w1.png) and put into a string. The string was then used to add the images to the array.
• The walk cycle array, walkAnim, was then associated with the image object bones, given an animation duration of .5 seconds, and set to start animating.
• Code for populating the array and for starting the walk cycle animation is on the next slide.
( void ) setupArrays { walkAnim = [[ NSMutableArray alloc ] init ];
// a loop that lists the walk cycle images for ( int i = 1 ; i < 16 ; i++){
}
NSString *pic = [ NSString stringWithFormat : @"w%d.png" , i];
UIImage *img = [ UIImage imageNamed :pic]; if (img) [ walkAnim addObject :img];
}
//set the default animation for the view - the walk cycle
[ bones setAnimationImages : walkAnim ];
[ bones setAnimationDuration : .5
];
[ bones startAnimating ];
• When the character jumps he needs to return to the default walking animation after one jump. The change is triggered by a tap on an invisible button floating above the character, so the method is defined in an IBAction (called jump). The image object bones also needs to move up, out of the way of the obstacle, and then return to its previous position.
• Switching the animation was easy – one image array was exchanged for another in the bones object. But to switch back a timer had to be set, equal to the length of the jump animation, so that when it expired it would switch the animations back.
The timer, of course, was set to run only once in response to the button press. The
selector for the timer was set to a method called showWalk.
• The animation block that moves the bones object up is part of the jump method. It uses a setAnimationDidStopSelector method to call a method moveDown that moves the bones object back down. The time allocated to the animation is half the duration of the jump animation, moveDown takes the other half. The code for the jump method is on the next slide, showWalk and moveDown are on the following slide.
( IBAction ) jump{
[ bones setAnimationImages : jumpAnim ];
[ bones setAnimationDuration : 2 ];
[ bones startAnimating ];
}
[ NSTimer scheduledTimerWithTimeInterval : 1.9
target : self selector : @selector ( showWalk ) userInfo : nil repeats : NO ];
// move the skeleton up bones .
frame = CGRectMake ( bones .
frame .
origin .
x , bones .
frame .
origin .
y , 121 , 180 );
[ UIView beginAnimations : nil context : nil ];
[ UIView setAnimationDuration : 1 ];
[ UIView setAnimationDelegate : self ];
[ UIView setAnimationDidStopSelector : @selector ( moveDown )]; bones .
frame = CGRectMake ( bones .
frame .
origin .
x , bones .
frame .
origin .
y 85 , 121 , 180 );
[ UIView commitAnimations ];
( void ) showWalk{
[ bones setAnimationImages : walkAnim ];
[ bones setAnimationDuration : .5
];
[ bones startAnimating ];
}
( void ) moveDown{
} bones .
frame = CGRectMake ( bones .
frame .
origin .
x , bones .
frame .
origin .
y , 121 , 180 );
[ UIView beginAnimations : nil context : nil ];
[ UIView setAnimationDuration : .5
];
[ UIView setAnimationDelegate : self ]; bones .
frame = CGRectMake ( bones .
frame .
origin .
x , bones .
frame .
origin .
y + 85 , 121 , 180 );
[ UIView commitAnimations ];
// moves the background steadily across the screen
- ( void ) moveBkgd { bkgd .
frame = CGRectMake ( 460 , 0 , 113 , 163 );
[ UIView beginAnimations : nil context : nil ];
[ UIView setAnimationDuration : 10.0
];
[ UIView setAnimationDelegate : self ]; bkgd .
frame = CGRectMake (100 , 0 , 113 , 163 );
}
[ UIView commitAnimations ];
• The moveBkgd animation code is fine if we don’t need to know where the object is in the view between the first and last key frames – the system takes care of that.
That kind of method is called “fire and forget”, like a self-guided missile. But if we want to detect a collision with another object then we need to know explicitly where the object is in each “frame” of the animation.
• To force the system to report the position x or y of the image object as it moves across the screen we had to amend the code to use a variable to describe the object’s x and y values as shown below. The tombsTimer is firing off very rapidly, constantly incrementing the animation based on the previous value of x or y.
• One more thing about the moveTombs method – the obstacle has to be reset to its original position (which is actually offscreen to the right) every time it animates off the left side of the screen. To do that a conditional statement was used to check to see what the x position of the obstacle object (tombs) was. If it was less than -100 pixels, then the object was placed back at the right, at 600 pixels. The code for the
moveTombs method is on the next slide.
( void ) moveTombs { tombs .
frame = CGRectMake ( tombs .
frame .
origin .
x , tombs .
frame .
origin .
y , 50 , 78 );
[ UIView beginAnimations : nil context : nil ];
[ UIView setAnimationDelegate : self ]; tombs .
frame = CGRectMake ( tombs .
frame .
origin .
x 2 , tombs .
frame .
origin .
y , 50 , 78 );
[ UIView commitAnimations ];
}
} if ( tombs .
frame .
origin .
x < 100 ){ tombs .
frame = CGRectMake ( 600 , 222 , 50 , 78 );
• Now that the animations were set, all I had to do was check to see whether the bounding box (frame) of the obstacle object (tombs) is overlapping the bounding box of the character object (bones). This check is called every .03 seconds by the checkTombsCollision timer in the
startTimers method described earlier.
• A conditional statement tests to see if a collision is detected, and if it is a method will be executed to stop the theme music from playing, and to flip the view over to the “game over” screen. The code is on the next slide.
• The flipsideViewController method was copied from the code generated by the Utility Application project template when the project was created.
It was originally attached to an info button in the main view which was deleted in Interface Builder.
}
}
// checks for collision and takes necessary action when a collision is detected
( void ) checkTombsCollision{ if ( CGRectIntersectsRect ( bones .
frame , tombs .
frame )){
// stop playing the theme song
[ themePlayer stop ];
// flip the view
FlipsideViewController *controller = [[ FlipsideViewController alloc ] initWithNibName : @"FlipsideView" bundle : nil ]; controller.
delegate = self ;controller.
modalTransitionStyle = UIModalTransitionStyleFlipHorizontal ;[ self presentModalViewController :controller animated : YES ];
[controller release ];
• So when the obstacle collides with the character, that session of the game is done and the game view flips over to the game over view. I did not do much to the
FlipsideView.xib file in Interface Builder. I changed the title to “Good try!” and left the default “Done” button to go back to the game (though I could have renamed the button label “Replay”).
• However, it’s important to reset the game assets to their starting state once the game over view flips back. The custom methods in the flipsideViewControllerDidFinish method reset the theme music to the beginning and start it playing, and also reset the position of the background image and the obstacle. The code is on the next slide.
( void )flipsideViewControllerDidFinish:( FlipsideViewController
*)controller {
[ self dismissModalViewControllerAnimated : YES ]; themePlayer .
currentTime = 0 ;
[ themePlayer play ];
} bkgd .
frame = CGRectMake ( 460 , 0 , 113 , 163 ); tombs .
frame = CGRectMake ( 600 , tombs .
frame .
origin .
y , 50 , 78 );
• First, some simple instructions should be added to the game. That could be done with a label on the main view, or it could be a good use of the currently empty panel of the flipside view, in which case code would have to be written to start the game in the flipside view mode instead of the main view. The button label in the flipside view would simply be “Play” to start the game session.
• To make the game more interesting, each time the view flips the game could toggle between hazards (the flying bat or the tombstone). The background image could toggle between the tree and the grave mound. The music could alternate between a few randomly selected themes. The dodging animation and the falling sequence of the character would have to be implemented. A game over sound could implemented when a collision is detected.
• Keeping score might be nice, and the display of the score could be in the flipside view along with the game rules. Saving the score to a .plist, so it persists between game sessions might also be a good idea. A login that saves to an array of players and their scores would be cool.
• Some students have suggested that the target bounding box of the character is too big. That could be addressed by creating a smaller target that floats above the character like the jump button does. Collision would then be checked between the obstacle and the smaller target.
• Let’s see how it goes by the end of the semester!