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
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:
The player will be able to adjust the timescale with the scrollwheel:
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
# 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()
Blender comes with the extremely useful audaspace library, which is controllable
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.
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.
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
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:
Download the .blend
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()