dot big bang

Developer API
Menu
All
  • Public
  • Public/Protected
  • All

Good sound design can set a game's tone, give feedback when things happen, and otherwise enrich an experience which would otherwise be limited to the visual and tactile senses.

In dotbigbang, sound is controlled primarily using the TS API. Playing sounds have associated state, which you can use to manipulate their gain, pitch, or spatial audio properties.

The TS API functions and types referenced on this page were introduced in API version 0.1.3, so make sure you are on that version or a later one to use them.

Contents

  1. Playing a Sound
  2. Playing a Spatial Sound
  3. Looping Behavior
  4. Manipulating Playing Sounds
  5. Updating the Audio Listener
  6. Master Gain
  7. Advanced Usage

Playing a Sound

There are three methods for playing a sound from TS API:

These will be explained in the examples below.

Example 1 - Playing a Sound Simply

Playing a sound "simply" (that is, without any special settings), is very easy:

soundRef = new SoundRef();

start() {
  const sound = this.soundRef.get();
  if (!sound)  throw new Error("soundRef must be set in the Game Editor!");

  // Play the sound at default gain and pitch.
  this.game.audio.play(sound);

  // Play the sound at half gain (volume).
  this.game.audio.play(sound, 0.5);

  // Play the sound at half pitch (speed).
  this.game.audio.play(sound, undefined, 0.5);
}

Use this in cases where you want to play a sound without spatialization, like when clicking a UI button or something.

Example 2 - Playing a Random Sound With Random Pitch

Sometimes, you may want to play a sound with a random pitch modifier within some range. For example, maybe you're making a combat game, and you don't want each sword hit to sound exactly the same.

In such cases, it's simple to make a pool of sounds to choose from, and play them with a random pitch, like so:

sound1 = new SoundRef();
sound2 = new SoundRef();
sound3 = new SoundRef();
sound4 = new SoundRef();

minPitch = 0.75;
maxPitch = 1.25;

private _sounds: Sound[] = [];

start() {
  // Build the pool of sounds.
  for (let i = 1; i <= 4; ++i) {
    const sound = (this as any)[`sound${i}`]?.get();
    if (sound) {
      this._sounds.push(sound);
    }
  }
  if (!this._sounds.length) {
    console.log("WARNING: no sounds provided!");
  }
  this.playRandomSound();
}

playRandomSound() {
  // Early-out if we have no sounds to play.
  // Needed because Random.index throws with a length of zero.
  if (!this._sounds.length) {
    return;
  }
  // Choose a sound from the pool.
  const index = Random.index(this._sounds.length);
  const sound = this._sounds[index];
  if (sound) {
    // Choose a random pitch within our range.
    const pitch = Random.numberInRange(this.minPitch, this.maxPitch);
    // Play the sound.
    this.game.audio.play(sound, 1.0, pitch);
  }
}

In this example, the sound will be played at full gain with a pitch multiplier (speed) in the range [0.75, 1.25].

Playing a Spatial Sound

In games with 3D gameplay, you often want sounds to seem as if they are "in the world" of the game, attenuating with distance from the camera, and panning between the left and right speakers based on their orientation to it.

There are two methods to play a spatial sound in dotbigbang:

Use playFromEntity when you want a sound to be "attached" to a specific Entity and follow it in 3D space. For example, say you have a moving enemy, and you want its growling sound to follow it around the world. For another example, say you have a spaceship, and you want it to make engine sounds as it flies.

Use playFromLocation when you want a sound to be attached to a specific point in 3D space. For example, you may want to play footstep or weapon sounds wherever the corresponding gameplay events happened in the game world.

Example 1 - Playing a Sound Intermittently From an Entity

In this example, we wait a random amount of time in a specified range, then play a growling sound from our Entity. Then, we reset the timer for the next growl.

If the previous growl is still playing when it's time to start the next one, we stop it early, so the two sounds won't overlap each other.

The sound will "follow" the Entity as it moves around in space.

soundGrowl = new SoundRef();
minWait = 3;
maxWait = 6;

private _timeNextGrowl = 0;
private _playingSound: SoundPlayback|null = null;

start() {
  this._scheduleGrowl();
}

postCollisionTick() {
  if (this.game.frameTime >= this._timeNextGrowl) {
    const sound = this.soundGrowl.get();
    if (sound) {
      // If a previous growl is playing, stop it first.
      this._playingSound?.stop();
      // Play our sound from this entity.
      this._playingSound = this.game.audio.playFromEntity(sound, this.entity);
      // Schedule the next one.
      this._scheduleGrowl();
    }
  }
}

private _scheduleGrowl() {
  this._timeNextGrowl =
    this.game.frameTime + Random.numberInRange(this.minWait, this.maxWait);
}

Example 2 - Playing Footstep Sounds

In this example we specify two sets of footstep sounds to play when we walk on Entities with an associated tag. We build a list of sounds for each "material", and then play a random one with a random pitch when "stepping" on it.

The sound will remain at the location where the footstep occurred.

// How far in world units we must travel before playing the next footstep sound.
stride = 30;

// Sounds for stepping on grass.
grass1 = new SoundRef();
grass2 = new SoundRef();
grass3 = new SoundRef();

// Sounds for stepping on wood.
wood1 = new SoundRef();
wood2 = new SoundRef();
wood3 = new SoundRef();

private _tags: string[] = ['grass', 'wood'];
private _sounds: Record<string, Sound[]> = {};
private _lastStepLocation = new Vector3();

start() {
  // Build a list of sounds for each tag.
  for (const tag of this._tags) {
    this._sounds[tag] = this._buildListFor(tag);
  }
}

tick() {
  // Check if we moved far enough to play a footstep sound.
  const movedEnough = this.entity.worldTransform.position
    .distanceToSquared(this._lastStepLocation) >= this.stride ** 2;
  if (!movedEnough) {
    return;
  }

  // Track where the last footstep sound happened.
  this._lastStepLocation.copy(this.entity.worldTransform.position);

  // Check if we stepped on something we have a sound for.
  const hitTag = this._findHitTag();
  if (!hitTag) {
    return;
  }
  const sounds = this._sounds[hitTag];
  if (!sounds?.length) {
    return;
  }

  // Choose a random sound from the list.
  const sound = sounds[Random.index(this._sounds[hitTag].length)];
  if (!sound) {
    return;
  }

  // Play the sound at our feet with a randomized pitch.
  this.game.audio.playFromLocation(
    sound, 
    this.entity.worldTransform.position, 
    0.2, 
    Random.numberInRange(0.8, 1.2)
  );
}

// Build a list of sounds from our properties with a specific prefix.
private _buildListFor(prefix: string) {
  const list: Sound[] = [];
  for (let i = 1; i <= 3; ++i) {
    const sound = (this as any)[`${prefix}${i}`]?.get();
    if (sound)  list.push(sound);
  }
  return list;
}

// Create a ray facing down.
private _ray = new Ray(undefined, new Vector3(0, -1, 0));

private _findHitTag() {
  // Place the ray 20 units above our feet.
  this._ray.origin.copy(this.entity.worldTransform.position);
  this._ray.origin.y += 20;
  // Look for what's beneath us.
  const hits = this.game.getEntitiesWithColliderIntersectingRay(this._ray, 25);
  for (const hit of hits) {
    // See if we hit something with an associated sound.
    for (const tag of this._tags) {
      if (hit.entity.tags.has(tag)) {
        return tag;
      }
    }
  }
}

Looping Behavior

Sounds can be looped, and you can supply the start and end points of the loop, in seconds, when playing a sound.

Note: If you play a looping sound, and you don't keep up with the SoundPlayback instance associated with it, you will be unable to stop the sound!

Example 1 - Playing and Stopping a Looping Sound

This script models a simple piston or platform that moves up and down, waiting for a time at the top and bottom of its path. When it begins to move or stops moving, it plays a "one off" sound, and during motion it plays a looping sound.

enum State {
    WaitBottom,
    WaitTop,
    MoveDown,
    MoveUp,
}

export class Script extends UserScriptComponent {
  
  startSound = new SoundRef();
  stopSound = new SoundRef();
  moveSound = new SoundRef();

  moveY = 30;

  waitTime = 2;
  moveTime = 5;

  // Which part of the cycle we're currently in.
  private _state = State.WaitBottom;
  // The frameTime when this phase ends and the next begins.
  private _timerEnds = 0;

  private _startPos = new Vector3();
  private _targetPos = new Vector3();

  // To hold the playing, looped sound during movement.
  private _moveSound: SoundPlayback|null = null;

  start() {
    // Store where we are and where we'll be going.
    this._startPos.copy(this.entity.localTransform.position);
    this._targetPos.copy(this._startPos);
    this._targetPos.y += this.moveY;
    // Set the timer for the first phase.
    this._timerEnds = this.game.frameTime + this.waitTime;
  }

  tick() {
    switch (this._state) {
      case State.WaitBottom:
        if (this.game.frameTime > this._timerEnds) {
          this._state = State.MoveUp;
          this._timerEnds = this.game.frameTime + this.moveTime;
          this._playSound(this.startSound);
          this._moveSound = this._playLoopingSound(this.moveSound, 0, 1);
        }
        break;
      case State.WaitTop:
        if (this.game.frameTime > this._timerEnds) {
          this._state = State.MoveDown;
          this._timerEnds = this.game.frameTime + this.moveTime;
          this._playSound(this.startSound);
          this._moveSound = this._playLoopingSound(this.moveSound, 0, 1);
        }
        break;
      case State.MoveUp:
        if (this.game.frameTime > this._timerEnds) {
          this._state = State.WaitTop;
          this._timerEnds = this.game.frameTime + this.waitTime;
          this._playSound(this.stopSound);
          this._moveSound?.stop();
          this.entity.worldTransform.position.copy(this._targetPos);
        } else {
          // Animate moving up to the target position.
          const t = 1.0 - (this._timerEnds - this.game.frameTime) / this.moveTime;
          this.entity.worldTransform.position
            .lerpVectors(this._startPos, this._targetPos, t);
        }
        break;
      case State.MoveDown:
        if (this.game.frameTime > this._timerEnds) {
          this._state = State.WaitBottom;
          this._timerEnds = this.game.frameTime + this.waitTime;
          this._playSound(this.stopSound);
          this._moveSound?.stop();
          this.entity.worldTransform.position.copy(this._startPos);
        } else {
          // Animate moving down to the start position.
          const t = 1.0 - (this._timerEnds - this.game.frameTime) / this.moveTime;
          this.entity.worldTransform.position
            .lerpVectors(this._targetPos, this._startPos, t);
        }
        break;
    }
  }

  private _playSound(ref: SoundRef) {
    const sound = ref.get();
    if (sound)  this.game.audio.playFromEntity(sound, this.entity);
  }

  private _playLoopingSound(ref: SoundRef, loopStart: number, loopEnd: number) {
    const sound = ref.get();
    if (sound) {
        return this.game.audio.playFromEntity(sound, this.entity,
            undefined, // stop policy
            undefined, // gain 
            undefined, // pitch
            undefined, // offset
            true,      // looping
            loopStart,
            loopEnd
        );
    }
    return null;
  }
}

Manipulating Playing Sounds

All playback methods return a SoundPlayback instance, and you can use it to change the properties of your sound as it plays. For example, you could change the gain or pitch of the sound.

You could use this to great effect when playing engine sounds for a vehicle, or by slowing down music in a cutscene to make it out of key.

If you are playing a spatial sound, and you want to manipulate its spatial properties during playback, you will need to store a SoundPositionalPlayback instead of a normal SoundPlayback—otherwise, storing a regular SoundPlayback is fine.

Example 1 - Changing the Pitch of a Playing Sound

In this example, we change a sound's pitch based on a sine wave as time passes. This results in the sound slowing down and speeding up in a smooth curve.

soundRef = new SoundRef();

// How long the sine wave should be, in seconds.
period = 6;

private _timer = 0;
private _playback: SoundPlayback | null = null;

start() {
  // Start a looping sound.
  const sound = this.soundRef.get();
  if (sound) {
    this._playback = this.game.audio.play(
      sound, // the sound to play
      0.5,   // gain
      1.0,   // starting pitch
      0,     // offset from start (in seconds)
      true   // looping
    );
  }
}

tick() {
  if (!this._playback) {
    return;
  }
  this._timer += this.game.frameDeltaTime;
  // Change the sound's pitch in a sine wave while it plays.
  this._playback.pitch = 1.0 + Math.sin(this._timer/this.period * Math.PI) * 0.5;
}

Updating the Audio Listener

Use this.game.audio.setListenerPosition to set the position in 3D space where the player's "ear" should be considered for spatial sounds. This will affect the attenuation of spatial sounds.

Use this.game.audio.setListenerOrientation to specify how the player's "ear" is rotated in the world. This will affect the panning of spatial sounds.

Most games come with a camera Entity which already has the UserScript dbb_spatialized_audio_listener on it, which will call the methods mentioned above to update the audio listener as you play.

In some games, the option onlyUpdateIfEntityIsActiveCamera is turned on, so that this.game.cameraEntity can be swapped out for cutscenes etc, and the listener will follow the new camera seamlessly.

In other games, the player Entity itself is the audio listener, and in others still, the player Entity updates the listener's position, but the camera Entity updates the listener's orientation.

For most types of games, it's fine to leave the active camera Entity as the audio listener.

Master Gain

You can set this.game.audio.masterGain to a value between zero and one, inclusive, to limit the volume of all sounds in the game. This is a simple multiplication: Whatever gain the sound would have is multiplied with masterGain for playback.

That is, a masterGain of 0.5 will play all audio in the game at half volume.

Setting this value affects all currently playing sounds, and all sounds played after.

Advanced Usage

There are several parameters for spatial sounds left unexplored here, and you can find those documented on this page.

Using them, you can specify how the sound should attenuate as the listener moves farther away from it, what radii are used for those calculations, and more.