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.
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.
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:
[sourcecode]
– (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];
}
}
}
[/sourcecode]
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:
[sourcecode]
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);
[/sourcecode]
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:
[sourcecode]
CCWaves* effect = [CCSequence actions:
[CCLiquid actionWithWaves:2
amplitude:10
grid:grid
duration:kDuration],
[CCStopGrid action],
nil];
[node runAction:effect];
[/sourcecode]
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:
[sourcecode]
v.x += K * sinf(2*pi*w*t + v.y * k);
v.y += K * sinf(2*pi*w*t + v.x * k);
[/sourcecode]
where k and K are both constants. Now, you clearly see the problem with the phase: at time 0, we have:
[sourcecode]
v.x += K * sinf(v.y * k);
v.y += K * sinf(v.x * k);
[/sourcecode]
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:
[sourcecode]
– (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];
}
}
}
[/sourcecode]
Where a new variable makes its entry:
[sourcecode]
float c = 4*(1/4.0 – (time-1/2.0) * (time-1/2.0));
[/sourcecode]
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.
Boundary value analysis for bug fixing
Anyone who has worked in quality assurance or is serious about testing knows about a technique used to define test cases that is called boundary value analysis. Here is how Wikipedia describes it:
cocos2d and the new retina iPad - take 2
I described in a previous post about cocos2d and the new retina iPad a quick solution to the problem of supporting higher-resolution images. The main objective of that work was to make easier re-building your app for submission on the App Store before you could possibly provide higher-resolution images for the new iPad. I mentioned anyway that that approach was just a quick hack and not a full answer to the need to provide 4 (four!) different versions for any of your artworks.
iOS6: dynamic autorotation
One of the most intrusive changes brought by iOS6 is the way autorotation is handled in UIViewControllers. If you have an autorotating app for iOS5, you will need to change it to correctly support autorotation under iOS6. If you develop an app which is supposed to run both on iOS5 and iOS6, then you will have to handle autorotation in the old as well as the new way.


