Game Maker Wave Motion Tutorial - csanyk.com

18 min read Original article ↗

Following up on my motion and position tutorial, I present a tutorial on wave-motion. This was something I wanted to include in the original article, but I realized that there’s enough complexity to this concept that it merited its own separate article.

Wave Motion

Wavelike motion is any motion that involves periodic oscillation, not just linear undulating motion. (Other types of wavelike motion include pulsing and concentric ripples, for example.) But we’ll talk mostly if not exclusively about linear undulation, since it is easiest to understand, simplest to implement, and the basis for many others.

A fake wave sprite animation in 12 sub-images.


Wave motion faked through animated sprite. A sprite animation in 12 sub-images.

Wave motion faked through animated sprite

Wave motion may be accomplished by “faking it” with a sprite animation, but it can also be easily created using trig functions. There are advantages and disadvantages to both approaches.

With an animation, you can get very precise positioning and motion, and at runtime it is cheap to play the animation compared to performing math functions. The downside is that it is time consuming to create animations, and they are not very flexible in terms of adjusting the motion procedurally.

With math-defined motion, you have a great deal of flexibility, and you can adjust the behavior at runtime by varying the equation or the inputs. As well, the code for math-defined approaches takes much less memory, meaning your executable files will both be smaller and use less RAM. The downside is that the math computation can get expensive if you have a lot of instances using it in the game. Try to avoid over-using calls to trig functions, because they are computationally more expensive than most math functions.

The other downside is that for a lot of people, the math is harder to figure out than simply drawing out a brute-force animation. You just need to wrap your head around the math involved, which this tutorial will help with.

There are a number of interesting ways to apply the sin() function to motion.

graph of y = sin(x)

The function y = sin(x) returns an oscillating value between -1 and 1. Varying the position of an instance by such a small amount (we’re talking +/-1 pixel) would scarcely be noticeable, but we can just multiply this by a constant to increase the amplitude of the function, to suit our needs. Varying x slowly or quickly will influence the frequency of the oscillation.

Note: Incidentally, the graphs in this article were generated by google. Search for “graph [function]” and google’s search engine will give you an interactive graph that you can use to understand how functions behave. This can be very handy when trying to figure out math functions that will be useful in your game projects.

Degrees and Radians

Note: In GML, the trig functions take arguments in radians, while things like direction and image_angle are given in degrees. Most people are used to thinking about trigonometry in degrees. Thus, if we want to feed our sine function a parameter value in degrees, we’ll actually need to convert it to radians, like this:

degtorad(degrees)

If you want to manually convert from degrees to radians, use this formula:

radians = degrees/180*pi

And, to convert radians to degrees manually, it’s:

degrees = radians * 180 / pi

GML provides the functions radtodeg() and degtorad() so we don’t really need to bother with knowing the math, but it’s good to know it.

Update: since I originally wrote this article, GameMaker has since added trig functions to GML that accept an argument in degrees: dsin(), dcos(), dtan(), darcsin(), darccos(), darctan(). So you won’t need to worry about converting degrees to radians and vice versa any longer if you don’t want to.

For our examples, I’ll discuss first in degrees since they are more familiar to most people.

Degrees-based oscillator function

The position-shift function will look like this: shift = amplitude * dsin(t);

  • shift signifies the number of pixels that we’ll be position-shifting our instance by.
  • amplitude is the peak height of the wave from the baseline, in pixels.
  • t is a value representing an angle (in degrees) which varies over time.

(In math texts, it’s standard to use the greek letter θ (theta) to represent the value of the angle which is provided to a trigonometric function. So if you’re reading about trigonometry somewhere else and see references to theta, this t-value is the same thing.

Usually in a math textbook where we talk about graphing a trig function, we use the familiar cartesian plane variables x and y. We use t rather than x because in Game Maker x is already defined as an instance’s x-axis position in the room — if we used x, the wave motion would be dependent upon the horizontal position of the instance in the room! We want the oscillation to be dependent on time, not position.)

We need to provide an argument to the sin() function that will increment through a series of values at a rate that will give an appropriate frequency. Normally t will be an instance variable, but it could potentially be a global variable if we want everything that oscillates to move in unison, regardless of when it was created. Initialize t in the Create event, and increment it in the Step event.

Note that t is just an ordinary variable, not a built-in GameMaker variable, and it won’t just increment on it’s own. We need to take care of that, like so:

t += increment;

Depending on what value we use for increment, the frequency of oscillation will increase or decrease. The lower increment is, the slower the frequency will be.

Note: If we’re using degrees, t only needs to vary between values of 0-360, and if we want to be absolutely proper about it, we’ll want to cap it to this range, which we can do very easily using the modulo function, after we increment, like so:

t += increment;
t = t mod 360;

or, if you want to do it in one line:

t = (t + increment) mod 360;

In many games, this may not be necessary from a practical standpoint; I have tested Game Maker by setting up an incrementer function and incrementing a variable by 1 billion (1,000,000,000) every step, and let it run for several minutes, counting up to values in the quintillions and sextillions, and did not see it do anything unexpected, like rollover to a negative value, or overflow a buffer, or crash, or anything. Game Maker games seem to be capable of handling very large floating point values, which in practice it is very unlikely you’ll ever exceed. So if you’re worried about performance enough that you want to get rid of that mod instruction, it’s probably safe to do so. I still feel safer using it, so I’ll use it in my examples.

So what do we want the increment to be? That’s the tricky thing, and it depends in part on room_speed. The default GameMaker room_speed is 30 steps/second, but it’s pretty common to see projects that are designed to run at 60. If we increment t by 1, we end up running through a complete cycle (t = 0..359) in 360/30 = 12 seconds, or a frequency of 1/12 Hz. (In a 60fps room, it’s 1/6 Hz.)

This is rather slow for many uses. But now that we know this, we can easily adjust our increment to make whatever frequency we need. If we want our oscillation to complete a cycle in 1 second, set increment = 12. Half a second, increment = 24. Quarter second, 48. We can go down, of course, as well: 6 = 2 second period, 3 = 4 second period, 1 = 12 second period, etc.

In a 60 fps room, of course, you can halve the above values. This will help smooth out the motion, as well. As the increment increases, the fidelity of the actual motion to the sine function becomes increasingly approximate, which looks jerky.

We do hit some limits with frequency, though. Say we want a frequency of 30Hz (in a 30 fps room). Well, just set increment to 360, right? …. but wait, this results in no oscillation at all! The sin() function is coming “full circle” every step, resulting in the same shift value: 0, so a 0-pixel shift every step is the result.

Moreover, anything over a room_speed-Hz oscillation results in “starting over” from the 0Hz point. There’s no way around this, since time and motion are discrete in the game runner. Fortunately, such high frequency oscillation is extremely rapid, and way more than you’re likely to ever need.

Wave (by position-shift)

Let’s create an object that performs a simple oscillation in the Y axis. Or if you wanted a horizontal oscillation, you could substitute x wherever you see y. Putting together what we’ve learned so far, we’ll create a wave-moving object:

objWaver 

Create:
t = 0;
increment = 12; //degrees -- freq = 1 oscillation per second (1Hz) in a 30 fps room
amplitude = 10; //pixels of peak oscillation

//clone the y-position (or use x instead if you're doing horizontal oscillation)
yy = y;
Step:
t = (t + increment) mod 360;
shift = amplitude * dsin(t);

//clone the movement from the object's speed and direction
yy += vspeed;
y = yy + shift; //vertical wave motion

This will cause a nice +/-10 pixel oscillation with a period of 1 second.

Note that we are using a technique called motion cloning (I just made that term up, but it sounds cool). That is, rather than apply shift directly to y every step, we’re “cloning” the value of y to another variable, yy, keeping track of what the instance’s y position would have been without the wave motion added to it. Each step, we update yy, and use it and the value of the shift in this step to calculate the new y position.

Why is this necessary? Well, if we simply shifted x or y directly, y += shift, the shift value from each step would accumulate over time. But actually, our shift calculation already gives us the total accumulated shift within an oscillation cycle — not an incremental, per-step shift. If we re-added the accumulated total in each step, it would compound, and we’d end up with a much greater amplitude than intended.

By motion-cloning, we can avoid “shift buildup” (I just made that term up too). We use two variables we create for this purpose, xx and yy, to temporarily store what the (x, y) position would have been had it not been for our oscillation. When we apply the shift to x or y, we do so by adding the cloned value and the shift value together, and assigning it to x or y, thereby avoiding shift buildup.

Wave Motion in any Direction

Our first example creates nice, vertical oscillation. But what if we want the direction to be other than vertical? Well, you can shift x instead of y, and this will give you horizontal oscillation. If you want any other angle, though, it gets tricky.

This just takes more trigonometry, and it’s easy to do once you understand the underlying geometry.  Let’s look at the relationship of the values that make up the trigonometry functions:

unit circle

If you draw a line in any direction θ from an origin point (0,0) to some other point (x, y) on the plane, you have a line of length h, which is the hypotenuse of a right triangle.

If we know the length of x and y, we can use the Pythagorean Theorem (a2 + b2 = c2) to calculate the length of h as sqrt(x2 + y2).

Now that we understand those basics, this is a very handy reference showing how the various trigonometric functions relate to the Unit Circle:

Trig functions and their relationship to the unit circle

Use this to figure out various distances and angles using the right trig functions with your known values.

In this case, h is the shift distance — we know the length of h, and we need to derive the x and y components of this vector, so that we can position-shift our instance in any direction θ. If we know h and we know θ, we can calculate and by using our understanding of trig functions.

Looking at the sin() function, sine is the ratio of the lengths y side and the hypotenuse side of the triangle: y/h.  So, therefore, sin(θ) * shift = y.  We need to keep in mind, though, that in GameMaker rooms, y-zero is at the top of the room and y counts upwards as y-position moves  downward, so we need to use -sin(θ) * shift.

Looking at cos(), cosine is the ratio of the hypotenuse and the x side of the triangle: shift/x.  So cos(θ) / shift = x.

GameMaker provides us these calculations through the functions lengthdir_x() and lengthdir_y().  This makes it easier to use, since we don’t really have to think about or understand the math to call these functions; we only need to know that they exist, and what they can do for us. But understanding how trigonometry works will enable you to do a lot of other useful calculations, so it’s worth explaining here so that you will have this knowledge.

Suppose we want an instance that oscillates in the plane of its own direction of motion. So, if it’s moving at a 45 degree angle, it needs the position shift imparted by our oscillation function to be translated by 45 degrees. No problem, we can alter the shift to incorporate the instance’s direction, using lengthdir_x() and lengthdir_y():

Create:
t = 0;
increment = 12; //degrees -- freq = 1 oscillation per second (1Hz)
amplitude = 10; //pixels of peak oscillation

//clone the x- and y-positions
xx = x;
yy = y;
Step:
t = (t + increment) mod 360;
shift = amplitude * dsin(t);

//clone the movement from the object's speed and direction
xx += hspeed;
yy += vspeed;

//apply the shift
x = xx + lengthdir_x(shift, direction + 90);
y = yy + lengthdir_y(shift, direction + 90);

(By adding 90 degrees to direction in our lengthdir functions, we’re angling the position shift from the oscillation function perpendicular to the direction of travel.) Now, no matter what direction the instance is moving in, it will move along that direction with a nice wavy motion.

Rotating an instance as it oscillates so that it is turning along the slope of the sine wave can be done like this: image_angle = darctan(dcos(t))

This is demonstrated in the below illustration, showing a still capture of a series of rectangular instances being emitted from the blue square at left, moving to the right in a wave motion, and their image angle matching the slope of the curve of the sine wave.

wave with segments angled along the sine waveThe above function works because the slope of the sine wave at t is equal to cos(t), and the angle of the slope is calculated by the arctangent function.

(Incidentally, I’m also “waving” the Hue value as I draw each instance, so that each instance that makes up a segment in the wave is color coded according to its t-value.  You can “wave” all sorts of variables, not just position!)

Additional optimization

If you are performance-conscious, you may want to avoid calling sin() every step. Particularly if you have many instances, if they’re all calling sin() every step, it can add up. And if you’re repeatedly calling sin() with the same argument, it’s wasteful — why are you asking the CPU to re-calculate this value again and again?

You can use an array to store the output of sin() in an array, like so:

//Call this in a create Event
for (i = 0; i < 360; i++)
{
 sintable[i] = dsin(i);
}

Now, instead of calling sin() every step, look up the result of sin(t) by calling the t-th index of the sintable[] array.

shift = amplitude * sintable[t];

You can also make sintable[] a global variable, so that every instance doesn’t have to calculate its own table.

This works great if t is always an integer, and if t is constrained to a value between 0-360. If t is a non-integer, you’ll need to use a rounding function (floor(), ceil(), round()) to approximate to the nearest integer.

Other applications

The position-shift approach to wave motion works well for most uses. But we can introduce oscillation into other properties, as well.

Wave (by direction)

Rather than position shifting, we can change an object’s direction. To do this, use the same code as above, but instead of shifting x or y, shift the value of direction. Remember that the shift amount that you’re applying to the direction is in degrees. So an amplitude of 10 results in a directional wiggle of +/-10 degrees over a period of 1 second.

Wave (by speed)

Shifting the speed of an object results in a lurching motion. If the instance’s starting speed = 10, and you “wave the speed” using your oscillation function by an amplitude of 10, with a frequency of 1 oscillation/second, it will speed up to speed 20, then slow down to speed 0, every second.

Hopping Motion

You can turn a wave motion into hopping motion by using the absolute value function:

shift = abs(amplitude * sin(t))

graph of the equation y = abs(sin(x))

And if you want, you can invert it:

shift = -abs(sin(t))

graph of the equation y = -abs(sin(x))

And of course, you can apply “hopping” to direction or speed just as you can apply it to position.

Demo

Download the project source.

Play the Wave Motion Demo in a new window

Things to notice in the demo:

  1. Wave-length and frequency are inter-related variables. The higher the frequency of oscillation, the shorter the wave length.
  2. As amplitude increases, so does the speed of the instance as it must travel more and more per step. This results in the wave motion becoming choppy, and can also result in problems with collision detection as the waving instance moves more pixels than the size of its collision mask in a single step. To correct for amplitude-induced speed up, drop the frequency by reducing the t-increment.

Cheap Wave

Sometimes, you want wavy motion but you can’t afford it. There’s too many instances in your game, and all the calls to the sin() function are slowing down your game too much. Maybe your object doesn’t change its directional orientation, and you just want a simple vertical wave motion. (Or maybe you’re programming on an 8-bit microcontroller that has no FPU;-) What can you do?

No-sin() Wave Motion

This approach uses a constant for the position shift (as opposed to our sine function) and a timer to reverse the shift. Because we’re using a constant for our shift, we don’t need to calculate it each step, which saves time. Since we’re doing a purely vertical shift, we don’t have to calculate the lengthdir_x and lengthdir_y components of the shift, either, so we save even more time.

This is a much faster function, and the motion is nearly like our sin function — a great compromise when performance is more important than precision. Indeed, the resulting motion is a triangle wave, which is a useful approximation of a sine wave.

If you need to be able to change the direction of the instance and need to maintain perpendicular oscillation, you can just to back to calculating lengthdir_x and lengthdir_y in the Step function, but it’ll cost a little performance.

objCheapWaver

Create:
shift = 1;
alarm[0] = 10;
Step:
y += shift; //or x += shift, if you want horizontal shift.
Alarm[0]:
shift = -shift;
alarm[0] = 10;

With this approach, amplitude is equal to 0.5 * shift * the alarm duration. Frequency is equal to 2 * alarm. Game Maker will accept fractional pixels, but the timers don’t work with fractional steps. Note also that the wave motion will not be centered on the original position of the object — rather, the object starts at the peak or valley of the wave.

Beyond Motion

Creative use of these oscillation functions is encouraged! See what else you can “wave”.

Ideas:

  • Try waving the HSV or RGB values of an object’s color, or alpha.
  • Wave the number of points a bonus object is worth, so that careful timing will maximize the score it gives the player.
  • Audio properties like pitch or gain.
  • I’ve already mentioned waving the position, direction, and speed.
  • What else can you think of?

Further Reading

  1. Unit Circle (Wikipedia)
  2. Trigonometry (Wikipedia)
  3. Inverse Trigonometric Functions (Wikipedia)