cocos2d move along a curve / bezier animation

I wanted to do a few things when displaying the points for an upcoming match 3 / bubble pop game I’m making:

1) After some bubbles are popped, add a points label at the position popped on the HUD layer. So we should have a delay before beginning this animation.
2) Animate this along a curve moving upward waving left and right
3) after traveling along the curve, scale the label to shrink it before it disappears
4) remove the label when finished

Some considerations:
– I will pass the method the position in the game layer to this method which is on the hud layer, and so we should convert the position using convertToWorldSpace as seen.

Implementation:

-(void) displayMsgWithPos:(CGPoint)sourcePos msg:(NSString*)str delay:(float)delay fontFile:(NSString*)fontFile
{
	CGSize s = [[CCDirector sharedDirector] winSize];
	CGPoint worldPos = [game_ convertToWorldSpace:sourcePos];
	
	CCLabelBMFont *label = [CCLabelBMFont labelWithString:str fntFile:fontFile];
	[label setPosition:worldPos];
	label.visible = NO;
	[self addChild: label];
	ccBezierConfig curve;
	curve.controlPoint_1 = ccp(worldPos.x -10, worldPos.y+10);
	curve.controlPoint_2 = ccp(worldPos.x +10, worldPos.y+20); 
	curve.endPosition = ccp(worldPos.x, worldPos.y+30);	
	
	id delayAction = [CCDelayTime actionWithDuration:delay];
	id addLabelAction = [CCShow action];
	id bezierMoveAction = [CCBezierTo actionWithDuration:1.0 bezier:curve];
	id scaleAction = [CCScaleTo actionWithDuration:0.5 scale:0.2];
	id actions = [CCSequence actions: delayAction, addLabelAction, bezierMoveAction, scaleAction, [CCCallFunc actionWithTarget:label selector:@selector(removeFromParentAndCleanup:)],nil];
	[label runAction:actions];
}

Follow me on google+ / twitter

Have some questions about game development with cocos2d?
Keep up to date on my newest games, and keep me up to date about yours.

google plus: gplus.to/dashiellgames

twitter: twitter.com/dashiellgames

match 3 / bust-a-move algorithm to match connecting same colored bubbles

Probably the most difficult parts about making a puzzle bobble/bust-a-move clone is 1) determining when 3 or more of the same color nodes in the grid are matched and removing them and then 2) determining which nodes are no longer connected to the grid after a match/removal has been made.

In my match 3 game, the player fires a bubble and when the bubble hits a target, we check if it has hit to the left/right and spawn on the left/right (of course also check to make sure if it’s the right most / left most node with no room).  Then spawn a new target node at this point.  All target nodes are stored in a 1d sparse array.

When the target node is spawned, we want to check if there is a match of 3 or greater nodes (including this newly spawned one).

The steps for this are:

  1. Get the surrounding nodes indeces (indexes in the 1d sparse array)
  2. Loop through each index
  3. Check if the index contains a target, and get a pointer to it.
  4. If a target exists, get it’s color (in my example, this is the enemyNum property)
  5. If the target can match any color (in my example, GameRoleMultiEnum) set it to the color of the newly spawned ball’s color (heroNum)
  6. Check if the target’s color matches the newly spawned color and make sure it’s not already added to a list to die.
  7. If there is a match in step 6, add the target to a list to be killed.

In my example, I am storing the array of targets to be killed in a dictionary (dict_) with the key as the color (enemyNum) .  The reason for this is because after using this method, I call another method, clearEnemies.  In clearEnemies, we check the count of the array stored in dict_ for the color key.  If it’s count is >= 3, we clear those enemies.  If it’s not, we don’t.

Some other notes about the code:

As this is a puzzle bobble/bust-a-move type of game, I want the effect whereby enemies/targets will be destroyed in a chain.  This means that if a new target /bubble is spawned in the middle of 6 other bubbles, they will be destroyed from the center, starting with the newly spawned, then the left/right nodes will be destroyed, and so on.  Just as they are matched in the recursive method.  To do this, we keep track of the iterations in the recursive method.  With each iteration, the delayToRemove the bubble increases.

Example implementation:

-(void) flagEnemiesWithNumber:(int)heroNum index:(int)index iter:(int)iter
{
	NSMutableArray *eToKillArr = [dict_ objectForKey:[NSNumber numberWithInt:heroNum]];

	if(iter == 0) {

		Enemytarget *enemy = [arrayEnemyTargets_ objectAtIndex:index];
		enemy.delayToRemove = 0.05;
		[eToKillArr addObject:enemy];
	}
	iter++;

	NSMutableArray *surroundingIndices = [self getSurroundingIndices:index];

	for(NSNumber *num in surroundingIndices) {
		int indexNum = [num intValue];

		id obj = [arrayEnemyTargets_ objectAtIndex:indexNum];
		if(obj != [NSNull null]) {
			Enemytarget *t = obj;

			int enemyNum = t.enemyNum;

			if(enemyNum == GameRoleMultiEnum)
				enemyNum = heroNum;

			if(enemyNum == heroNum && ![eToKillArr containsObject:t] /* && !t.isDead */) {
				t.delayToRemove = iter * 0.05 + 0.05;
				[eToKillArr addObject:t];
				[self flagEnemiesWithNumber:heroNum index:t.index iter:iter];
			}
		}
	}

}

-(int) clearEnemies
{
	int totalKilled = 0;
	for(id key in dict_) {
		NSMutableArray *eToKillArr = [dict_ objectForKey:key];
		int numKilled = [eToKillArr count];

		if(numKilled >= 3) {
			for(Enemytarget *t in eToKillArr) {
				[self removeEnemy:t delay:t.delayToRemove showPopAnim:YES];
				totalKilled++;
			}
		}
		[eToKillArr removeAllObjects];
	}

	return totalKilled;
}

The reason we are keeping track of the total number killed is that we only want to begin an animation to clear unconnected nodes after all of the matched enemies have been cleared.

You will remember the second major problem in developing this kind of game: 2) determining which nodes are no longer connected to the grid after a match/removal has been made.

This can be achieved using virtually the same recursive method to match same colored bubbles.  But for this, we want to begin our search at the top most node, and match any neighboring bubble not already flagged as matched, flag the bubble as matched, and call ourself with this new bubbles index.  After this is finished, we can just loop through all of the bubbles, and any bubble not flagged as matched can be removed.

cocos2d add free memory display to showFPS

Replace CCDirector.m with the content:

http://go.pastie.org/2187329

This is for cocos2d 1.0.0-beta.  If using another version, use a diff tool and integrate relevant changes.

The modified showFPS method will also display free memory in MB with the frame rate.

cocos2d, run an animation then remove the sprite

The interesting part here is

[CCCallFunc actionWithTarget:fixedSprite selector:@selector(removeFromParentAndCleanup:)]

Most online examples will add a new callback method and pass the sprite to it, then remove from within the callback.

You can see clearly also how to add an animation over an existing sprite by setting (the class used is a subclass of CCSprite, so self refer’s to a sprite). All sprite’s have a contentSize, so positioning it is clean.

-(void) animateAndRemove
{
	CCSprite *fixedSprite = [CCSprite spriteWithSpriteFrameName:@"fixed_01.png"];

	fixedSprite.position = ccp(self.contentSize.width/2,self.contentSize.height/2);
	[self addChild:fixedSprite];

	NSMutableArray *animFrames = [NSMutableArray array];
	for(int i = 1; i <= 6; ++i) {

		[animFrames addObject: [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName: [NSString stringWithFormat:@"fixed_%02d.png", i]]];
	}

	CCAnimation *anim = [CCAnimation animationWithFrames:animFrames delay:0.05f];
	CCActionInterval *animAction = [CCAnimate actionWithAnimation:anim restoreOriginalFrame:NO];
	id seq = [CCSequence actions: animAction, [CCCallFunc actionWithTarget:fixedSprite selector:@selector(removeFromParentAndCleanup:)], nil];
	[fixedSprite runAction:seq];
}

box2d preSolveContact

if you have similar requirements:

– can’t use a sensor, because you want the object to still react to some other objects in the world
– can’t use collision filter, because you want to listen to the contact

you can use preSolveContact.

An example, where I want to remove the node as I’m about to hit it. Unlike beginContact, preSolve may be called many times on a single colision, so once we have the class from the presolve contact node’s userData, we set a isDead property of that class to true so that we don’t try and remove the same node from the box2d world many times.

Example:

-(void) preSolveContact:(b2Contact*)contact  manifold:(const b2Manifold*) oldManifold
{
	b2WorldManifold worldManifold;
	contact->GetWorldManifold(&worldManifold);
	b2Fixture *fixtureA = contact->GetFixtureA();
	b2Fixture *fixtureB = contact->GetFixtureB();
	NSAssert( fixtureA != fixtureB, @"preSolveContact bug");
	
	b2Body	*bodyA = fixtureA->GetBody();
	b2Body	*bodyB = fixtureB->GetBody();
	NSAssert( bodyA != bodyB, @"preSolveContact bug");
	b2Body *otherBody = (bodyA == body_) ? bodyB : bodyA;
	b2Fixture *otherFixture = (bodyA == body_) ? fixtureB : fixtureA;
	
	if( !otherFixture->IsSensor() ) {
		BodyNode *bNode = (BodyNode*)otherBody->GetUserData();
		Class tClass = [Tank class];
		
		if([bNode isKindOfClass:tClass])
		{
			contact->SetEnabled(false);
			Tank *t = (Tank*)bNode;
			
			if(!t.isDead) {
				t.isDead = YES;
				[arrayTanksToRemove_ addObject:t];
			}
		}	
	}
}

pass a native object to selector withObject

In this case, I wanted to pass a CGPoint object.  This can be completed using [NSValue valueWith..]

-(void) removeBadGuy:(BadGuyNode*)badGuy delay:(float)delay
{

    b2Vec2 pos = [badGuy body]->GetPosition();
    CGPoint cPos = CGPointMake(pos.x * kPhysicsPTMRatio, pos.y * kPhysicsPTMRatio);
    NSValue *vPos = [NSValue valueWithCGPoint:cPos];
    [self increaseBadGuysWaitingForRemoval];
    id bgRemovedAnimation = [CCCallFuncO actionWithTarget:self selector:@selector(badGuyRemovedAnimation:) object:vPos];
    id removeBadGuy = [CCCallFuncO actionWithTarget:self selector:@selector(removeBadGuy:) object:badGuy];
    id decrWaitingCount = [CCCallFuncO actionWithTarget:self selector:@selector(decreaseBadGuysWaitingForRemoval)];

    id seq = [CCSequence actions: [CCDelayTime actionWithDuration:delay], badGuyRemovedAnimation, removeBadGuy, decrWaitingCount, nil];
    [game_ runAction: seq];
}