Simple Game Animation * iPhone Programming Exercise 4

Download Report

Transcript Simple Game Animation * iPhone Programming Exercise 4

Simple Game Animation –
iPhone Programming Exercise 3
CSE 391 Fall 2012
Tony Scarlatos
Putting the pieces together
• 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.
The Grave 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.
Game design
• 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).
Creating the game assets
•
•
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
Setting up the Xcode project
• 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
• 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.
Image views, image arrays, timers,
and an audio player
@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
}
A button action and
the custom methods
// 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;
A couple of other items
• 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.
Building the interface
•
•
•
•
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.
Making connections
• 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.
Implementation
• 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
Implementation (sound)
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];
}
Implementation (timers)
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.
Timer code
-
(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];
}
Implementation (image arrays)
• 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.
Walk cycle animation
-
(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];
}
Switching the animations
•
•
•
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.
The jump method
-
(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];
}
showWalk and moveDown
-
(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];
}
Implementation (move method)
Moving the background image across the
screen was simple. It used the standard
animation block methods for the UIView. The
first line gives the starting key frame position
and size of the object. The third line gives the
animation duration. The fifth line gives the
final key frame coordinates and size. The last
line commits the animation. The code is on
the next slide.
Move method code
// 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];
}
Fire and forget
•
•
•
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.
The moveTombs method
-
(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);
}
}
Checking for collision
• 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.
The checkTombsCollision method
// 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];
}
}
The flipside view
• 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.
flipsideViewControllerDidFinish
-
(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);
}
Success!
So what’s next?
•
•
•
•
•
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!