Sine of the Times. How to add a little bounce to your game.

To every thing there is a season, and every time a purpose. History is said to comes in cycles. Each generation some ideas and fashions fall out of favor and some are rediscovered. But life isn't a circle, for time ever marches forward. A cycle marching forward forms a wave and a wave is a useful thing.

In this article we'll see how to use waves to add a bit of life to our games.

A cycle marching forwards is a useful thing.

The sine wave is illustrated above, the x axis moves from 0 to 2π and the y value is math.sin(x). Looking at the illustration you can see how the sine wave is the y coordinate of a circle as it's drawn. The sine function returns values from -1 to 1 but the rate of movement is not linear, it's smooth and wave like.

Intuition

Sine waves are great for special effects but to know where and how to use it you need to build up a little wave-intuition. Try thinking of sine as a infinite wave smoothly going up and down. This wave like motion is something that can easily be applied to objects in your game.

Let's consider a concrete example. Imagine Mario or any 3d platformer, in the game, you might collect stars or coins, and in the game world these collectable items float in the air bobbing up and down. The bobbing motion is an application of a wave. We just decide how long we want the bob to be and then apply the sine wave to the Y position of the item.

Bobbing

We can use waves to bob game objects up and down.

HTML 5 Interactive figure. If you're seeing this, your browser may not support HTML5.

The above diagram shows how we take the value of the wave at a moment in time and apply it to the Y position of the circle. Notice that the rate of moment is smooth, this wouldn't look as nice with a linear rate of movement.

local gTime = 0
local gBobDistance = 50 -- pixels
local gCircle = CreateCircleSprite()

-- Center of the screen, sprites are center aligned.
local gCirclePosX = 0
local gCirclePosY = 0

function update()

    gTime = gTime + Time.deltaTime

    local sine = math.sin(gTime) -- In the range -1 to 1
    local yOffset = gBobDistance * sine

    gCircle:SetPosition(gCirclePosX,
                        gCirclePosY + yOffset)

    gRenderer:DrawSprite(gCircleSprite)
end

The example shows an effect that would work well with an item the player should collect but the behavior can be applied in many other places! Imagine a Final Fantasy style selection menu using a white glove cursor. The cursor is bobbed horizontally to draw the eye to the selected item. A similar use case is bobbing a continue caret vertically in a dialog box to indicate that there's more text to read.

Laser Beams

If you want to make a nice wavy laser beam, in a vertical shooter or a platformer; sine waves are the way to go. Start by creating you laser with a straight line, then you can apply the wave to the line by breaking it into segments and applying the sine wave to each part using the normal of the line. Collision detection can be a little trickier! The simplest way is to consider the beam a rectangle the height being the wave height. For more accurate collision testing each segment gives a good approximation.

The Sea

Sine waves work really well for waves cresting on the beach. As well as 3d waves moving through bodies of water. A little experimentation can give you reasonable water effects as long as you don't the water to be too dynamic.

Applying sine to a mesh's UVs can be used to rhythmically move a texture back and forth which works well for water.

Pulsing

To make a bob effect we apply waviness to the Y position. To make a pulse effect we apply the waviness to the scale pulsing a game object up and down.

HTML 5 Interactive figure. If you're seeing this, your browser may not support HTML5.

Pulsing game objects is useful for all sorts of situations; indicating when an enemy has been hit, or a extra life is awarded, collectables, imminent explosions etc.

local gTime = 0
local gPulseScale = 0.1 -- 10 percent
local gCircle = CreateCircleSprite()

function update()

    gTime = gTime + Time.deltaTime

    local sine = math.sin(gTime) -- In the range -1 to 1
    local scale = 1 * sine

    gCircleSprite:Scale(1 + scale, 1 + scale)
    gRenderer:DrawSprite(gCircleSprite)
end

A pulse effect might change color rather than size. In this case we can use the waviness on the alpha channel or to control a Lerp (don't know this word? Learn more here) between two colors. Remember the sine wave goes from the -1 to 1 but alpha and Lerp are more suited to 0 to 1 range. To get the sine return value into the correct range you can map directly to 0 - 1 or use a math.abs - both give a pulse-like result but in different ways.

Transitions

The full wave cycle for sine repeats every 2π. From 0 to π we have wave that goes from 0 to 1 and back to 0. Then from π to 2π we go from 0 to -1 and back to 0. If we just take the first half of the wave it can use to make nice transitions. We can fade to black, or smoothly move a game object in space or scale something up or down. Using a sine wave makes the movement smooth and more natural.

I use a helpful Tween class and one of the functions for moving things around uses the sine wave. Now by reading the code, you can probably understand how it works (whereas before it may just have been a string of mathematical-noise). Whereas the other examples are in pseudo-code Lua, this function is taken from my c# codebase.

public static float EaseOutSine(float timePassed, float start,
                                float distance, float duration)
{
    return distance * Mathf.Sin(timePassed/duration * (Mathf.PI/2)) + start;
}

Tweaking Waves

The basic sine wave is useful but we can change it's properties to make it even more suitable for our needs.

local sine = math.sin(time) -- returns cyclic wave in the range -1 - 1

Faster

We make the wave faster by increasing the time value passed in. Below shows a wave that would a bob or pulse occur twice as quickly.

local speed = 2
local sine = math.sin(time * speed)

Deeper

Making the waves deeper is also known as increasing the amplitude. We can make our bob behavior cover 2x the distance by multiplying the result by 2 thereby increasing the range for -1 to 1, to -2 to 2.

local amplitude = 2
local sine = math.sin(time) * amplitude

Offset

Sometimes we might want items to pulse at different offsets, so the pulse isn't synchronised. We can do this by adding a value to the time.

local offset = 3.141
local sine = math.sin(time + offset)

Wave Operations

You can add waves together, subtract and multiply them to get more interesting wave behaviors for your special effects. There are correct math ways to do this or just can just average them together like below. With an intuitive sense for what's going to happen you'll know what you can get away with in order to reach a desired effect.

local offset = 3.141
local sine1 = math.sin(time + offset)
local sine2 = math.sin(time)
local combined = (sine1 + sine2) / 2

To get a square wave you can use the round function.

local digital = math.round(math.sin(time))

You can use an absolute function to get a wave that spends half it's time at 0 and half it's time transitioning to and from 1.

local absWave = math.abs(math.sine(time))

Making small changes to the standard sine wave can give you effects that look organic or mechanical or something in-between!

More than Waviness

For special effects it's helpful to think of sine as bottled waviness essence but it's more than that! You can use it to draw circles and arcs, for polar coordinates and many many other applications in geometry. As a game programmer you don't need to know everything about sine, you just need to know how to use it for games. The more you learn, the more power you have! I'd suggest dipping your toe in and becoming comfortable with a few uses of sine. Then periodically dive a little deeper and get a more filled-out understanding of how it works and it's used in other areas of mathematics.

Proviso

Sin (and it's friend Cos) can be relatively expensive functions (check out the source code). If you find yourself using a lot of them (thousands per frame) you might consider using a look-up table to cache the results at a certain resolution. For instance you might make a big table with the values for each 0.01 increment.

Hope you've learned something new about sine and that your future games will be all the more bouncy because of it!