Tiny Wings Hills with Cocos2D

Not only Tiny Wings is a fun and popular game, but it has also blown our developers mind with its gorgeous hills made of procedural graphics.

I’ve spent a few days trying to make something similar with Cocos2D. I’ll share in this article a piece of code that is responsible for drawing the hills. A bit of OpenGL ES knowledge is necessary to understand it. If you want to see the code in action, there’s a free game (work in progress) using it called Space Dragons (available on the appstore for iPhone4, iPad and over).

I’ve called this Cocos2D/OpenGL object CCSpriteBiCurve. The code is forked from CCSprite.

The idea behind CCSpriteBiCurve, is to draw a texture with the top and bottom edges being curved. In the case of Tiny Wings hills, the top edge only is curved.






The initial approach was to draw one horizontal band (an OpenGL GL_TRIANGLE_STRIP command). Unfortunately, one band is not enough to get a perfectly smooth result. The final code splits the texture in N horizontal bands, N being a parameter of the object.

Note that this code doesn’t deal with the procedural generation of the striped texture. This is a totally different matter. I’ll share later a piece of code for this too (be notified first). You can see a preview of the procedural stripes here.

The texture used in Space Dragons was generated with stripegenerator.com. Noise and lights were added with Photoshop. So here is the code. Starting with an example of how to use CCSpriteBiCurve in your projects. It’s not optimized and not very well commented. Please don’t hesitate to post your questions/suggestions. Note that the width of the texture must be n a power of 2 (GL_REPEAT mode).
[updated] Here is XCode sample project kindly provided by Eko Mirhard (it’s based on Cocos2D 0.99.5).

//Object creation, from a texture/file "stripe0.png"
CCSpriteBiCurve *bicurve=[[[CCSpriteBiCurve alloc] initWithFile:@"stripe0.png"] autorelease];
[self addChild:bicurve]; //self is a CCNode


//Creation of the bottom and top curves
int nbPoints=11; //Number of points on each curve
int xInc=32; //X increment for the curve creation. The lower it is, the smoothest the curve is.
CGPoint *bPoints,*tPoints; //C arrays of CGPoints defining the bottom and top curves
int nbBands=4; //Number of bands the texture will be splited in


//Memory allocation for the CGPoint arrays
bPoints=(CGPoint*)malloc(nbPoints*sizeof(CGPoint));
tPoints=(CGPoint*)malloc(nbPoints*sizeof(CGPoint));
//We use a sinus for the top curve and a straight line for the bottom curve
for (int i=0;i<nbPoints;i++) {
	bPoints[i]=ccp( i*xInc , 0 );
	tPoints[i]=ccp( i*xInc , 250+sin((double)i*2*M_PI/(double)nbPoints)*50.0f );
}


//Send the curves parameters to the CCSpriteBiCurve object
[bicurve setBiCurve:tPoints :bPoints :nbPoints :nbBands];
//CCSpriteBiCurve.h

#import 
#import "cocos2d.h"

@interface CCSpriteBiCurve : CCNode  {
	int pointsNb,bandsNb,vertexDataCount;
	ccV2F_C4F_T2F *vertexData;
	
	//
	// Data used when the sprite is self-rendered
	//
	ccBlendFunc blendFunc_; // Needed for the texture protocol
	CCTexture2D *texture_; // Texture used to render the sprite
	
	// Texture rects
	CGRect rect_;
	CGRect rectInPixels_;
	
	// opacity and RGB protocol
	GLubyte opacity_;
	ccColor3B	color_;
	ccColor3B	colorUnmodified_;
	BOOL opacityModifyRGB_;
}
-(id) initWithFile:(NSString*)filename;
/** conforms to CCTextureProtocol protocol */
@property (nonatomic,readwrite) ccBlendFunc blendFunc;
/** updates the texture rect of the CCSprite in points. */
-(void) setTextureRect:(CGRect) rect;
/** Set the parameters of the top and bottom curves **/
-(void)setBiCurve:(CGPoint*)topCurvePoints :(CGPoint*)bottomCurvePoints :(int)nbPoints :(int)nbBands;
@end
//CCSpriteBiCurve.m
#import "CCSpriteBiCurve.h"

@implementation CCSpriteBiCurve

@synthesize blendFunc = blendFunc_;

-(id) init {
	if( (self=[super init]) ) {
		opacityModifyRGB_ = YES;
		opacity_ = 255;
		color_ = colorUnmodified_ = ccWHITE;
		
		blendFunc_.src = CC_BLEND_SRC;
		blendFunc_.dst = CC_BLEND_DST;
		
		// update texture (calls updateBlendFunc)
		[self setTexture:nil];
		
		// Default transform anchor: top left corner
		anchorPoint_= ccp(0.0f, 0.0f);
		
		vertexDataCount=pointsNb=0;
		[self setTextureRectInPixels:CGRectZero untrimmedSize:CGSizeZero];
	}
	return self;
}

-(id) initWithTexture:(CCTexture2D*)texture rect:(CGRect)rect {
	NSAssert(texture!=nil, @"Invalid texture for sprite");
	if( (self = [self init]) ) {
		[self setTexture:texture];
		[self setTextureRect:rect];
	}
	return self;
}

-(id) initWithTexture:(CCTexture2D*)texture {
	NSAssert(texture!=nil, @"Invalid texture for sprite");
	
	CGRect rect = CGRectZero;
	rect.size = texture.contentSize;
	return [self initWithTexture:texture rect:rect];
}

-(id) initWithFile:(NSString*)filename {
	NSAssert(filename!=nil, @"Invalid filename for sprite");
	
	CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage: filename];
	if( texture ) {
		CGRect rect = CGRectZero;
		rect.size = texture.contentSize;
		return [self initWithTexture:texture rect:rect];
	}
	
	[self release];
	return nil;
}

-(id) initWithFile:(NSString*)filename rect:(CGRect)rect {
	NSAssert(filename!=nil, @"Invalid filename for sprite");
	
	CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage: filename];
	if( texture )
		return [self initWithTexture:texture rect:rect];
	
	[self release];
	return nil;
}
- (void) dealloc {
	if (vertexData) free(vertexData);
	[super dealloc];
}

-(void) updateColor {
	ccColor4F color4;
	color4.r=(float)color_.r/255.0f;
	color4.g=(float)color_.g/255.0f;
	color4.b=(float)color_.b/255.0f;
	color4.a=(float)opacity_/255.0f;
	
	for (int i=0; i<vertexDataCount; i++) {
		vertexData[i].colors=color4;
	}
}
-(void)updateTextureCoords:(CGRect)rect {
	CCTexture2D *tex = texture_;
	if(!tex)
		return;
	
	float atlasWidth = (float)tex.pixelsWide;
	float atlasHeight = (float)tex.pixelsHigh;
	
	float width=rect.size.width/atlasWidth;
	float height=rect.size.height/atlasHeight;
	
	
	float left,right,top,bottom;
	
	left	= rect.origin.x/atlasWidth;
	right	= left + width;
	top		= rect.origin.y/atlasHeight;
	bottom	= top + height;
	
	
	for (int j=0;j<bandsNb;j++) {
		int offset=j*pointsNb*2;
		for (int i=0; i<pointsNb; i++) {
			vertexData[offset+2*i].texCoords=(ccTex2F){left+vertexData[2*i].vertices.x/rect.size.width, top+height*(float)((bandsNb-j))/(float)bandsNb/*bottom*/};
			vertexData[offset+2*i+1].texCoords=(ccTex2F){left+vertexData[2*i+1].vertices.x/rect.size.width, top+height*(float)((bandsNb-j-1))/(float)bandsNb /*top*/};
		}
	}
}

//This is where we create the vertex data, used later in the draw method (see below)

-(void)setBiCurve:(CGPoint*)topCurvePoints :(CGPoint*)bottomCurvePoints :(int)nbPoints :(int)nbBands {
	int newVertexDataCount=(nbPoints*2)*nbBands;
	if ((vertexData)&&(vertexDataCount!=newVertexDataCount)) {
		free(vertexData);
		vertexData = (ccV2F_C4F_T2F*) malloc(newVertexDataCount * sizeof(ccV2F_C4F_T2F));
	} else {
		if (!vertexData) {
			vertexData = (ccV2F_C4F_T2F*) malloc(newVertexDataCount * sizeof(ccV2F_C4F_T2F));
		}
	}
	vertexDataCount=newVertexDataCount;
	pointsNb=nbPoints;
	bandsNb=nbBands;
	

	for (int i=0;i<pointsNb;i++) {
		CGPoint diff=ccp(topCurvePoints[i].x*CC_CONTENT_SCALE_FACTOR()-bottomCurvePoints[i].x*CC_CONTENT_SCALE_FACTOR(),topCurvePoints[i].y*CC_CONTENT_SCALE_FACTOR()-bottomCurvePoints[i].y*CC_CONTENT_SCALE_FACTOR());

		for (int j=0;j<bandsNb;j++) {
			int offset=j*pointsNb*2;
			vertexData[offset+2*i].vertices=(ccVertex2F){bottomCurvePoints[i].x*CC_CONTENT_SCALE_FACTOR() + (float)j*diff.x/(float)bandsNb ,bottomCurvePoints[i].y*CC_CONTENT_SCALE_FACTOR() + (float)j*diff.y/(float)bandsNb};
			vertexData[offset+2*i+1].vertices=(ccVertex2F){bottomCurvePoints[i].x*CC_CONTENT_SCALE_FACTOR() + (float)(j+1)*diff.x/(float)bandsNb ,bottomCurvePoints[i].y*CC_CONTENT_SCALE_FACTOR() + (float)(j+1)*diff.y/(float)bandsNb};
		}
	}

	[self updateTextureCoords:rectInPixels_];
	[self updateColor];
}


This is where we perform the OpenGL calls, draw the N horizontal bands

-(void) draw {
	BOOL newBlend = NO;
	if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
		newBlend = YES;
		glBlendFunc( blendFunc_.src, blendFunc_.dst );
	}
	
	glBindTexture(GL_TEXTURE_2D, [texture_ name]);
	
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

	// Here we draw the bandsNb bands
	for (int j=0;j<bandsNb;j++) {
		int offset=(pointsNb*2)*j;
		glVertexPointer(2, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData[offset].vertices);
		glTexCoordPointer(2, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData[offset].texCoords);
		glColorPointer(4, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData[offset].colors);
		glDrawArrays(GL_TRIANGLE_STRIP, 0, pointsNb*2);
	}

	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	
	if( newBlend )
		glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
}

-(void)setTextureRectInPixels:(CGRect)rect untrimmedSize:(CGSize)untrimmedSize {
	rectInPixels_ = rect;
	rect_ = CC_RECT_PIXELS_TO_POINTS( rect );
	
	[self setContentSizeInPixels:untrimmedSize];
	[self updateTextureCoords:rectInPixels_];	
}
-(void)setTextureRect:(CGRect)rect {
	CGRect rectInPixels = CC_RECT_POINTS_TO_PIXELS( rect );
	[self setTextureRectInPixels:rectInPixels untrimmedSize:rectInPixels.size];
}

//
// RGBA protocol
//
#pragma mark CCSprite - RGBA protocol
-(GLubyte) opacity {
	return opacity_;
}

-(void) setOpacity:(GLubyte) anOpacity {
	opacity_			= anOpacity;
	
	// special opacity for premultiplied textures
	if( opacityModifyRGB_ )
		[self setColor: (opacityModifyRGB_ ? colorUnmodified_ : color_ )];
	
	[self updateColor];
}

- (ccColor3B) color {
	if(opacityModifyRGB_){
		return colorUnmodified_;
	}
	return color_;
}

-(void) setColor:(ccColor3B)color3 {
	color_ = colorUnmodified_ = color3;
	if( opacityModifyRGB_ ) {
		color_.r = color3.r * opacity_/255;
		color_.g = color3.g * opacity_/255;
		color_.b = color3.b * opacity_/255;
	}
	[self updateColor];
}

-(void) setOpacityModifyRGB:(BOOL)modify {
	ccColor3B oldColor	= self.color;
	opacityModifyRGB_	= modify;
	self.color			= oldColor;
}

-(BOOL) doesOpacityModifyRGB {
	return opacityModifyRGB_;
}

#pragma mark CCSprite - CocosNodeTexture protocol

-(void) updateBlendFunc {
	if( !texture_ || ! [texture_ hasPremultipliedAlpha] ) {
		blendFunc_.src = GL_SRC_ALPHA;
		blendFunc_.dst = GL_ONE_MINUS_SRC_ALPHA;
		[self setOpacityModifyRGB:NO];
	} else {
		blendFunc_.src = CC_BLEND_SRC;
		blendFunc_.dst = CC_BLEND_DST;
		[self setOpacityModifyRGB:YES];
	}
}

-(void) setTexture:(CCTexture2D*)texture {	
	// accept texture==nil as argument
	NSAssert( !texture || [texture isKindOfClass:[CCTexture2D class]], @"setTexture expects a CCTexture2D. Invalid argument");
	
	[texture_ release];
	texture_ = [texture retain];

	ccTexParams texParams = { GL_NEAREST, GL_NEAREST, GL_REPEAT, GL_REPEAT };
	[texture_ setTexParameters: &texParams];

	[self updateBlendFunc];
}

-(CCTexture2D*) texture {
	return texture_;
}
@end