Welcome, Code Ninjas!
This time we'll be looking at a simple script that makes certain motions look more natural.
Imagine you have a platform that you'd like to move back and forth, a common element in platformer games. You might use code something like this:
Create Event:
r = 64;//maximum distance in pixels the platform may
travel
from its
origin before reversing direction
s = 1/32;//speed of the platforms. The divisor is how many
steps you
wish the platform to take to reach its maximum distance from its
origin.
Step Event:
if a //moving forward...
{
p += s;
if p >= 1 { p = 1; a
= 0; }
}
else //...and moving back
{
p -= s;
if p <= 0 { p = 0;
a = 1; }
}
x = xstart +
r*p;//update
platform's position
xspeed = x-xprevious;//get
platform's speed in pixels
(Note: If this code seems a little overcomplicated, it is because it had been purposefully written to be able to make platforms of any speed and range.)
This effect that this code achieves is a platform that moves away from its starting point at the specified speed, reaches its maximum distance, and then immediately reverses direction and trots back to repeat the process indefinitely. This is the sort of platform you'll often see in early 8- and 16-bit games.
The trouble with this kind of motion is the immediate reversal of motion. In one step, the platform can be moving with a speed of +5 pixels, and the in the next, with a speed of -5 pixels. This doesn't look very realistic, because in reality, most often when something reverses direction, it has to slow down to a halt, and then begin to accelerate again.
This jerky motion isn't so bad if the platform's "patrol area" is bounded by walls at its extremes - then it just looks like the platform is bouncing off of the walls, and the motion doesn't look too bad. But if the platform is floating in mid-air, as they often are, there is nothing that appears to plausibly reverse it, and the motion looks unnatural.
And it's worse than just looking unpleasant. It actually makes the game less fair, and less fun. If the player can't tell by some visual cue when the platform is going to decide to turn around, they have a much harder time getting the proper timing on their jump. They will have to use more trial and error, watching the platform make its rounds more than once before they can confidently make their move.
This is almost game-breaking if you want to keep good flow, as in a Sonic game. In the Sonic the Hedgehog games for the Mega Drive (Genesis), almost all platforms not bounded by walls move with a natural motion - decelerating as they reach their extremes, and accelerating toward their point of origin as they turn back around. This allows players to intuit exactly where the platform will be any time when they first come across it, without patient study of its entire cycle. This is one of the subtler points about the Sonic the Hedgehog game, seldom recognised, but it contributes not insignificantly to the sense of speed that made them popular.
Well then, how can we achieve the same effect so that our platforms move with a natural motion, rather than an outdated, unrealistic, and unfair jerky one? Why, with the trusty cosine function, of course!
Imagine, now, a platform that - instead of moving simply up and down, or left and right - moves in a complete circle, as many do in Sonic and Mario games. The platform's speed should be uniform, but if we were to look at just one component of its velocity - say, just the xspeed, or just the yspeed - we would notice acceleration and retardation of its speed. Simply imagine looking at the platform's circular path edge on, instead of face on. It would appear to be moving in a straight line, but slowing down at the edges and speeding up in the middle, just like those Sonic platforms we want to emulate.
So, in effect, what we want to do is make the platform move in a circle - just a really flat circle that might as well be a line. We'd use code something like this:
Create Event:
r = 64;//maximum distance in pixels the platform may
travel
from its
origin before reversing direction (radius of the circle)
s = 180/32;//speed of the platforms. The divisor is how many
steps you
wish the platform to take to reach its maximum distance from its
origin. (This time we use 180, not 1, because we'll be using degrees.)
Step Event:
a += s;
if a >= 360 a -= 360;
//alternatively the preceding two lines could be 'a = (a+s)
mod 360;'.
//also, here we don't need to use two states, forward and
back, because
the circular motion takes care of that for us automatically.
x = xstart +
r*cos(degtorad(a));//update
platform's position
xspeed = x-xprevious;//get
platform's speed in pixels
To make the platform move vertically, we can just replace all the references to x and make them y. Or, to make it actually move in a perceptible circle, we can have both sets of lines. By using a different range value for both x and y, you can squash the circle into any sort of ellipse you want - for example, a circle that is twice as wide as it is tall:
Create Event:
xr = 64;
yr = 32;
Step Event:
//...
x = xstart +
xr*cos(degtorad(a));
xspeed = x-xprevious;
y = ystart -
yr*sin(degtorad(a));
yspeed = y-yprevious;
Remember to subtract the sine for y, and add the cosine for x, otherwise instead of moving circularly, it'll just move in a smooth diagonal - which is actually another useful effect you might want to achieve.
Well, that about does it for platforms, but the power of sinusoidal motion goes far beyond. There are other applications, and for just such an example, I'll use the purpose for which I first I needed it myself.
In games like Phantasy Star, when you talk to townsfolk or shopkeepers, their dialogue appears on the screen inside of bordered window, or a 'text box'. In most games, the text boxes appear on the screen gradually, either opening up, dropping down, or fading in.
I wanted this animation to appear smoother, so I thought perhaps I could apply sinusoidal motion as the solution. But there was a slight hitch.
Imagine you want to fade in a window, from an alpha of 0 (invisible) to an alpha of 1 (fully opaque). You could simply add 0.1 for ten steps, but that wouldn't look very smooth. How about, instead, we use a sine function.
Code:
step = 18;
for {a=0;a<=180;a+=step}
{
alpha = sin(degtorad(a));
}
Well, clearly this won't work. The value of alpha will go from 0 (the sine of angle 0), accelerate toward 1 (the sine of angle 90) and decelerate back to 0 again (the sine of angle 180). The text box wouldn't fade in, it would fade in and back out again just as quickly! This obviously isn't what we want.
But why not just use 90, instead of 180, so that alpha will stop at 1, thereby fading the text box in how we want it? Well, in that case, the fade would start smoothly, but stop abruptly. I wanted it to both start and stop smoothly.
I needed some way to have the alpha value "move" like a half-circle (slow start and stop), but only "traverse" a quarter-circle (start at 0 and end at 1).
So I made a script, called it 'sinusoidal()', and this is the function I used:
sinusoidal()
//argument0: any value between 0 and 1
return (cos(degtorad(180-(180*argument0)))+1)/2;
Now, sinusoidal motion can be employed anywhere by calling the script. The text box fade code ends up looking something like this:
Code:
step = 0.1;
for {a=0;a<=1;a+=step}
{
alpha = sinusoidal(a);
}
This little script can be very versatile. You can use it to slide logos or menus onto the screen. You could use for flashing lights for smoother look. You could use it to animate a pendulum. You could even use it to make your character push a block (as Link does in the Zelda games) with a less abrupt and better looking motion. And with clever modification, who knows to what ends a Code Ninja might put it to.
For an example GMK illustrating the difference between normal and sinusoidal motion in several types of movement and animation (flashing, shrinking, swinging, sliding), click here.
Next time we'll be looking deeper into text boxes - how to make the text type out, change the text speed, and more. Until then, happy coding, Code Ninjas, and happy holidays, too!
Thanks for this!
ReplyDeleteI never thought of using a circle... brilliant ;)