Programming iPhone Games with Cocos2D Part 4

Cocos2D gives you a huge amount of features and all that stuff you need and want in your game. But one thing is really missing – a simple collision detection between sprites. In this article I want to give you some hints how you could implement your own CCSprite collision detection routine without using box2d or other physic libraries.

Update Calls

Ok, lets say you want to detect a collision between two objects A and B by checking one with the other. One way would be to enhance your CCSprite with an update routine which will detect its collisions by checking collision with all other children. The pseudo-code would look like:

// assuming we are implementing an update routine 
// for an extended CCSprite class, lets say: CCSpriteExt.m
-(void) onUpdate:(ccTime) delta
{
	for (CCNode * you in children)
	{
		if ( you == self) continue;
 
		if ( [self isCollidingWithObject:you] ) 
		{
			// collision detected !
			[collisionDelegate performSelector:collisionSelector withObject:self withObject:you];
		}
	}
}

Lets say you have 5 Objects (A,B,C,D and E) and you use the above code to check the collisions, you will count 20 check calls where 10 calls are double :

AB, AC, AD, AE -> 4 checks, 0 double checks
BA, BC, BD, BE -> 4 checks, 1 double checks: AB
CA, CB, CD, CE -> 4 checks, 2 double checks: AC, BC
DA, DB, DC, DE -> 4 checks, 3 double checks: AD, BD, CD
EA, EB, EC, ED -> 4 checks, 4 double checks: AE, BE, CE, DE
20 total checks, 10 double checks

If you have many objects this really would take time.

Another solution would be one update call for all you objects. Like if you have a game scene or something which allready has an update call, you could place your collision detection there and write a more efficiant object loop:

// assuming we are implementing an udate routine in a CCScene or CCLayer class
..
for (int idxMe=0; idxMe < [children_ count]; idxMe++ )
{
	CCNode * me = (CCNode *) [children_ objectAtIndex:idxMe];
	for (int idxYou=idxMe+1; idxYou < [children_ count]; idxYou++ )
	{		
		CCNode * you = (CCNode *) [children_ objectAtIndex:idxYou];
 
		if ( [self isCollidingObject:Me WithObject:you] ) 
		{
			// collision detected !
			[collisionDelegate performSelector:collisionSelector withObject:me withObject:you];
		}
	}
}

So now you would’nt have any double check calls:

AB, AC, AD, AE -> 4 checks, 0 double checks
BC, BD, BE -> 3 checks, 0 double checks
CD, CE -> 2 checks, 0 double checks
DE -> 1 checks, 0 double checks
10 total checks, 0 double checks

if you have objects in your scene which you dont want to test in your collision detection or even better if you want to have collision groups you have to advance your CCSprite with a collisionGroupID. To be sure the collision detection will be as fast as possible you should check the collisionGroupID before you check the collision it self :

// assuming we are implementing an udate routine in a CCScene or CCLayer class
..
for (int idxMe=0; idxMe < [children_ count]; idxMe++ )
{
	CCNode * meNode = (CCNode*) [children_ objectAtIndex:idxMe];
 
	if ( ! [meNode isKindOfClass:[CCSpriteExt class]] )
	{
		continue;
	}
 
	CCSpriteExt * me = (CCSpriteExt *) meNode;
 
	for (int idxYou=idxMe+1; idxYou < [children_ count]; idxYou++ )
	{		
		CCNode * youNode = (CCNode*) [children_ objectAtIndex:idxYou];
		if ( ! [youNode isKindOfClass:[CCSpriteExt class]] )
		{
			continue;
		}
 
		CCSpriteExt * you = (CCSpriteExt *) youNode;
 
		if ( (me.collisionID == you.collisionID) && 
		    [self isCollidingObject:Me WithObject:you] )
		{
			// collision detected !
			[collisionDelegate performSelector:collisionSelector withObject:me withObject:you];
		}
	}
}

Now take a deep breath before we take a closer look into the method isCollidingObject:WithNode:. To keep the performance and avoid lags you should know how many collision calls you could have and how time intensive they are - even if you optimize your collision detection method pretty much.

Collision Detection Method

You should start by testing a bounding box even if you want a pixel by pixel collision detection to save cpu time. May be its a good idea to setup a collision type for each CCSpriteExt and when detecting the collision you could choose the best algorithm for it. Lets say there are 4 collision body types: BOUNDING_BOX, BOUNDING_SPHERE, POLYGON and PIXEL. All of them starts with bounding box since this is a really easy thing for the cpu. If you are not rotating your sprites you could use CCSprite.contentSize and CCSprite.anchorPoint to calculate your bounding box. You could also use CCSprite.boundingBox which returns the current transformed bounding box.

To keep all colliding stuff in one method you should shift the code "(me.collisionID == you.collisionID)" into this function, too and remove it from the collision test loop. To make this example not to difficult I will just test collisions with the same collision body type and I will not implement a pixel by pixel test routine here.

-(BOOL) isCollidingObject:(CCSpriteExt*)obj1 WithObject:(CCSpriteExt*) obj2
{
	if ( obj1.collisionID == obj1.collisionID )
	{
		if ( obj1.collisionBodyType != obj2.collisionBodyType) 
		{
			// not supported yet.
			return NO;
		}
		switch (obj1.collisionBodyType )
		{
			case BOUNDING_BOX : 
			return [self isCollidingBox:obj1 WithBox:obj2]; 
 
			case BOUNDING_SPHERE :
			return [self isCollidingBox:obj1 WithBox:obj2] && // first boxed test!
			[self isCollidingSphere:obj1 WithSphere:obj2];
 
			case POLYGON : 
			return [self isCollidingBox:obj1 WithBox:obj2] && // first boxed test!
			[self isCollidingPolygon:obj1 WithPolygon:obj2];
 
			case PIXEL : 
			return [self isCollidingBox:obj1 WithBox:obj2] && // first boxed test!
			[self isCollidingPixel:obj1 WithPixel:obj2];
 
			default:
			// not supported type
			break;
		}
	}
	return NO;
}

Since all collision tests need a boundingbox test first lets take a look into its method. Basically you want to check if a bounding box intersects with another bounding box. If you dont have transformed bounding boxes you could check the intersection even more efficiant. Two sprites are colliding if the vertical distances between anchor points and edges of both sprites are smaller than the vertical distance between them and the horizontal distances between anchor points and edges of both sprites are smaller than the horizontal distance between them.

Non-transformed bounding boxes goes here :

-(BOOL) isCollidingBox : (CCSpriteExt *) obj1 WithBox: (CCSpriteExt *) obj2 
{	
	float dx = fabs(obj2.position.x - obj1.position.x);
	float dy = fabs(obj2.position.y - obj1.position.y);	
 
	float dx1;
	float dx2;
 
	if (obj2.position.x > obj1.position.x) 
	{
		dx1 = (1-obj1.collisionSprite.anchorPoint.x) * obj1.collisionSprite.contentSize.width * obj1.collisionSprite.scaleX;
		dx2 = obj2.collisionSprite.anchorPoint.x * obj2.collisionSprite.contentSize.width * obj2.collisionSprite.scaleX;
	}
	else 
	{
		dx1 = (obj1.collisionSprite.anchorPoint.x) *  obj1.collisionSprite.contentSize.width * obj1.collisionSprite.scaleX;
		dx2 = (1-obj2.collisionSprite.anchorPoint.x) *obj2.collisionSprite.contentSize.width * obj2.collisionSprite.scaleX;
	}
 
	float dy1;
	float dy2;
 
	if (obj2.position.y < obj1.position.y) 
	{
		dy1 = obj1.collisionSprite.anchorPoint.y * obj1.collisionSprite.contentSize.height * obj1.collisionSprite.scaleY;
		dy2 = (1 - obj2.collisionSprite.anchorPoint.y) * obj2.collisionSprite.contentSize.height * you.collisionSprite.scaleY;
	}
	else 
	{
		dy1 = (1 - obj1.collisionSprite.anchorPoint.y) * obj1.collisionSprite.contentSize.height * obj1.collisionSprite.scaleY;
		dy2 = (obj2.collisionSprite.anchorPoint.y) * obj2.collisionSprite.contentSize.height * obj2.collisionSprite.scaleY;
	}
 
	return !((dx > (dx1+dx2)) || (dy > (dy1+dy2)));
}

But if you have transformed bounding boxes use cocos boundingbox function and CGRechtIntersection to test the collision - and even more it is human readable ;-) :

-(BOOL) isCollidingBox : (CCSpriteExt *) obj1 WithBox: (CCSpriteExt *) obj2 
{
	return !CGRectIsNull( CGRectIntersection(obj1.boundingBox, obj2.boundingBox) );	
}

The spherical collision detection needs a radius to test the collision. A collision is detected if the distance between two objects is smaller than the sum of radius. If you dont want to enhance CCSprite with a radius use CCSprite.contentSize.width/2 or CCSprite.boundingBox.width/2 or what ever.

-(BOOL) isCollidingSphere:(CCSpriteExt*) obj1 WithSphere:(CCSprite *) obj2
{
	float minDistance = object.radius + self.radius;
	float dx = obj2.position.x - obj1.position.x;
	float dy = obj2.position.y - obj1.position.y;
	if (! (dx > minDistance || dy > minDistance) )
	{
		float actualDistance = sqrt( dx * dx + dy * dy );
		return (actualDistance < = minDistance);
	}
	return NO;
}

If you dont want mix up CCSprite with your collision detection code you could also try to create a new class "CCCollisionBody" and put all the collision attributes like "radius" and "collisionBodyType" there and also a reference to your sprite so you can access the position and dimensions of your sprite :

//
//  CCCollisionBody.h
//
 
/**
 * collision body types
 * @version 1.0
 */
#define COLLISIONBODYTYPE_NONE		0
#define COLLISIONBODYTYPE_POINT		1
#define COLLISIONBODYTYPE_LINE		2
#define COLLISIONBODYTYPE_SPHERE	3
#define COLLISIONBODYTYPE_RECT		4
#define COLLISIONBODYTYPE_POLY		5
#define COLLISIONBODYTYPE_PIXEL		6
 
@class CCNode; // pre decleration of CCNode, import will be in .m file
/** 
 * A class to define the collision body for a proxy game object.
 * collisionbodys are classified by types. Not all types are yet defined to collide with each other.
 * please use sphere and rect collisionbodys only to ensure proper functionallity
 */
@interface CCCollisionBody : NSObject 
{
	CCNode * proxy; // proxy object 
	uint type; // defines the collision body type (see above)
	CGSize size; // if used by the type : defines the dimensions of the collision body.
	float radius; // if used by the type : defines the radial dimensions of the collision body.
	BOOL disabled; // if true the collions are deactivated otherwise collisions with this body will be checked.
	int collisionGroupID; // collision group ID - only objects of the same group IDs will collide
}
@property (nonatomic, assign) int collisionGroupID;
@property (nonatomic, assign) bool disabled;
@property (nonatomic, retain) CCNode * proxy;
@property (nonatomic, assign) uint type;
@property (nonatomic, assign) CGSize size;
@property (nonatomic, assign) float radius;
 
/**
 * creates a spherical collision body specified by a given radius and a proxy game object.
 */
-(id) initAsSphereWithProxy:(CCNode*) proxyObject Radius: (float) sphereRadius;
 
/**
 * creates a rectangle collision body specified by a given size and a proxy game object.
 */
-(id) initAsRectWithProxy:(CCNode*) proxyObject Size:(CGSize) rectSize;
 
/**
 * returns if a collision body collides with another collision body.
 * v1.0 only supports sphereical and rectangle collisionbodys.
 */
-(bool) collidesWith:(CollisionBody*) object;
 
@end
//
//  CollisionBody.m
//
 
#import "CCCollisionBody.h"
#import "cocos2d.h"
 
#pragma mark -
#pragma mark CCCollisionBody Implementation
@implementation CCCollisionBody
@synthesize radius, size, type, proxy, disabled, collisionGroupID;
 
#pragma mark -
#pragma mark Initialisierung
 
#pragma mark  
#pragma mark COLLISIONBODYTYPE_NONE : no collision
 
-(id) initWithProxy:(CCNode*) proxyObject
{
	self = [super init];
	if (self)
	{
		self.type = COLLISIONBODYTYPE_NONE;
		self.proxy = proxyObject;
	}
	return self;
}
 
#pragma mark  
#pragma mark COLLISIONBODYTYPE_RECT : bounding box
-(id) initAsRectWithProxy:(CCNode*) proxyObject Size:(CGSize) rectSize
{
	if ([self initWithProxy:proxyObject])
	{
		self.size = rectSize;
		self.type = COLLISIONBODYTYPE_RECT;
	}
	return self;
}
 
#pragma mark  
#pragma mark COLLISIONBODYTYPE_SPHERE : bounding sphere
-(id) initAsSphereWithProxy:(CCNode*) proxyObject Radius: (float) sphereRadius;
{
	if ([self initWithProxy:proxyObject])
	{
		self.radius = sphereRadius;
		self.type = COLLISIONBODYTYPE_SPHERE;
	}
	return self;
}
 
#pragma mark -
#pragma mark cleanup
 
-(void) dealloc
{
	self.proxy = nil;
	[super dealloc];
}
 
#pragma mark -
#pragma mark collision tests
 
#pragma mark  
#pragma mark Rect-Rect collision 
-(bool) isRectRectIntersection:(CollisionBody*) object
{
	return !CGRectIsNull( CGRectIntersection(proxy.boundingBox, object.proxy.boundingBox) );	
}
 
#pragma mark  
#pragma mark Sphere-Sphere collision 
-(bool) isSphereSphereIntersection:(CollisionBody*) object
{
	float minDistance = object.radius + self.radius;
	float dx = object.proxy.position.x - self.proxy.position.x;
	float dy = object.proxy.position.y - self.proxy.position.y;
	if (! (dx > minDistance || dy > minDistance) )
	{
		float actualDistance = sqrt( dx * dx + dy * dy );
		return (actualDistance < = minDistance);
	}
	return false;
}
 
 
 
#pragma mark  
#pragma mark main collision detection
-(bool) collidesWith:(CollisionBody*) object
{
	if (!self.disabled && !object.disabled && self.collisionGroupID == object.collisionGroupID)
	{
		if (self.type == COLLISIONBODYTYPE_RECT && object.type == COLLISIONBODYTYPE_RECT)
		{
			return [self isRectRectIntersection:object];
		}
 
		if (self.type == COLLISIONBODYTYPE_SPHERE && object.type == COLLISIONBODYTYPE_SPHERE)
		{
			return [self isSphereSphereIntersection:object];
		}
 
		// NOT SUPPORTED COLLISION TYPE!
	}
 
	return NO;
}
 
@end

About this entry