Slow motion in the BGE

Controlling the speed of time

Thanks to the hard work of the devs (as always), Blender 2.77 has improved methods for handling time in the BGE.
Controlling the rate at which time passes is now quite easy to do via bge.logic.setTimeScale(<multiplier>):

Set the time multiplier between real-time and simulation time. A value greater than 1.0 means that the simulation is going faster than real-time, a value lower than 1.0 means that the simulation is going slower than real-time.

Here's an example with two functions, one for accelerating time and one for decelerating.

# time_control.py

# Get the setTimeScale() and getTimeScale() methods from bge.logic
from bge.logic import setTimeScale, getTimeScale

# How much to increase or decrease the timescale at once
time_increment = .03
# How high we allow the timescale to go
time_max = 1
# How low we allow the timescale to go. Mustn't let the timescale get completely to 0, otherwise the game will stop!
time_min = .01

# A simple function which ensures "value" is between "min_value" and "max_value" (inclusive)
def clamp(value, min_value=0, max_value=1):
    return max(min(value, max_value), min_value)

def speed_up():
    # Set the timescale to the current timescale + time_increment, but within time_min and time_max
    setTimeScale(clamp(getTimeScale() + time_increment, time_min, time_max))

def slow_down():
    # Same as above, but - time_increment instead of +
    setTimeScale(clamp(getTimeScale() - time_increment, time_min, time_max))

So for example, when the above script is hooked up with some sensors like this:

Two mouse sensors, one set to Wheel Up and one to Wheel Down, connected to two python controllers referencing the respective speed<em>up() and slow</em>down() functions defined above

The player will be able to adjust the timescale with the scrollwheel:

Adjusting logic

You may notice that your gamelogic doesn't slow down along with the physics and animations.
For example, this script will spawn a coin every 30 logic tics.

# counter which keeps track of logic tics elapsed since last spawning
spawn_time = 0
# time between spawnings (in logic tics. In this case, 30 tics = .5 seconds)
spawn_wait = self.get("spawn_wait", 30)

def main(self):

    # check if it's time to spawn another coin
    if self.spawn_time > self.spawn_wait:
        # if yes, spawn the coin and reset the timer
        self.spawnCoin()
        self.spawn_time = 0

    # increment the timer by 1
    self.spawn_time += 1

This script is simplified and unrelated code is omitted for the sake of example. The full script can be found in the .blend as coin_spawn.py. (see below for download)

As you can see, coins keep spawning at the same rate even when the timeScale is reduced:

To fix this, all we need to do is make the timer increment slower when time is running slower. Instead of adding 1 to the timer each tic, we add the current timeScale:

# timer which keeps track of logic tics elapsed since last spawning
spawn_time = 0
# maximum range at which coins can spawn
range = self.get("range", 10)
# time between spawnings (in logic tics. In this case, 30 tics = .5 seconds)
spawn_wait = self.get("spawn_wait", 30)

def main(self):

    # check if it's time to spawn another coin
    if self.spawn_time > self.spawn_wait:
        # if yes, spawn the coin and reset the timer
        self.spawnCoin()
        self.spawn_time = 0

    # increment the timer by 1
    self.spawn_time += bge.logic.getTimeScale()

Much better:

Adjusting sound

Blender comes with the extremely useful audaspace library, which is controllable through the aud python module.

Using this module, we can play audio files in 3D and adjust their pitch in real-time. However, in order to get 3D audio we must first set up a bit of boilerplate.

  1. Before we can even play think of playing a sound, we need a way to store and represent one. aud provides us with a Factory object for doing just that. The simplest way to create one is with:

    myFactory = aud.Factory.file("/absolute/path/to/file.wav")
    

    bge.logic.expandPath("//path/relative/to/.blend") can be used to specify a relative path.

  2. Next we need to specify the manner in which the sound will be output, with a Device. This object will keep track of the current position, orientation, and velocity of the "listener", in order to produce 3D sound with doppler shifts etc.
    To create and configure our device:

    # Get the gameobject who's position etc. will be used for "listening" to the 3D sound
    listener = bge.logic.getCurrentScene().objects["<your object's name here>"]
    
    
    # Initialize aud device once for all coin objects
    sound_device = aud.device()
    # Tell audaspace to make sound fade out with distance
    sound_device.distance_model = aud.AUD_DISTANCE_MODEL_LINEAR
    # Set listener variables
    sound_device.listener_location = listener.worldPosition
    sound_device.listener_orientation = listener.worldOrientation.to_quaternion()
    sound_device.listener_velocity = listener.worldLinearVelocity
    
  3. Finally we can play our sound, and in the process create a Handle which allows us to manipulate that sound as it plays.

    # Play the sound, using the sound_device and factory created earlier
    sound_handle = self.sound_device.play(myFactory)
    sound_handle.relative = False
    sound_handle.distance_maximum = 100
    sound_handle.distance_reference = 2
    
    
    # set the pitch as an example
    sound_handle.pitch = <pitch multiplier>
    

By setting the handle's pitch to the current timescale at every logic tic, we get sound which adapts to the timescale.

sound_handle.pitch = bge.logic.getTimeScale()

The full script used in the example .blend is as follows:

import bge
import random
import aud
from utility_functions import clamp

# Load and buffer sound files into aud Factories
floor_sound = aud.Factory.buffer(aud.Factory.file(bge.logic.expandPath("//coin_falling_on_concrete.wav")))
coin_sound = aud.Factory.buffer(aud.Factory.file(bge.logic.expandPath("//coins.wav")))

class Coin(bge.types.KX_GameObject):

    # The object who's postion/orientation will be used for "listening" to the 3D sound
    listener = bge.logic.getCurrentScene().objects["Player"]

    # Initialize aud device once for all coin objects
    sound_device = aud.device()
    # Tell audaspace to make sound fade out with distance
    sound_device.distance_model = aud.AUD_DISTANCE_MODEL_LINEAR
    # Set listener variables
    sound_device.listener_location = listener.worldPosition
    sound_device.listener_orientation = listener.worldOrientation.to_quaternion()
    sound_device.listener_velocity = listener.worldLinearVelocity

    def __init__(self, own):

        self.sound_handle = None
        self.pitch_offset = 0

    def play_sound(self, factory):

        # Play the sound, using the sound_device and factory created earlier
        self.sound_handle = self.sound_device.play(factory)
        self.sound_handle.relative = False
        self.sound_handle.distance_maximum = 100
        self.sound_handle.distance_reference = 2
        # Pick a random offset between -.3 and +.3 to be applied to the pitch later
        self.pitch_offset = random.randrange(-30, 30)*.01

    def main(self):
        # Check that there's a sound_handle and it's currently playing sound
        if self.sound_handle and self.sound_handle.status == aud.AUD_STATUS_PLAYING:

            # Tell aud device the most recent location/orientation/velocity of our listener object
            self.sound_device.listener_location = self.listener.worldPosition
            self.sound_device.listener_orientation = self.listener.worldOrientation.to_quaternion()
            self.sound_device.listener_velocity = self.listener.worldLinearVelocity

            # Tell aud handle the most recent location/orientation/velocity of this coin object
            self.sound_handle.location = self.worldPosition
            self.sound_handle.orientation = self.worldOrientation.to_quaternion()
            self.sound_handle.velocity = self.worldLinearVelocity

            # Update pitch to match current timescale,
            # plus a random offset to make sounds a little different each time.
            # Also clamp to ensure pitch is never set below .1, otherwise our file starts to sound very distorted and ugly.
            self.sound_handle.pitch = clamp(bge.logic.getTimeScale() + self.pitch_offset, .1)

def main(cont):
    own = cont.owner

    if "init" not in own:
        own["init"] = True
        own = Coin(own)

    collision_sensor = cont.sensors["Collision"]

    # If we've just collided and we are not already playing a sound
    if collision_sensor.status == bge.logic.KX_SENSOR_JUST_ACTIVATED and not own.sound_handle:
        # If we've hit another coin object, play coin_sound (//coins.wav)
        if "coin" in collision_sensor.hitObject:
            own.play_sound(coin_sound)
        # If we've hit the floor, play floor_sound (//coin_falling_on_concrete.wav)
        elif collision_sensor.hitObject.name == "Floor":
            own.play_sound(floor_sound)

    # Update audaspace variables
    own.main()
Download the .blend