Game Maker Wave Motion Tutorial

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)

14 Comments

Add a Comment
  1. This was super helpful! However, I’m having trouble getting it to work using direction rather than the motion cloning. Could you post the exact code? I’ve been at it for a night or two now and I can get it to work but… not exactly how I want it to. What I want is arrows that slither with a higher stronger and stronger oscillation over time– They do that… however now my arrows will only travel one direction when I move them using direction rather than motion cloning. This is my step event:

    speed = room_speed/6

    time += increment;
    shift = amplitude * sin(time);

    x_pos += hspeed;
    y_pos += vspeed;

    //makes sure arrow face the way its travelling to make it slither

    //x = x_pos + lengthdir_x(shift, direction + 90);
    //y = y_pos + lengthdir_y(shift, direction + 90);

    direction = shift;

    image_angle = direction;
    amplitude += 1;

    }

    Thanks in advance for any help or advice!

      

    1. I guess that section of the article is a little bit misleading. Rather than say, “use the same code as above, but instead of shifting x or y, shift the value of direction” I should have said “use the same general approach as above.”

      I don’t think you want to set direction equal to shift — rather, you would want to add a shift to the direction.

      so,

      direction += shift

      not

      direction = shift

      Note that if you do it this way, you’ll have the same “shift buildup” problem, only affecting direction rather than the x and y. So, you can create a temp variable to track what the original direction was, call it dd, and do this each step:

      direction = dd + shift;

      (You’d set dd = direction in the Create event; thereafter, dd would only need to be updated if some other change to direction besides the oscillation were to occur.)

      That way, the shift from each previous step won’t add itself cumulatively to the overall direction.

      Hope that helps!

        

  2. This is the most helpful thing I’ve ever been able to find. Thanks a lot!

      

  3. it doesn’t work in GMS2 I tried offsetting “t” and it does weird things

      

    1. Thanks for the report. This tutorial was written a few years ago, in GMS1.x. I’m not planning on updating these articles for GMS2, but will be writing new articles as I get into the beta.

      Update 1/25/2017: I’ve taken my WaveDemo project and imported it into GMS2, and it works. I don’t find any problems with varying t.

        

      1. Chris, trying to download the source code above takes me to a corrupted HTML page. Have you any alternative links?

          

        1. I just tested the download link, and it seems to be working fine. Try it again.

            

  4. Thanks! This was really helpful. I was trying to make a wavy text effect and this helped a lot.

      

  5. Hi! Thank you for this tutorial!
    I’m trying to see if I alter your code to have the character move along a sine wave to the side. I guess just adding a hspeed will do.
    But I also want to have the speed of movement constant, so a full cycle on a larger sine will take more time to complete than on a smaller one.
    I can’t figure out how to do that, or if it’s possible using this approach.
    Could you point me in the right direction?

      

    1. That’s a great question. It will take a little bit more effort to do this than I cover in the demo.

      If I may, I will refer you to my game for Global Game Jam 2017, Quaver. I’m not 100% sure, but I think you may find the answer you are looking for in the source code there. It’s been about 2 years since I looked at it, but I think I have some additional calculations in there that does exactly what you’re looking to do.

      If not, ask again and I’ll try to set you right.

        

      1. Hey Chris!

        Thanks for your fast reply, much appreciated!

        What I’m trying to do might require calculus and integrals to calculate the player’s position on a sine and then to draw an altered sine thus changing the player’s path this way. But I’m looking for simpler alternatives to ‘fake it’, cuz I have no idea how to do that or if that’s even possible in GameMaker… This might be to hard for my first game LOL…

        I tried to run your game but it didn’t react to the input from my crusty old Logitech dual action pad, hmmm…

        If you have any ideas for the best method to do this – tell me please! :)

          

  6. Hey Chris,
    I have another problem concerning the sine wave: I want to create a sine wave-like laser (pointing towards the bottom).
    I can’t just stretch the sprite of the laser, because the laser’s path is obviously not a straight line. I also tried to chain the same object onto another, which kind of looked like a wave but also caused a big loss of performance.
    You could maybe give me some help with that?

      

    1. @Danilo – I’m not sure… In Quaver, I took the approach of spawning a bunch of instances that moved in a sinewave pattern, and if I spawned them close enough together they would combine into a contiguous line. For me, this was easy to implement and didn’t result in a performance problem, but depending on what else you have going on in your game, I’m sure it could be a problem to have so many instances being spawned and destroyed to simulate a sinewave.

      I’m sure there’s other, better methods to achieve the same effect, but I haven’t thought of them. It would depend in part on the exact effect that you’re trying to produce. My thought would be to look into shaders for the graphical appearance, and for collisions, maybe an approach that chains a bunch of collision_line() calls could do, but I expect that this would get expensive.

      If you go with a chain of instances, you can try to keep them as lightweight as possible, by using a manager object that updates all of the instances that make up the wave. If you set it up so that every instance isn’t keeping track of its own theta and performing sine calculations every step, you can reduce a lot of the processing overhead, and get back a lot of the performance that you’re losing.

      I believe that’s the approach I took in Quaver as well — but, again, I haven’t looked at that code since I wrote it, so my memory isn’t very clear.

      It also occurs to me that if your frequency is constant, and the theta increment divides evenly into 360, or (2*Pi if you’re working with radians) then you could pre-calculate all the sin(theta) values in advance, and store them in an array, and do all your position shifts by looking up the shift amount in the array, rather than re-calculating it every step. I bet this approach would save you a lot of CPU as well. The only time you’d really need to calculate the shift every step is if the frequency could change.

      I hope this all makes sense, and helps with your problem.

        

      1. It totally helped me! I feel bad that I couldn’t geht the answer myself *shrug*

          

Leave a Reply to ChrisCancel reply