2011年10月16日星期日

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 1: Menus

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 1: Menus:
Menu ScreenshotAnd so the quest to develop an RPG game for the iPhone begins! If you haven’t read up on our initial Cocos2D Menu tutorials before getting started, now is probably a good time! Also if you haven’t seen the outline for this tutorial series, you may want to read that as well to give you an idea of where this is all going. This part of the tutorial will be the primer to get us started – it’s important because it’s the first part of the game that users will see once they start playing. In our example, I’m using some graphics I threw together pretty quickly, so please don’t judge my art and design capabilities harshly :) For this series we’ll continue to use the Cocos2D iPhone framework. Source code after the break!



You can download the source for this tutorial here: RPG Tutorial Part 1.

So lets get to the good stuff! If you’ve been keeping up with our site, some of this may look familiar to you, but we’re throwing in a few new things of course! Let’s start with a little music – these lines are in RPGTutorialPart1AppDelegate.m:

[MusicHandler preload];
...
[MusicHandler playBackgroundMusic];

The MusicHandler class provides convenience methods for playing music and sound effects using the SimpleAudioEngine class provided in Cocos2D. In our case, we only have methods so far for preloading our sounds and music, playing the background music, and playing a button click sound:

#import "MusicHandler.h"

@implementation MusicHandler

static NSString *BUTTON_CLICK_EFFECT = @"button-21.mp3";

+(void) preload
{
[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:@"Temple_of_Groovy-freesoundtrackmusic.mp3"];
[[SimpleAudioEngine sharedEngine] preloadEffect:BUTTON_CLICK_EFFECT];
}

+(void) playBackgroundMusic
{

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"Temple_of_Groovy-freesoundtrackmusic.mp3" loop:YES];
}

+(void) playButtonClick
{
[[SimpleAudioEngine sharedEngine] playEffect:BUTTON_CLICK_EFFECT];
}

@end

A few important things here:

  • Preloading sounds – this is where the device handles all of the decompression and loading necessary to play the music/effects. Its possible that without preloading there would be a short delay before the sound/music would actually start playing.
  • That brings up memory concerns – when you have lots of sound effects preloaded into memory, they can cause your app to reach its memory threshhold and crash. This is more dependent on the size of your files and memory available on your device than how many files you use. In that case, you would want to use the [unloadEffect] method to unload any sounds you’re not using. Background music doesn’t seem to suffer from the same issue.
  • We’re using a variable to hold the name of our button click sound – if this class was more complicated you may have to reference the file name several times, and if the file name for your button click sound changes its very easy to modify it in one place as opposed to several.

So now that we’ve got the awesome music out of the way (by the way, the music here was picked up from Freesoundtrackmusic.com, and the button click sound from Soundjay.com, in case anyone was wondering), lets move on to some menus. Our Menu Layer has a background image and 3 buttons – pretty simple.

from MenuLayer.m:

-(id) init
{

if ((self = [super init])) {
CGSize winSize = [[CCDirector sharedDirector] winSize];

CCSprite *background = [CCSprite spriteWithFile:@"menu_background.png"];
background.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:background];

CCMenuItemImage *startNew = [CCMenuItemImage itemFromNormalImage:@"new_game_button.png" selectedImage:@"new_game_button_down.png" target:self selector:@selector(onNewGame:)];
CCMenuItemImage *options = [CCMenuItemImage itemFromNormalImage:@"options_button.png" selectedImage:@"options_button_down.png" target:self selector:@selector(onOptions:)];
CCMenuItemImage *credits = [CCMenuItemImage itemFromNormalImage:@"credits_button.png" selectedImage:@"credits_button_down.png" target:self selector: @selector(onCredits:)];

CCMenu *menu = [CCMenu menuWithItems:startNew, options, credits, nil];
menu.position = ccp(winSize.width/2, winSize.height/2 - 50);
[menu alignItemsVerticallyWithPadding: 20.0f];
[self addChild:menu];
}

return self;
}

Nothing really out of the ordinary here – if you read our Cocos2D Menu tutorials this should all make sense. You can see the picture at the top of the post showing what this will look like.

The selectors are all basically the same as well:

- (void)onNewGame:(id)sender
{
[MusicHandler playButtonClick];
[SceneManager goPlay];
}

- (void)onOptions:(id)sender
{
[MusicHandler playButtonClick];
[SceneManager goOptions];
}

- (void)onCredits:(id)sender
{
[MusicHandler playButtonClick];
[SceneManager goCredits];
}

The only difference in onNewGame, onOptions, and onCredits is the SceneManager function thats being called. The Credits Layer and the Play Layer (for now) are really simple and straight forward so we won’t spend much time on it – basically a few CCLabelTTF’s displaying text (using a custom font is the only interesting portion, the font is added in the Resources folder of our project), and a Back button taking us back to the Menu Layer.

Options ScreenshotThe most interesting screen right now is our Options screen because it has 2 sliders to control the music and sound volume. To make the sliders we use a class made by a user and posted to the Cocos2d wiki called CCMenuItemSlider. To be able to use this class we have to slightly modify the CCMenu class provided with Cocos2d – it’s already been done in the source for this project, but you want to update the ccTouchMoved function to this:

-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
NSAssert(state_ == kCCMenuStateTrackingTouch, @"[Menu ccTouchMoved] -- invalid state");

CCMenuItem *currentItem = [self itemForTouch:touch];

if (currentItem != selectedItem_) {
[selectedItem_ unselected];
selectedItem_ = currentItem;
[selectedItem_ selected];
} else {
if ([selectedItem_ respondsToSelector: @selector(dragToPoint:)]) {
CGPoint touchLocation = [selectedItem_ convertTouchToNodeSpace: touch];
[selectedItem_ dragToPoint: touchLocation];
}
}
}

The only portion we had to add is the “else” statement, so we’re not affecting much – also the [respondsToSelector] makes sure that nothing is executed unless our selectedItem_ has a function called dragToPoint. The code change here is so minimal that this doesn’t affect anything else in Cocos2D!

I’m going to hit up the OptionsLayer piece by piece to show you what we’re doing, so let’s start with the prototype:

OptionsLayer.h

#import "cocos2d.h"
#import "CCMenuItemSlider.h"
#import "SceneManager.h"
#import "MusicHandler.h"
#import "SimpleAudioEngine.h"

@interface OptionsLayer : CCLayer {
CCMenuItemSlider *_musicSlider;
CCMenuItemSlider *_soundSlider;
int prevMusicLevel;
int prevSoundLevel;
}

@property (nonatomic, retain) CCMenuItemSlider *musicSlider;
@property (nonatomic, retain) CCMenuItemSlider *soundSlider;
@property int prevMusicLevel;
@property int prevSoundLevel;

- (id) init;
- (void) onMusicClick:(id)sender;
- (void) onMusicSlide:(id)sender;
- (void) onSoundClick:(id)sender;
- (void) onSoundSlide:(id)sender;
- (void)onBackClick:(id)sender;

@end

We need to keep a reference to the sliders for later use so we define a musicSlider and a soundSlider, and the prevMusicLevel and prevSoundLevel variables will be used for when we click on the Music or Sound buttons to mute. Let’s look at the init function in OptionsLayer.m now:

- (id) init
{
self = [super init];
if (self != nil) {

CGSize size = [[CCDirector sharedDirector] winSize];
CCSprite *background = [CCSprite spriteWithFile:@"menu_background.png"];
background.position = ccp(size.width/2, size.height/2);
[self addChild:background];

CCMenuItemToggle *musicBtn = [CCMenuItemToggle itemWithTarget:self selector:@selector(onMusicClick:) items:
[CCMenuItemImage itemFromNormalImage:@"music_button.png" selectedImage:@"music_button_down.png"],
[CCMenuItemImage itemFromNormalImage:@"music_button_down.png" selectedImage:@"music_button.png"],
nil];
musicBtn.position = ccp(size.width/2 - 165, size.height/2 + 40);

// Sound on/off button
CCMenuItemToggle *soundBtn = [CCMenuItemToggle itemWithTarget:self selector:@selector(onSoundClick:) items:
[CCMenuItemImage itemFromNormalImage:@"sound_button.png" selectedImage:@"sound_button_down.png"],
[CCMenuItemImage itemFromNormalImage:@"sound_button_down.png" selectedImage:@"sound_button.png"],
nil];
soundBtn.position = ccp(size.width/2 - 165, size.height/2);

SimpleAudioEngine *sae = [SimpleAudioEngine sharedEngine];

// Music slider
self.musicSlider = [CCMenuItemSlider itemFromTrackImage: @"slider_bar.png" knobImage: @"slider_knob.png" target:self selector: @selector(onMusicSlide:)];
self.musicSlider.minValue = 0;
self.musicSlider.maxValue = 100;
self.musicSlider.value = floor(sae.backgroundMusicVolume * 100);
self.musicSlider.position = ccp(size.width/2 + 45, size.height/2 + 40);

// Sound slider
self.soundSlider = [CCMenuItemSlider itemFromTrackImage: @"slider_bar.png" knobImage: @"slider_knob.png" target:self selector: @selector(onSoundSlide:)];
self.soundSlider.minValue = 0;
self.soundSlider.maxValue = 100;
self.soundSlider.value = floor(sae.effectsVolume * 100);
self.soundSlider.position = ccp(size.width/2 + 45, size.height/2);

// Back
CCMenuItemSprite *backBtn = [CCMenuItemImage itemFromNormalImage:@"back_button.png" selectedImage:@"back_button_down.png" target:self selector:@selector(onBackClick:)];
backBtn.position = ccp(size.width - 60, 25);

CCMenu *menu = [CCMenu menuWithItems:
musicBtn, self.musicSlider,
soundBtn, self.soundSlider,
backBtn,
nil];

menu.position = ccp(0,0);
[self addChild:menu];
}
return self;
}

As you can see in the screenshot above, we have a background, 2 buttons, and 2 sliders. The buttons are actually CCMenuItemToggle’s, which switch between the button backgrounds we specify when clicked, as opposed to just showing the selectedImage when the button is held down and the mouse is over the button. We add our background to the stage and create our Music and Sound toggle buttons. Next we create our sliders with our CCMenuItemSlider class. One note from the author about the graphics to be used for the slider track and the slider knob:

Sliders of different styles aren’t hard to do, the code handles automatically if the slider is horizontal or vertical: the only care the designer should take is that the track (i.e. the fixed part) is used to calculate the actual content size, so it should include the whole area intended to be used as slider. If knob is meant to be bigger than the track, just make the exceeding content transparent.

Basically when designing the graphics for your slider, if you plan on having the slider knob taller than the track, add blank space around the track to make it the same height as the slider knob.

Back to the code – we instantiate our sliders with a callback function that will be called every time the slider slides (even a tiny bit). Then we set a minValue and maxValue, which tells our slider the range of values that we want it to return (it figures out where you’ve slid the bar to and calculates the appropriate value). Finally we give it an initial value – in this case, we want the initial value to be the music volume and the sound volume. Both of those are stored in the SimpleAudioEngine class as floats with a range of 0-1, so we multiply by 100 and round down with “floor” to get an integer value. In theory, we could make the minValue and maxValue of the slider 0 and 1 respectively, and could skip that step, but I prefer volumes from 0-100 :) That’s all for the setup, let’s take a look at the callbacks now:

- (void)onMusicSlide:(id)sender
{
CCMenuItemSlider* slider = (CCMenuItemSlider*)sender;
[SimpleAudioEngine sharedEngine].backgroundMusicVolume = (slider.value / 100.0);
}

- (void)onMusicClick:(id)sender
{
[MusicHandler playButtonClick];
if ([sender selectedIndex] == 1) {
// button in off state
self.prevMusicLevel = self.musicSlider.value;
self.musicSlider.value = 0;
[SimpleAudioEngine sharedEngine].backgroundMusicVolume = 0;
}
else {
// button in on state
self.musicSlider.value = self.prevMusicLevel;
[SimpleAudioEngine sharedEngine].backgroundMusicVolume = (self.prevMusicLevel / 100.0);
}
}

The callback for the slider is simple – cast the sender to a CCMenuItemSlider, and set the backgroundMusicVolume to our slider’s value divided by 100.0 (the “.0″ is important as it forces the result to be a float).

The callback for our toggle button is a little more complicated because we’re going to mute the music and set the slider value to 0 as well, but we have to store the value of the slider for when the user clicks our toggle button again. We check the selectedIndex of our CCMenuItemToggle to find which state its in, and either set the slider value to 0 and the volume to 0 or set it to our stored prevMusicLevel, so the volume goes back to where it was when it was originally muted.

Whew! That was a long one – now you can go out and get the foundations for your RPG game ready. Start thinking about your characters and the stories of your game, as well as the look and feel and if you want to go for an old world look or something more modern. This is your quest – build the one game to rule them all!

没有评论:

发表评论