显示标签为“RPG”的博文。显示所有博文
显示标签为“RPG”的博文。显示所有博文

2011年10月16日星期日

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 2: Tiled Maps

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 2: Tiled Maps:
Update: Made some modifications to the source code – changed the title music (Hope you enjoy a remix of Dragon Warrior) and expanded the map to give a better idea of how you can use the space. Also, I added grid lines to the tiles so it didn’t looks so much like a painting. More parts to come!

It’s been a while since part 1 – we know from your comments that it’s not your favorite thing in the world (it’s not ours either) but we’re gonna try to get going again!

That being said, if you haven’t read part 1 of the tutorial, or the intro describing what we’re working towards, now’s your chance to play catch-up before plunging forward into the next piece of unknown – tile based maps using Tiled!

As will be the case for the rest of the tutorial, we’re using the Cocos2D framework for game development, which already includes support for the TMX file format (we’ll get to that later – just know that it makes our lives easier!). To build the map, we’re going to be using Tiled Qt, a fantastic piece of software that makes creating maps a piece of cake!

Source code after the break!



Download the source for this project from here!

I’ve gone ahead and made what I believe to be some of the finest tiles available. Feel free to use these tiles in any way you see fit (but for the sake of artists everywhere please don’t).

Awesome Tiles
One if by land, Two if by sea, Three if by tree, Four if by...mountain

Be consistent in your spacing – whether its 1 pixel, 0 pixels, or 100 pixels. Tiled needs to know how much space is between each tile when reading in your graphic, otherwise when the map is drawn it will look wrong. The same applies to the margin around the sides. In my case, I’ve used a 1px margin and 1px spacing.

Also I’ve made my tiles 44px by 44px, so they’re large enough to see the wonderful detail on them – how big you make your tiles is completely up to you. If you’re programming for iPhone, just remember that you have a smaller space to deal with and you probably don’t want your user to have to constantly be scrolling around – 44×44 gets you about 11 tiles horizontally, and a little more than 7 vertical. For the purposes of this tutorial, I’ve made a really simple map with 15×15 tiles. Let’s get you started on designing your own map first!

Compression Setting First things first – the default compression that Tiled is set to (at least on the Mac version) is incompatible with Cocos2d. So click on the Tiled menu and select “Preferences”. Then in the drop down select “Base 64 (gzip compressed)” – you do NOT want the zlib compressed version. Click on the pic on the left to see the details if necessary.

Next, go to File->New, and then put in the parameters for your new map. You want the orientation set to “Orthogonal”, and the map size and tile size set to whatever works for you. For your map to work with this project, you’ll want to rename the tile layer from “Tile Layer 1″ to “Background”. Once you’re set there, go to Map->New Tileset, give it a name, choose your tile sheet, then set the height and width, and margin and spacing. Once you’ve done all that, you’ll see the Tilesets section of Tiled (generally the bottom right) become populated with your tiles. Now you’re ready to start drawing!

You can use the Stamp Brush tool Stamp Brush to draw individual squares, or use the Bucket Fill tool Bucket Fill Tool to fill a large portion. Either way, you draw by clicking on the tile you want to use in the Tilesets section, and then click to fill a square or a section. Whenever you’re done, save your map as “Level1″ to the Resources directory of your Xcode project, and make sure you copy or move your tile sheet file in there also, and then add them both in the actual project.

Enough Tiled – time for some code!

We only had to add one class for this tutorial, Map.h/m. The Map class simply loads up the TMX file you created and adds it to the screen, and uses the touch detection methods to pan the screen around. Also we made a static variable to use the Singleton design pattern so that it can be accessed from anywhere (this comes in handy later on). Here’s the code where we load up the map:

Map.h

- (void) loadLevel:(int)levelNum
{
NSString * fileName = [NSString stringWithFormat:@"Level%i.tmx", levelNum];
self.anchorPoint = ccp(0,0);
self.position = ccp(0,0);
self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:fileName];
self.background = [self.tileMap layerNamed:@"Background"];

[self addChild:self.tileMap z:-1];
}

As you can see, the tile layer we renamed to “Background” in Tiled is what we are referencing with the “layerNamed” function. Like I mentioned, the rest of the code in the Map class is mainly for moving the map around – the “boundLayerPos” function makes sure we can’t scroll our Map past its edges.

The only other change we make is in PlayLayer. In the header file, we add:

#import "Map.h"

and in PlayLayer.m we update the init function:

- (id) init
{
if ((self = [super init])) {
Map *m = [Map getInstance];
[m loadLevel:1];
[self addChild:m];
}

return self;
}

And that’s it! Now we have our menu system that loads up our tile based map when you start the game. Part 3 to come as soon as we can get it out!

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!