Quantcast
Channel: cocos2d for iPhone » cocos2d
Viewing all articles
Browse latest Browse all 14

A SpaceManager Game

$
0
0

This article is going to build upon the basics described in the previous SpaceManager article written some time ago which can be found here: http://www.cocos2d-iphone.org/archives/677

About the Author:

My name is Rob, I’m one of the guys working under the mobile bros. LLC title. We’ve released a few games in the recent years, all using SpaceManager, including: Pachingo, Trundle, and Kill Timmy. I am personally interested in physics myself and always looking for new ways to incorporate them in our games.

Goals

Today’s goal is to design a basic game using Cocos2d, Chipmunk, and SpaceManager. I believe the best lessons are those learned from example, so for this article I’ll walk you through building a small example of a game and show off some of the features of SpaceManager. Some of the things we’ll demonstrate here are:

* Save and Load entire game state
* Explosions in Chipmunk
* Creating a debug-layer
* Using impulses
* Retina Support

So what type of game should we do… perhaps one where we slingshot grenades at structures hoping to kill some kind of ninja animal inside? Sounds like a plan, so lets dive right in. This tutorial will use Cocos2d 0.99.5 and SpaceManager 0.0.6. I HIGHLY recommend you download the accompanying source code for this article so you can follow along; this will have everything you need and you can skip the setup steps. Source is found here:

Download the Source-Code: GrenadeGame.zip

Edit: Updated source location

Basic SpaceManager Game Setup:

You’ll want to create a new project, I’m naming mine GrenadeGame and I’m choosing the Cocos2d Chipmunk Application template. Once created, jump into the root of your project directory in Finder and place the “src” folder from your previously downloaded SpaceManager folder right here in the root and rename it from “src” to “SpaceManager”…. or perhaps wherever you want below the root.

No go back to Xcode and add the “SpaceManager” directory to your project… almost done. In order to get GrenadeGame to compile you must go to XCode’s menu bar and click Project->Edit Active Target “GrenadeGame”, and then find the “Header Search Paths” setting and add a new value; this value being:

“$(SRCROOT)/libs/Chipmunk/include/chipmunk”

Please keep the quotes or XCode will get it wrong most likely. Now you should be able to compile and run! You should see the default grossini dance for now.

Doing Some Good Stuff:

Now that your project is all set up, go ahead and delete both the HelloWorldScene files and then add a new Objective-C class; I’m calling mine simply “Game”. I then modify the .h to look like this:

#import "SpaceManagerCocos2d.h"
 
@interface Game : CCLayer
{
	SpaceManagerCocos2d *smgr;
}
 
+(id) scene;
@end

and the .m to look like this:

#import "Game.h"
 
@interface Game (PrivateMethods)
@end
 
@implementation Game
 
+(id) scene
{
	CCScene *scene = [CCScene node];
	[scene addChild:[Game node]];
	return scene;
}
 
- (id) init
{
	[super init];
 
	[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:NO];
 
	//allocate our space manager
	smgr = [[SpaceManagerCocos2d alloc] init];
	smgr.constantDt = 1/55.0; 
 
	return self;
}
 
- (void) dealloc
{
	[smgr release];
	[super dealloc];
}
 
- (BOOL)ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event
{
	return YES;
}
 
- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
}

So nothing too crazy going on here, I’ve setup some basic methods for setup, teardown, and handling touches. At this point I’m wanting to see some visual feedback though so I create a function to get some shapes simulating in chipmunk:

- (CCNode*) createBlockAt:(cpVect)pt
			width:(int)w
			height:(int)h
			mass:(int)mass
{
	cpShape *shape = [smgr addRectAt:pt mass:mass width:w height:h rotation:0];
	cpShapeNode *node = [cpShapeNode nodeWithShape:shape];
	node.color = ccc3(56+rand()%200, 56+rand()%200, 56+rand()%200);
 
	[self addChild:node];
 
	return node;
}

So you can see here I’m just creating a rectangle block with a given mass, attaching it to a cpShapeNode (which will just draw a primitive representation of the shape) and then adding that node to “self” aka the game’s visible layer. I use rand() here just to get some color variation going as I plan to call this function a bunch of times. Now that I have this function I can modify the init function to use it and tell SpaceManager to start simulating:

	[self createBlockAt:cpv(240,200) width:12 height:40 mass:100];
	[smgr start];


I add these two lines right after I allocate smgr in the init, and I run the app. Whoops! the block I created shows up but falls immediately off the screen. I guess I need some sort of containment rectangle around the screen. Add this call right before createBlockAt:

	[smgr addWindowContainmentWithFriction:1.0 elasticity:1.0 inset:cpvzero];

Phew! Upon running this time, the block falls down, bounces a bit, and comes to rest.

The black background is noticeable at this point so I pop one in at the bottom of init using an image I drew up real quick in Paint.NET and also ask smgr to add some static segment shapes to act as my physical terrain.

	CCSprite *background = [CCSprite spriteWithFile:@"smgrback.png"];
	background.position = ccp(240,160);
	[self addChild:background];
 
	[smgr addSegmentAtWorldAnchor:cpv(72,13) toWorldAnchor:cpv(480,13) mass:STATIC_MASS radius:1];
	[smgr addSegmentAtWorldAnchor:cpv(72,13) toWorldAnchor:cpv(72,133) mass:STATIC_MASS radius:1];
	[smgr addSegmentAtWorldAnchor:cpv(72,133) toWorldAnchor:cpv(0,133) mass:STATIC_MASS radius:1];
 
	[self addChild:[smgr createDebugLayer]];

So what’s this createDebugLayer you ask? Well since I did not attach any shapes to any CCNodes I do not get any visual feedback of what’s going on; adding a debug layer will automatically make visible anything in chipmunk that currently is not; this includes most constraints as well. This allows me to line up my segments with the background image perfectly.

Interactables

I realize the init function is starting to look a bit bloated, but for now I’ll wait to separate things out; because I want to get my ninja animals working first! So I create an Objective-C file again and make my header looks like this:

Ninja.h

#import "SpaceManagerCocos2d.h"
 
@class Game;
 
@interface Ninja : cpCCSprite
{
	Game *_game;
	int _damage;
}
 
+(id) ninjaWithGame:(Game*)game;
-(id) initWithGame:(Game*)game;
 
-(void) addDamage:(int)damage;
 
@end

cpCCSprite is a special type of CCSprite that is similar to cpShapeNode in that it will keep track of a shape and do all the necessary syncing with chipmunk. My plan is to make my Ninja’s basic circle shapes, my class definition looks like this:

Ninja.m

#import "Ninja.h"
#import "Game.h"
 
@implementation Ninja
 
+(id) ninjaWithGame:(Game*)game
{
	return [[[self alloc] initWithGame:game] autorelease];
}
 
-(id) initWithGame:(Game*)game
{
	cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:50 radius:9];
	[super initWithShape:shape file:@"elephant.png"];
 
	_game = game;
 
	//Free the shape when we are released
	self.spaceManager = game.spaceManager;
	self.autoFreeShape = YES;
 
	return self;
}
 
-(void) addDamage:(int)damage
{
	_damage += damage;
 
	if (_damage > 2)
	{
		[_game removeChild:self cleanup:YES];
	}
}
 
@end


There’s a few things going on here that I’ve just thrown at you, let’s discuss the init method. First of all I create the shape we’re going use, a circle of radius 9. Next I initialize with the shape and a file, I’ve chosen an elephant as the animal to use as you can tell by the filename. I then set the autoFreeShape property to true and because of this I must set the spaceManager as well. I also started creating an addDamage method, I basically want to be able to track any damage done and at some threshold the Ninja should be “destroyed”.

Now I need something to slingshot at these guys! I create another class similar to the Ninja’s……

Bomb.h:

#import "SpaceManagerCocos2d.h"
 
@class Game;
 
@interface Bomb : cpCCSprite
{
	Game *_game;
	BOOL _countDown;
}
 
+(id) bombWithGame:(Game*)game;
-(id) initWithGame:(Game*)game;
 
-(void) startCountDown;
-(void) blowup;
 
@end

Bomb.m:

#import "Bomb.h"
#import "Game.h"
 
@implementation Bomb
 
+(id) bombWithGame:(Game*)game
{
	return [[[self alloc] initWithGame:game] autorelease];
}
 
-(id) initWithGame:(Game*)game
{
	cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:STATIC_MASS radius:7];
	[super initWithShape:shape file:@"bomb.png"];
 
	_game = game;
	_countDown = NO;
 
	//Free the shape when we are released
	self.spaceManager = game.spaceManager;
	self.autoFreeShape = YES;
 
	return self;
}
 
-(void) startCountDown
{
	//Only start it if we haven't yet
	if (!_countDown)
	{
		_countDown = YES;
 
		id f1 = [CCFadeTo actionWithDuration:.25 opacity:200];
		id f2 = [CCFadeIn actionWithDuration:.25];
 
		id d = [CCDelayTime actionWithDuration:3];
		id c = [CCCallFunc actionWithTarget:self selector:@selector(blowup)];
 
		[self runAction:[CCRepeatForever actionWithAction:[CCSequence actions:f1,f2,nil]]];
		[self runAction:[CCSequence actions:d,c,nil]];
	}
}
 
-(void) blowup
{
	[self.spaceManager applyLinearExplosionAt:self.position radius:100 maxForce:4500];
	[_game removeChild:self cleanup:YES];
}

So the init function should look familiar at this point, and since I already know what the functionality of this bomb will be I implement a “startCountDown” function to basically “blink” the bomb and then call another function, “blowup”, after 3 seconds. The “blowup” function asks the SpaceManager to apply an explosion to all shapes within a radius, and also removes itself. Simple!

Collisions

Almost there, we just need way to launch the bombs at the elephant ninjas and handle a few collisions and such. Lets do the collision handling first. If you created your project through the cocos2d template, you should have a file named “GameConfig.h”; find some empty space before the “#endif” and put these defines in:

#define kNinjaCollisionType		1
#define kGroundCollisionType		2
#define kBlockCollisionType		3
#define kBombCollisionType		4

These are just integer values that chipmunk needs in order to sort out the different types of collision shapes. Now we have to go back to all the place’s we’ve created shapes and fill in their collision_type field appropriately. For example, the Bomb’s init function needs this line now:

shape->collision_type = kBombCollisionType;

Make sure Ninja, Blocks, and the segments we used for Ground also get their collision_type set appropriately. Now we must tell the SpaceManager we are interested in specific collisions, Ninja w/Ground, Ninja w/Block, etc. Put these lines in Game’s init function.

	[smgr addCollisionCallbackBetweenType:kNinjaCollisionType
					otherType:kGroundCollisionType
					   target:self
					 selector:@selector(handleNinjaCollision:arbiter:space:)
					  moments:COLLISION_POSTSOLVE,nil];
	[smgr addCollisionCallbackBetweenType:kNinjaCollisionType
					otherType:kBlockCollisionType
					   target:self
					selector:@selector(handleNinjaCollision:arbiter:space:)
					 moments:COLLISION_POSTSOLVE,nil];
	[smgr addCollisionCallbackBetweenType:kNinjaCollisionType
					otherType:kBombCollisionType
					   target:self
					 selector:@selector(handleNinjaCollision:arbiter:space:)
					  moments:COLLISION_POSTSOLVE,nil];
	[smgr addCollisionCallbackBetweenType:kBombCollisionType
					otherType:kGroundCollisionType
					   target:self
					 selector:@selector(handleBombCollision:arbiter:space:)
					  moments:COLLISION_POSTSOLVE,nil];
	[smgr addCollisionCallbackBetweenType:kBombCollisionType
					otherType:kBlockCollisionType
					   target:self
					 selector:@selector(handleBombCollision:arbiter:space:)
					  moments:COLLISION_POSTSOLVE,nil];

These calls will make SpaceManager call a ninja collision handler and a bomb collision handler immediately after a collision occurs between the types we specified. These two handlers need to be defined in Game:

-(BOOL) handleNinjaCollision:(CollisionMoment)moment arbiter:(cpArbiter*)arb space:(cpSpace*)space
{
	CP_ARBITER_GET_SHAPES(arb, ninjaShape, otherShape);
 
	//Get a value for "force" generated by collision
	float f = cpvdistsq(ninjaShape->body->v, otherShape->body->v);
 
	if (f > 600)
		[(Ninja*)ninjaShape->data addDamage:f/600];
	return YES;
}
 
-(BOOL) handleBombCollision:(CollisionMoment)moment arbiter:(cpArbiter*)arb space:(cpSpace*)space
{
	CP_ARBITER_GET_SHAPES(arb, bombShape, otherShape);
 
	[(Bomb*)bombShape->data startCountDown];
	return YES;
}

The first function will take care of adding damage to the Ninja by calculating a fake force value based on the velocities; if the value is greater than a threshold (600 seemed good) we pass it on as damage. The second function will just activate the bomb’s countdown when it hits anything. Collision stuff is done!

The next step is to actually setup some enemies and launch bombs at them! Setting up the enemies in Game’s init function is easy enough:

	Ninja *ninja = [Ninja ninjaWithGame:self];
	ninja.position = ccp(250,23);
	[self addChild:ninja z:5];

Bomb’s a slightly harder, we want to keep track of them so we create 2 more member variables in Game’s header file.

////
	NSMutableArray	*_bombs;
	Bomb			*_curBomb;
////

And back again in the init we initialize the array and setup our bombs.

	_bombs = [[NSMutableArray array] retain];
 
	for (int i = 0; i < 3; i++)
	{
		Bomb *bomb = [Bomb bombWithGame:self];
		bomb.position = ccp(10+i*16, 143);
		[self addChild:bomb z:5];
		[_bombs addObject:bomb];
	}
 
	[self setupNextBomb];

Make sure you put [_bombs release] in Game’s dealloc method as well. We also need to setup the “current” bomb so we add a function to do just that:

- (void) setupNextBomb
{
	if ([_bombs count])
	{
		_curBomb = [_bombs lastObject];
 
		//move it into position
		[_curBomb runAction:[CCMoveTo actionWithDuration:.7 position:ccp(60,166)]];
 
		[_bombs removeLastObject];
	}
	else
		_curBomb = nil;
}

Here we look to see if any bombs are in the queue and move one into position if there is at least one. I suppose some of you are wondering how I’ve decided on what positions to use in both these previous two snippets. Well this next snippet might explain a bit, it also can go in the Game’s init function for now:

	CCSprite *sling1 = [CCSprite spriteWithFile:@"sling1.png"];
	CCSprite *sling2 = [CCSprite spriteWithFile:@"sling2.png"];
 
	sling1.position = ccp(60,157);
	sling2.position = ccpAdd(sling1.position, ccp(5,10));
 
	[self addChild:sling1 z:10];
	[self addChild:sling2 z:1];

It’s the slingshot! I placed it in the middle left of the screen on the ledge; the “current” bomb is placed in the slingshot, and the other bombs are lined up beside it. We don’t really need a shape for the slingshot itself so it just remains two sprites with differing z orders so that the bomb gets launched “through” it.

To make this slingshot work, we need but only to implement the touch functions on the Game layer.

-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event
{
	CGPoint pt = [self convertTouchToNodeSpace:touch];
	float radiusSQ = 25*25;
 
	//Get the vector of the touch
	CGPoint vector = ccpSub(ccp(60,157, pt);
 
	//Are we close enough to the slingshot?
	if (ccpLengthSQ(vector) < radiusSQ)
		return YES;
	else
		return NO;
}

First of all we implement the began function, we only want to detect at this point if the touch is near the slingshot; if it is we return YES to indicate we’d like to accept the touch. Next is the move function:

-(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent *)event
{
	CGPoint pt = [self convertTouchToNodeSpace:touch];
	CGPoint bombPt = ccp(60,166);
 
	//Get the vector, angle, length, and normal vector of the touch
	CGPoint vector = ccpSub(pt, bombPt);
	CGPoint normalVector = ccpNormalize(vector);
	float angleRads = ccpToAngle(normalVector);
	int angleDegs = (int)CC_RADIANS_TO_DEGREES(angleRads) % 360;
	float length = ccpLength(vector);
 
	//Correct the Angle; we want a positive one
	while (angleDegs < 0)
		angleDegs += 360;
 
	//Limit the length
	if (length > 25)
		length = 25;
 
	//Limit the angle
	if (angleDegs > 245)
		normalVector = ccpForAngle(CC_DEGREES_TO_RADIANS(245));
	else if (angleDegs < 110)
		normalVector = ccpForAngle(CC_DEGREES_TO_RADIANS(110));
 
	//Set the position
	_curBomb.position = ccpAdd(bombPt, ccpMult(normalVector, length));
}

We calculate the bomb’s position as you move your touch, we limit both angle and length away from the slingshot. And finally the end function:

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
	CGPoint vector = ccpSub(ccp(60,166), _curBomb.position);
 
	if (_curBomb)
		[smgr morphShapeToActive:_curBomb.shape mass:30];
 
	[_curBomb applyImpulse:cpvmult(vector, 240)];
 
	[self setupNextBomb];
}

We grab the vector between our launch pt and the bomb’s position, we turn the current bomb’s mass from static (unmoving) to an active mass of 30, and then finally we use the vector to apply an impulse that’ll launch the bomb. We also call our function we made earlier, setupNextBomb, to move the next bomb into position. Give it a try!

We now have the working essence of a game, you can launch bombs, there’s obstacles, and there’s enemies that can be destroyed. I also added a bit of touchup such as “poof” clouds when the enemy dies and particles to the explosion that I won’t discuss here because this article is already getting huge.

Serialization

So our huge init function needs to be broken up first, I create 4 discrete functions to handle all the setup.

- (void) setupShapes;		//setup terrain and blocks
- (void) setupEnemies;		//setup the enemies
- (void) setupBackground;		//setup background image and slingshot
- (void) setupBombs;		//setup the bombs

I leave all the SpaceManager and collision setup in the init, but below I now morph all that junk into something looks a lot more readable:

	[self setupBackground];
 
	//Try to load it from file, if not then create from scratch
	if (!([smgr loadSpaceFromUserDocs:"saved_state.xml" delegate:self]))
	{
		[self setupBombs];
		[self setupEnemies];
		[self setupShapes];
	}
 
	[self setupNextBomb];

As you can see , I ask SpaceManager to load up the chipmunk space from a file; if it fails or doesn’t exist then we create it manually. Notice that we pass a delegate to the load function and that delegate is self, meaning we are able to capture certain events now.

-(BOOL) aboutToReadShape:(cpShape*)shape shapeId:(long)id
{
	if (shape->collision_type == kBombCollisionType)
	{
		Bomb *bomb = [Bomb bombWithGame:self shape:shape];
		[self addChild:bomb z:5];
 
		if (cpBodyGetMass(shape->body) == STATIC_MASS)
			[_bombs addObject:bomb];
	}
	else if (shape->collision_type == kNinjaCollisionType)
	{
		Ninja *ninja = [Ninja ninjaWithGame:self shape:shape];
		[self addChild:ninja z:5];
	}
	else if (shape->collision_type == kBlockCollisionType)
	{
		cpShapeNode *node = [cpShapeNode nodeWithShape:shape];
		node.color = ccc3(56+rand()%200, 56+rand()%200, 56+rand()%200);
		[self addChild:node z:5];
	}
 
	//This just means accept the reading of this shape
	return YES;
}

Since we only care about the reading of shapes, we implement this function; identifying the shapes merely by their collision_type. A STATIC_MASS bomb means its in the queue, so we add it to the array. You may notice that both Bomb and Ninja are calling functions we did not write…. yet. Since they are similar I will just give you the Bomb’s changes:

+(id) bombWithGame:(Game*)game shape:(cpShape*)shape
{
	return [[[self alloc] initWithGame:game shape:shape] autorelease];
}
 
-(id) initWithGame:(Game*)game
{
	cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:STATIC_MASS radius:7];
	return [self initWithGame:game shape:shape];
}
 
-(id) initWithGame:(Game*)game shape:(cpShape*)shape;
{

So the init now takes a shape, but regular old init will create a shape if called, this lets the read code work…. am I forgetting something? Oh right I need a save function.

-(void)save
{
	[smgr saveSpaceToUserDocs:"saved_state.xml" delegate:self];
}

There! I can call that at any time now and save down the state, and the init will reload it if it is there.

Retina Mode

Well this is a toughie, In the app delegate I usually find the line where FPS is enabled and put this line after it:

[director enableRetinaDisplay:YES];

That’s it! SpaceManager and Chipmunk are indifferent to the change because the point scale does not change. Of course make sure you have the “-hd” on all your hd images which should be twice as large as the originals. You will notice that the simulator runs a bit slower (30 FPS) and since we’re using a constant dt in SpaceManager it will cause the simulation to look slow.

Closing Notes

The included example source has a little bit more that added to it than what I discussed in the article, just to make it more complete and playable. Here’s a list of what else is in there:

- Added a few more block functions to make pillared structures and triangles (for roofs)
- Added a restart button
- Added configurable defines for many of the positions and radii.
- Keep track of enemies killed so that a “You Win” appears when all are dead
- “poof” clouds and particles add to ninja and bomb for special effect

Some notes of what else could go in or be changed:

- A way to load different levels (XML files perhaps?)
- Different Bomb and Ninja types
- The slingshot and touch code could be better encapsulated

Check out the example source, here’s a video of it in action!


Viewing all articles
Browse latest Browse all 14

Trending Articles