Smoothing CCGridActions in Cocos2D for iPhone

CCGridActions are a great feature of cocos2d. They provide nice 2D and “3D” effects that you can use to make your app or game appearance more appealing. I have already written about them, mostly about the high cost the exact in terms of CPU time (and FPS), what makes them barely usable in a real, sufficiently complex scenario. In a recent post of mine, I described a solution to this issue, which though not a full replacement for “real” grid actions, makes them usable, fast, and not at all demanding in terms of processing power.

Here, I would like to point out to another glitch that grid actions may have, and hint at a solution to it. This time, the glitch has nothing to do with the architecture of grid actions; rather, it is something that has to do with the details of implementation of particular actions. First of all, let’s try and understand what glitch I am talking about: have a look at the video below.

Liquid Effect

As you can see, this is a “liquid” deformation of a sprite, trying to resemble the effect that would be seen if the sprite were a piece of fabric floating on water. Pay attention to the moment when the deformation begins, and to the moment when it ends. The deformation effect is repeated several times, so that it be easier for you to catch the “jump” that the sprite ¬†does at those times. Now, to see what I am aiming at, have a look at the second video below, where you will see what the deformation effect will look like at the end of the post.

Improved Liquid Effect

As you see, the start and end of the deformation are much smoother. Now, to the code!

Without going into much detail, the method responsible for the deformation is:

- (void)update:(ccTime)time {
	int i, j;
	for (i = 1; i < gridSize_.x; i++) {
		for( j = 1; j < gridSize_.y; j++ ) {
			ccVertex3F	v = [self originalVertex:ccg(i,j)];
			v.x += (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.x * .01f) * amplitude * amplitudeRate);
			v.y += (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.y * .01f) * amplitude * amplitudeRate);
			[self setVertex:ccg(i,j) vertex:v];
		}
	}
}

You’ll notice the two statements that are actually deforming, at any given time, each of the vertices that make up the sprite Open GL texture definition:

			v.x += (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.y * .01f) * amplitude * amplitudeRate);
			v.y += (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.x * .01f) * amplitude * amplitudeRate);

What happens here is that the vertices position is made follow a sinusoidal wave. So, the first important point you have to stick to is: the overall duration of the action should be an integral multiple of the sinusoidal period:

    CCWaves* effect = [CCSequence actions:
                       [CCLiquid actionWithWaves:2
                                      amplitude:10
                                            grid:grid
                                       duration:kDuration],
                       [CCStopGrid action],
                       nil];
    [node runAction:effect];

the “waves” parameter represents the number of oscillations per second; so, its inverse, 1/2.0, is the period. Any, integral multiple of it will do: 2.0, 2.1; but not 1.7.

The second thing we can notice is the phase inter-modulation that is applied; streamlining the assignment, we have:

   v.x += K * sinf(2*pi*w*t + v.y * k);
   v.y += K * sinf(2*pi*w*t + v.x * k);

where k and K are both constants. Now, you clearly see the problem with the phase: at time 0, we have:

   v.x += K * sinf(v.y * k);
   v.y += K * sinf(v.x * k);

This means that at the very start of the deformation, we already have a non-zero value for the vertices deformation. On the contrary, to get a smooth deformation, we want those values also be increased smoothly. In other words, at time 0, v.x and v.y shall be zero, and then grow from there up to its maximum.

The solution to this is applying a transformation to the phase shift, so that it is not fixed, i.e. only depending on the vertices coordinates, but also from the actual moment in time. The kind of transformation we would like to have is:

Where T is the period, x is the time, and N is a constant that will make the curve more or less flat. The following image shows a sample of the curve for T==2 and N==3.

Taking into account that the time argument to the update method is normalized, i.e, varies between 0 and 1, the following simplified formula can be applied:

So we have:


- (void)update:(ccTime)time {
	int i, j;
	float c = 4*(1/4.0 - (time-1/2.0) * (time-1/2.0));
	for (i = 1; i < gridSize_.x; i++) {
		for( j = 1; j < gridSize_.y; j++ ) {
			ccVertex3F	v = [self originalVertex:ccg(i,j)];
            float dx = (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.x * .01f * c) * amplitude * amplitudeRate);
            float dy = (time-duration_)/duration_*(sinf(time*(CGFloat)M_PI*waves*2 + v.y * .01f * c) * amplitude * amplitudeRate);
			v.x += dy;
			v.y += dx;
			[self setVertex:ccg(i,j) vertex:v];
		}
	}
}

Where a new variable makes its entry:



	float c = 4*(1/4.0 - (time-1/2.0) * (time-1/2.0));

which used as a multiplier for the phase shift, gives the result shown in the second video above. Worth to notice that it is a simple parabolic curve; indeed, even extracting the square root from it so to make it flatter would not produced enough smoothing.

Copyright © Labs Ramblings

Built on Notes Blog by TDH
Powered by WordPress

That's it - back to the top of page!