Fancy Rat Studios

  • Home
  • Games
  • About

01
Feb
2010

ProgressTimer for cocos2d

Lam

Every Monday, Wednesday and Friday I will put my thoughts on game design concepts and ideas into words so for today let’s focus on a topic that I hope many games will use. A Loading indicator!

The problem at hand

Sometimes we will need to process data or have an in-game wait or delay. We can’t let users wait without any visual indication so generally we’ll implement a loading progress image. This simple concept is shown below.

If that’s what you are also aiming to do then read ahead!

Discovery

We want a solution that can work for any image type and is setup for cocos2d-iphone. A really neat idea is that we can map triangles using opengl to show certain portions of the image.

We see that for the mapping to work, we’ll use:

  1. the midpoint vertex (vertex 0)
  2. the 12 o’clock vertex (vertex 1)
  3. each corner edge up-to, (vertex 2-5)
  4. the point created from the progress percentage (vertex 6)


Not too hard when we really get down to solving it.

Let’s implement the thing

First we setup a ProgressTimer class that inherits a CCNode so we can add it to the engine.
ProgressTimer, at it’s core, will hold the percentage value, a texture, and the vertex data so we can render the object in opengl. (Note that the sample below has more because I’ve implemented the option to change its opacity and color. I’ve also added different progress timer types.)

ProgressTimer.h

typedef enum {
	kProgressTimerTypeRadialCCW,
	kProgressTimerTypeRadialCW,
	kProgressTimerTypeHorizontalBarLR,
	kProgressTimerTypeHorizontalBarRL,
	kProgressTimerTypeVerticalBarBT,
	kProgressTimerTypeVerticalBarTB,
} ProgressTimerType;

@interface ProgressTimer : CCNode {
	ProgressTimerType	type_;
	float				percentage_;
	CCTexture2D			*texture_;

	int					vertexDataCount_;
	ccV2F_C4F_T2F		*vertexData_;

	int					indicesCount_;
	GLubyte				*indices_;

	GLubyte				opacity_;
	ccColor3B			color_;
	ccBlendFunc			blendFunc_;
}
///
//	Change the percentage to change progress.
///
@property ProgressTimerType type;
///
//     The percentage of progress from 0..100
///
@property float percentage;
@property (retain) CCTexture2D *texture;
@property ccColor3B color;
@property GLubyte opacity;
@property ccBlendFunc blendFunc;

///
//	Creates a progress timer with the texture as the shape the timer goes through
///
+ (id) progressWithTexture:(CCTexture2D*) aTexture;
- (id) initWithTexture:(CCTexture2D*) aTexture;

@end

We’ll skip the basic initializing and destruction of variables and get right to the most important piece. The mapping of the image in order to create the radial indicator.

Every time a user changes the percentage we will update the ProgressTimer vertex data.


-(void)setPercentage:(float) percentage
{
	if(percentage_ < 0.f)
		percentage_ = 0.f;
	else if(percentage > 100.0f)
		percentage_  = 100.f;
	else
		percentage_ = percentage;

	[self updateProgress];
}

ProgressTimer’s updateProgress will call updateRadial if our type is set to kProgressTimerTypeRadialCCW or kProgressTimerTypeRadialCCW. Radial type is our pie slice indicator and the CW/CCW suffix represents it’s rotation direction (clockwise or counter-clockwise).

What we will do is understand the implementation details of developing the radial slices.

Every radial update, we will release all vertex data memory on the heap.

ProgressTimer.m updateRadial

	//	release all previous information
	if(vertexData_){
		free(vertexData_);
		vertexData_ = 0;
		vertexDataCount_ = 0;
	}

	if(indices_){
		free(indices_);
		indices_ = 0;
		indicesCount_ = 0;
	}

	//	If percentage is zero it's quicker not to create the vertexData
	if(percentage_ == 0.f)
		return;

Finding that midpoint

The easiest vertex point to find on the image is the midpoint and as an added bonus we will use a CCNode’s anchorPoint so we have a choice for the center. The midpoint has to be adjusted by the actual texture maximums since we don’t always deal with texture sizes with powers of 2. Once we have the midpoint we now have enough information to find the next needed vertex.

	CGPoint tMax = ccp(texture_.maxS,texture_.maxT);
	CGPoint midpoint = ccpCompMult(self.anchorPoint, tMax);

ccCompMult is just a multiplying each component of the two points together

CGPoint ccpCompMult(CGPoint a, CGPoint b)
{
	return ccp(a.x * b.x, a.y * b.y);
}

The percentage point

Now we find the vertex point based on the ProgressTimer percentage.

We can find the angle needed to rotate from the 12 o’clock position.
Taking that angle, the point of rotation is found by doing a vector rotation from the vertex at the 12 o’clock to the percentage point.

	float alpha = percentage_ / 100.f;
	//	Otherwise we can get the angle from the alpha
	float angle = 2.f*M_PI * ( type_ == kProgressTimerTypeRadialCW? alpha : 1.f - alpha);

	//	We find the vector to do a hit detection based on the percentage
	//	We know the first vector is the one @ 12 o'clock (top,mid) so we rotate
	//	from that by the progress angle around the midpoint pivot
	CGPoint percentagePt = ccpRotateByAngle(ccp(midpoint.x, 0.f), midpoint, angle);

ccpRotateByAngle takes our point and a pivot and rotates it around the pivot by the angle.

CGPoint ccpRotateByAngle(CGPoint v, CGPoint pivot, float angle) {
	CGPoint r = ccpSub(v, pivot);
	float t = r.x;
	double cosa = cosf(angle), sina = sinf(angle);
	r.x = t*cosa - r.y*sina;
	r.y = t*sina + r.y*cosa;
	r = ccpAdd(r, pivot);
	return r;
}

We’re almost there but the rotated point isn’t the correct point needed for the vertex data. What we really need is the intersection point from the line edge of the texture and the percentage line.

How I attacked this problem is that the texture coordinates need to be in an array so we can quickly find the correct edge for the intersection test.

#define kProgressTextureCoordsCount 4
const CGPoint kProgressTextureCoords[kProgressTextureCoordsCount] = {{0,0},{0,1},{1,1},{1,0}};


As I finished writing this I noted that maximum texture coordinate values are always 0 and 1 so let’s represent the array as a sequence of bits 00011110 (which corresponds to 0×1e).

So I rewrote the array as

#define kProgressTextureCoordsCount 4
const char kProgressTextureCoords = 0x1e;

And a function that will give me the corresponding texture coordinates for an index value. It pretty much operates at the bit level to retrieve the correct value (if this is confusing I may write a lesson on bit operations).

-(CGPoint)boundaryTexCoord:(char)index
{
	if (index < kProgressTextureCoordsCount) {
		switch (type_) {
			case kProgressTimerTypeRadialCW:
				return ccp((kProgressTextureCoords>>((index<<1)+1))&1,(kProgressTextureCoords>>(index<<1))&1);
			case kProgressTimerTypeRadialCCW:
				return ccp((kProgressTextureCoords>>(7-(index<<1)))&1,(kProgressTextureCoords>>(7-((index<<1)+1)))&1);
			default:
				break;
		}
	}
	return CGPointZero;
}

Now we have the texture corner coordinates; we now need the edge of the texture and the percentage point line.

We can find the nearest edge based on the alpha value. Since we need 2 points for the line, we grab the previous index and make sure the previous index is wrapped within our array bounds.

	int index = roundf(kProgressTextureCoordsCount*alpha);
	int pIndex = (index + (kProgressTextureCoordsCount - 1))%kProgressTextureCoordsCount;

	CGPoint edgePtA = [self boundaryTexCoord:index % kProgressTextureCoordsCount];
	CGPoint edgePtB = [self boundaryTexCoord:pIndex];

The percentage line is just the midpoint and the percentage point.

ccpLineIntersect returns true if an intersection occurred. We pass it 4 points that make up 2 lines.
L1 = p2 – p1
L2 = p4 – p3
Then we grab the hit point using the returned ’s’ or ‘t’ value.
hit = midpoint + (percentagePoint – midpoint)*t;

bool ccpLineIntersect(CGPoint p1, CGPoint p2,
								 CGPoint p3, CGPoint p4,
								 float *s, float *t){
	CGPoint p13, p43, p21;
	float d1343, d4321, d1321, d4343, d2121;
	float numer, denom;

	p13 = ccpSub(p1, p3);

	p43 = ccpSub(p4, p3);

	//Roughly equal to zero but with an epsilon deviation for float
	//correction
	if (ccpFuzzyEqual(p43, CGPointZero, kCGPointEpsilon))
		return false;

	p21 = ccpSub(p2, p1);

	//Roughly equal to zero
	if (ccpFuzzyEqual(p21,CGPointZero, kCGPointEpsilon))
		return false;

	d1343 = ccpDot(p13, p43);
	d4321 = ccpDot(p43, p21);
	d1321 = ccpDot(p13, p21);
	d4343 = ccpDot(p43, p43);
	d2121 = ccpDot(p21, p21);

	denom = d2121 * d4343 - d4321 * d4321;
	if (fabs(denom) < kCGPointEpsilon)
		return false;
	numer = d1343 * d4321 - d1321 * d4343;

	*s = numer / denom;
	*t = (d1343 + d4321 *(*s)) / d4343;
	return true;
}

We pass the points into our intersect formula (notice we multiply the corner components by tMax since we need the actual texture coordinates and sometimes we’ll have images that are not powers of 2.

//	s and t are returned by ccpLineIntersect which explains how to use them
	float s = 0, t = 0;
	ccpLineIntersect(ccpCompMult(edgePtA,tMax), ccpCompMult(edgePtB,tMax),
					 midpoint, percentagePt, &s, &t);

	//	get the hit point
	CGPoint hit = ccpAdd(midpoint, ccpMult(ccpSub(r2, midpoint),t));

We have all our needed points calculated! Now all that’s left is creating the vertex data.

Creating Our Vertex Data

To create the vertex data we first need to convert texture coordinates (in the 0..1) range to actual vertex coordinates [0..imageSize] which I have written below.

///
//	@returns the vertex position from the texture coordinate
///
-(CGPoint)vertexFromTexCoord:(CGPoint) texCoord
{
	if (texture_) {
		float min = 0, maxW = texture_.contentSize.width, maxH = texture_.contentSize.height;
		return ccp(lerp(min, maxW, texCoord.x/texture_.maxS),
				   lerp(min, maxH, (1.f -(texCoord.y/texture_.maxT))));
	} else {
		return CGPointZero;
	}
}

Now we have all that we need to create our vertex data what we’ll do is first store the midpoint position. Then the point at 12 o’clock on the texture.

Now we add each corner of the texture into the vertex array until we reach the hit point and we add the hit point last.

After the vertex data is complete we begin creating the triangle by setting up the indexes. Let’s take a look at our original setup of vertex data.

To create triangles from the example vertexes above we can see that the indexes will hold.

indices_ = {0,1,2, 0,2,3, 0,4,5, 0,5,6};

The code is shown below:

	//	The size of the vertex data is the index from the hitpoint
	//	the 3 is for the midpoint, 12 o'clock point and hitpoint position.
	vertexDataCount_ = index + 3;
	vertexData_ = malloc(vertexDataCount_ * sizeof(ccV2F_C4F_T2F));

	//	First we populate the array with the midpoint, then all
	//	vertices/texcoords/colors of the 12 'o clock start and edges and the hitpoint
	vertexData_[0].texCoords = (ccTex2F){midpoint.x, midpoint.y};
	vertexData_[0].vertices = [self vertexFromTexCoord:midpoint];

	vertexData_[1].texCoords = (ccTex2F){midpoint.x, 0.f};
	vertexData_[1].vertices = [self vertexFromTexCoord:ccp(midpoint.x, 0.f)];

	for(int i = 0; i < index; ++i){
		CGPoint texCoords = ccpCompMult([self boundaryTexCoord:i], tMax);

		vertexData_[i+2].texCoords = (ccTex2F){texCoords.x, texCoords.y};
		vertexData_[i+2].vertices = [self vertexFromTexCoord:texCoords];
	}

	//	hitpoint will go last
	vertexData_[vertexDataCount_ - 1].texCoords = (ccTex2F){hit.x, hit.y};
	vertexData_[vertexDataCount_ - 1].vertices = [self vertexFromTexCoord:hit];	

	indicesCount_ = 3 * (index + 1);
	indices_ = malloc(indicesCount_ * sizeof(GLubyte));

	//	We only go up to index since we do three increments
	for(int i=0;i < indicesCount_/3; ++i){
		indices_[3*i] = 0;
		indices_[3*i + 1] = i + 1;
		indices_[3*i + 2] = i + 2;
	}

The only thing left to do is draw our vertex data

ProgressTimer draw

	glBindTexture(GL_TEXTURE_2D, texture_.name);
	glVertexPointer(2, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData_[0].vertices);
	glTexCoordPointer(2, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData_[0].texCoords);
	glColorPointer(4, GL_FLOAT, sizeof(ccV2F_C4F_T2F), &vertexData_[0].colors);
	glDrawElements(GL_TRIANGLES, indicesCount_, GL_UNSIGNED_BYTE, indices_);

And now we have a ProgressTimer indicator. If you want to see the demo file of this tutorial then just click on the link.


Download the XCode Demo

This demo uses the fast, easy and open source cocos2d framework.

Tags: bar, cocos2d, indicator, loading, OpenGL, progress, radial, timer

You can leave a response, or trackback from your own site.

8 Conversations

  1. ProgressTimer for cocos2d | cocos2d for iPhone says:
    February 5, 2010 at 11:44 am

    [...] The article is here: ProgressTimer for cocos2d [...]

  2. chebur says:
    February 6, 2010 at 2:00 am

    great article. very useful.

    please, can you explain how -(CGPoint)boundaryTexCoord:(char)index method works? I know bit operations, but i can’t understand how exactly you retreive corresponding texture coordinates for an index value.

    thanks

  3. Lam says:
    February 6, 2010 at 3:18 am

    Oh whoops… the code in the post previously was incorrect, I seemed to have mixed the bit shifting for the post. I’ve fixed that now. The demo does have the correct bit shifting operation so I’m going to use that function.

    Maybe fixing that mistake makes more sense to you now but I’ll break down that complicated bit operation that I did if you are still unsure.

    Let’s assume that normally we have an array of kProgressTextureCoords = { {0,0}, {0,1}, {1,1}, {1,0} } which represents the corners of the rectangle.
    {0,0} is the top left, {0,1} is the bottom left, {1,1} is the bottom right, {1,0} is the top right

    For now I’ll assume our type is kProgressTimerTypeRadialCW. This means we want to grab corners in a clock-wise direction.
    So if I passed in index = 1 we need to get access to the bottom right corner. {1, 1}
    So an index 0 => {1,0}, 1 => {1,1}, 2 => {0,1} and so on.

    But now I’ve simplified it into bits to save space 00011110.

    If I pass in index = 1, it should return {1,1}. Let’s see if it works.
    The function sets up our CGPoint for (x,y) where:
    x = (kProgressTextureCoords>>((index< <1)+1))&1
    and
    y = (kProgressTextureCoords>>(index< <1))&1)

    Remember that we want the value between the square bracket in our bit pattern: 0001[11]10. Which is contained in kProgressTextureCoords.

    Let's solve each term to see if we do get the correct value for x = (kProgressTextureCoords>>((index< <1)+1))&1
    Since our index is 1. We'll work of course from our inner brackets outward.

    x = (00011110>>((1< <1)+1))&1
    (00011110>>(2+1))&1
    (00011110>>3)&1
    (00011)&1
    x = 1


    y = (00011110>>((1< <1)))&1
    (00011110>>2)&1
    (000111)&1
    y = 1

    Now we get {1,1} which is what we needed. Hope that helps but do ask if there are still problems.

  4. Sherwin says:
    February 12, 2010 at 1:28 pm

    This is most impressive, my team and I were about to try to figure this out until we saw that riq on Cocos2d linked to your article. Thanks a lot for the tutorial!

  5. Sherwin says:
    February 14, 2010 at 3:54 pm

    Hi, I also just wanted to point out that the Anchor Point of the Cocosnode/Progress Timer is not taken into consideration

  6. Lam says:
    February 15, 2010 at 5:00 pm

    That’s true. I was going to make changing the anchorPoint change the midpoint of the radial progress timer. I should check to see if that has bugs.
    If you need to change the anchor point where the image is drawn you can access the sprite property and change the sprite anchorPoint.

    Now anchorPoint may be confusing so any suggestions on how that functions would be great and I’ll add it to the class.

  7. 2010-02-20 cocos2d 学习 says:
    February 19, 2010 at 10:13 pm

    [...] ProgressTimer for cocos2d [...]

  8. Truffle Apps says:
    April 11, 2010 at 7:37 pm

    Awesome article
    Thanks for sharing

Converse

Click here to cancel reply.

Games

  • Mini Games

Community

  • About
  • Contact

Sitemap

  • Blog
  • RSS FeedSubscribe in a reader
  • XHTML Strict 1.0

Fancy Rat Studios is proudly powered by WordPress 2.9.2

Background image courtesy of robertvitulano

© 2010 Fancy Rat Studios. All Rights Reserved.