Ambient Sounds - Zone (using triggers)
written by demagogue
Note: This system is still being developed and will certainly change in the future, and possibly made a general Darkmod feature. But for now it's functionable, and anyway instructive for learning about scripting.
This describes a method of doing ambient sounds in which a trigger turns on an omni ambient when you enter a zone, then turns it off when you leave, possibly starting a new ambient for the new zone. It uses a script, a custom soundshader, and some in-map entities.
It's useful for situations when you want one ambient sound to cover a very large or complex area that might be unweildy with multiple overlapping speakers, or any other situation where using speakers causes an issue. On the other hand, in a lot of situations it's much easier to just use speakers, so it's not for every use. (For the method using speakers, see this tutorial: Adding_ambient_Sounds_to_your_Map#Localized_Ambient_Sounds_in_one_Area_.28with_Speakers.29).
- Pros: A robust way to keep sound from leaking into areas you don't want it, or losing sound where you do. For a very big area, or inside buildings or winding paths, a few triggers will fully cover the whole area with one seamless ambient, whereas it might take many speakers that have to overlap and aren't as seamless. Also, you can change the ambient for a whole area by changing the property of one button (as opposed to many speakers).
- Cons: More involved setup than a speaker. An ambient fading in at the start works differently than a speaker (could be a pro or con depending on what you want to do and preference). You need to make sure the player can't enter/leave the area without going through a trigger to turn on/off the ambient (a speaker doesn't care). And at the beginning you should test it to make sure everything is working (speakers are drop and go).
Mechnically, it works like a radio, where touching the trigger changes the station. For that reason, the method is also a good template for any type of machine with various "states" that each have their own sound, e.g., pressing different buttons turns on their own unique sound, stopping the previous ones, including a button to turn all sounds off. But I'll only deal with using it for zoning ambient sounds here.
Set up
The Blueroom
- Say there are 3 ambients you want to use in your map with the zone approach (it's easy to change the number once you get the principles, and you can even set up for more than you might actually need. You don't have to use every channel, but it's set up in case you want to add some more ambients later).
- In your map, make a blueroom (i.e., a room off to the side the player will never enter) and create a set of 4 "command" buttons in it, the "radio stations", one for each sound you want to use, plus an extra one reserved for silence and turning all ambient sounds off. Create a button by right-click in the map window, create model, darkmod->mechanical->numberwheel_button.lwo->ok, then change the classname to "atdm:mover_button", then copy it as many times as you need.
- Also set up an ambient speaker in the blueroom (right-click, create-entity, Darkmod->Sound->speaker_ambient_music).
- (Two asides: 1. A blueroom is normally textured with common/caulk. You might temporarily put the buttons on a brush, light the blueroom, and have some other textures, all of which you can get rid of later, so you can put the StartingPoint inside and easily see and test the buttons later, which I recommend you do if you have any trouble. 2. The buttons would be your "machine" if you just wanted to use this method for machine sound-management.)
- The Ambient Speaker
- Name the ambient speaker you created "ambient_player" (If you want a different name, you'll need to change the name in the script below to correspond.)
- The ambient speaker, at least for the purposes of this tutorial, works differently from a normal speaker. Instead of each speaker having its own individual soundshader to play using the s_shader property, our one ambient speaker is going to contain multiple properties covering all the sounds we want to play with the zone method. Again, it's like one radio with multiple stations.
- Now you're going to add consecutive properties in the following form for as many ambients as you want to use with this approach, with values that I will explain below.
- snd_station1
- snd_station2
- snd_station3
- etc.
- Now you're going to add consecutive properties in the following form for as many ambients as you want to use with this approach, with values that I will explain below.
- For each snd_station* property, you need to enter the value as the soundshader name of the sound you want to play. Since you cannot add separate play instructions like a normal speaker (like looping, global, omni, etc), this information needs to be in the soundshader itself. I have created a soundshader with instructions typical of ambient sounds (that covers all of the Darkmod ambients_ambience sounds) which you can create and drop into the Darkmod/sound folder (custom_ambient_trig.sndshd, I'll make a new wiki page for it for now Custom ambient trig so it doesn't get lost). For its naming convention, it basically uses the normal ambient name (listed in sound/tdm_ambient_ambience.sndshd), and adds the suffix "_trig" so there isn't a conflict.
- You can listen to the Darkmod ambients in the folder "Darkmod\sound\ambient\ambience" to choose the one you like (if your music player can play .ogg's), then the snd_station* value is going to (should) be the filename plus the "_trig" suffix after it. But the name is literally defined in the custom_ambient_trig.sndshd file I linked to above, so use that name (e.g., in case there's a typo).
- So some typical properties/values in your speaker_ambient_music entity might be:
snd_station1 alien01_loop_trig
snd_station2 haunted_revenants01a_trig
snd_station3 mansion_piano02a_trig
etc...
(Strange, some of those underscores seem to be disappearing in the code tag.)
- Do this for each snd_station* that you want, and that's all you have to do with the ambient speaker itself. You're done with it. Everything else will be handled by the soundshader and the script. (I hope to add a feature in the future to handle things like looping and volume in the ambient_player, however.) By the way, notice that you do NOT need to create a snd_station0 for the silence channel. That's handled all by the script itself.
- You will probably typically want to use a shader name from custom_ambient_trig.sndshd for the value of your snd_stationX property. But technically you can use other names, e.g., from any soundshader in the sound folder, or even make your own. Just be very sure they will work like you want. I'll discuss more about a Sound Shader at the end of this tutorial to help you if you want to do something like that.
- Command Buttons
- Name each command button (except for one exception below) whatever you want (e,g., the name of the zone it's covering, like: factory_zone, streets_zone, forest, whatever), just remember what it is so you can "target" it with a trigger entity later.
- The one exception is that the first button, which is "station 0", should be reserved for turning all sounds off, that is, radio silence. This is important because it is the initial state of the setup when the game starts, even if you set up a trigger to turn on a station the moment after it starts. (So even though you don't have a "station0" listed in the ambient_player, you still have the button for it to activate silence in the script). Name the button something that lets you know it is for having no ambient sound, such as "silence" or "nosound".
- Add a "state_change_callback" property to each command button, with the value being "station_0", "station_1", "station_2", etc., sequentially for each button. This name needs to be *exactly* like that (including the underscore; and don't forget that the first button is station_0 and is reserved for "no sound").
The Gameworld
- You trigger ambients in the gameworld with a trigger_multiple. To make one, first create a brush the size of the entrance to the zone you want to do (e.g., door-sized, or cave-entrance sized, or street-wide-to-the-sky-sized). It should be quite thick so that they player cannot run through it without it registring a hit.
- Footnote: Roughly speaking, a trigger checks in pulses, and if the player is inside during a pulse there's a hit (at least, I think that's how it works). To get an idea of how quickly the checks pulse, when you have everything set up you can stand inside of one and watch how fast a message like "Station 2 is already on." repeats itself. Try to make it thick enough that a player could not get lucky and run through without that pulse registering him inside it. I'm still experimenting myself with the ideal thickness, 8 occassionally misses if I try, so maybe something like 16 or 24 or 32 just to be safe? (I will update this with a recommended number as I test more.) Just be sure to test your setup a lot, running in and out, to make sure the trigger never fails to start the ambient.
- Note you can also make multiple trigger_multiple's to cover the entrance how you want, e.g., if it's L-shaped or something. It doesn't have to be just one wall. Whatever works. It also doesn't have to be a perfect fit (like a visPortal), just that the player cannot possibly enter the zone without touching a trigger, but it doesn't hurt making it a perfect fit either.
- Texture it with common/trigmulti, then with the brush highlighted in Dark Radiant, right click on it, create-entity, Darkmod/Triggers/Trigger_multiple, ok. If you have others the same size (e.g., door sized) you can copy-paste it for them.
- In the map, set up a trigger_multiple entity at every entrance to an area you want covered by a single omni ambient sound, completely covering the entrance so the player has to pass through it to get in the area, each targeting the command button of the station you want to play in that zone (e.g., property/value "target"/factory_zone, or whatever you named the command button you want to use). (Do not add a "wait" property). When you exit the area, have another trigger_multiple at the entrance of the new area target the command button for a new station.
- Here's an image of that for reference: [1] (One note: After some testing, I think that this trigger is not thick enough, as I described above, but maybe another 8 units thicker or so would do.)
- Be sure you cover every possible entry even those you aren't sure the player can cross, and match every entry-trigger with a mirroring exit-trigger for the adjacent area, and watch for possible busts like teleports (where you can just make sure the teleport process includes activating the command button for the ambient it's teleporting into).
- If you want no sound for an area (i.e., just turn the previous ambient off), have a trigger_multiple turn on the first command button, "station_0", which I recommended you name something like "silence".
- There's one special case. If you want the player to start in an area with an ambient playing, place a trigger right over the area where the player starts, targeting the command button for the sound you want to use (except for the first button, "silence", which would be redundant since the game starts already on "station_0" no sound).
The Script
Once you have all that set up, you need to create a script in something like notepad or wordpad. Remember to name it the name of your map "mapname.script" (and that you don't accidentally name it mapname.script.txt), and put it into the same folder as your map. Just copy and paste the following code into the textfile and save it.
Note that this version covers three stations, but in the last line it's easy to see how to add more.
// ZONED AMBIENT SYSTEM (demagogue)
//
// Description: This script allows a method in which a trigger turns on an omni ambient when you enter a zone, then turns it off when you leave, possibly starting a new ambient for the new zone.
// The set up is described at http://modetwo.net/darkmod/wiki/index.php?title=Ambient_Sounds%2C_a_zone_approach.
float station_state = 0; // the "current radio station playing"
float channel_state = 1; // used to manage separate snd_channels so the fade-ins/fade-outs can blend together
float buttoncall; // which button triggered the script, by radio station number. It could be named "scriptcall" too.
// If other entities are going to trigger the script, like an idLocation check or whatever,
// it could use this variable in the same way to getKey all the values associated with the zone it's covering.
// values the mapper enters into ambient_player spawnargs to control the incoming-outgoing ambient transition
float fadeOutDelay, fadeOutDuration, fadeOutVolume, fadeInDelay, fadeInDuration, fadeInVolume;
float channel1, channel2; // the string names of the snd_channels used with channel_state
string buttoncallstr; // The string version of buttoncall
string newambient, newambientname; // The spawnarg & key for the soundshader of the ambient you want started when you enter a new zone
void main()
{
}
void ambientplayer()
{
// This swaps channels 1 & 2 every trigger-hit to allow for the ambient_player entity
// to blend an outgoing ambient fade-out with an incoming ambient fade-in both playing on separate channels
// of the same entity.
// E.g., Ambient-1 is on Ch1-BODY, trigger, Ch1-BODY fades-out as Ch2-BODY2 fades-in with Ambient-2,
// Ch1 & Ch2 swap so now it's Ch1-BODY2 and Ch2-BODY. Ambient-2 is playing on BODY2.
// Turn around and walk back out, trigger, Ch1-BODY2 now fades-out as Ch2-BODY fades-in (with Ambient-1).
// Ch1 & Ch2 swap again so BODY2 is on ch2 again, and BODY is on Ch1 again.
// Rinse and repeat.
if (channel_state == 1)
{
channel1 = SND_CHANNEL_BODY;
channel2 = SND_CHANNEL_BODY2;
channel_state = 2;
}
else
{
channel1 = SND_CHANNEL_BODY2;
channel2 = SND_CHANNEL_BODY;
channel_state = 1;
}
// This pulls all the needed values for an ambient transition from ambient_player spawnargs
// associated with the button (ie, zone trigger) which started the process.
fadeOutDelay = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_fodelay" );
fadeOutDuration = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_foduration" );
fadeOutVolume = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_fovolume" );
fadeInDelay = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_fidelay" );
fadeInDuration = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_fiduration" );
fadeInVolume = $ambient_player.getFloatKey( "snd_st" + buttoncallstr + "_volume" );
newambient = ( "snd_st" + buttoncallstr );
newambientname = $ambient_player.getKey( "snd_st" + buttoncallstr );
// sys.print( "Old Channel " + channel1 + ", FO Delay: " + fadeOutDelay + ", FO Duration: " + fadeOutDuration + "\n" );
// sys.print( "Starting Ambient " + newambient + ", FI Delay " + fadeInDelay + ", FI Duration " + fadeInDuration + ", volume " + fadeInVolume + ", on channel " + channel2 + "\n" );
if (station_state != buttoncall) // if any other station than the current one is started
{
station_state = buttoncall;
sys.print( "Radio Station " + buttoncallstr + " is now playing.\n");
// $ambient_player.startSound( newambient, SND_CHANNEL_BODY, false); // This is for easy testing if any sound
// sys.print( "Playing Ambient " + newambient + "\n" ); // at all will play.
sys.wait( fadeOutDelay ); // the mapper can delay the start of the old ambient fade out
$ambient_player.fadeSound( channel1, fadeOutVolume, fadeOutDuration ); // begins fade-out of the old ambient on channel 1
$ambient_player.startSound( newambient, channel2, false ); // starts the new ambient on channel 2
$ambient_player.fadeSound( channel2, -60, .001 ); // immediately fades-out new ambient on channel 2 to prime for fade-in
sys.wait( .01 + fadeInDelay ); // gives time for the fade-out to work, plus if the mapper wants to delay the timing of the fade in
$ambient_player.fadeSound( channel2, fadeInVolume, fadeInDuration ); // begins fade-in of new ambient on channel 2
sys.print( "Playing Ambient " + newambientname + " on " + newambient + "\n" );
}
else
{
sys.print( "Radio Station " + buttoncallstr + " is already playing.\n");
}
}
// The following threads simply give the ambientplayer thread the new station number
// when called, for a given number of stations.
void station_0(entity button, boolean bOpen, boolean bLocked, boolean bInterrupted) // Reserved for nosound
{
if(bOpen)
{
button.Close();
buttoncall = 0;
buttoncallstr = "0";
sys.threadname( "killme" );
thread ambientplayer();
sys.killthread( "killme" );
}
}
void station_1(entity button, boolean bOpen, boolean bLocked, boolean bInterrupted)
{
if(bOpen)
{
button.Close();
buttoncall = 1;
buttoncallstr = "1";
sys.threadname( "killme" );
thread ambientplayer();
sys.killthread( "killme" );
}
}
void station_2(entity button, boolean bOpen, boolean bLocked, boolean bInterrupted)
{
if(bOpen)
{
button.Close();
buttoncall = 2;
buttoncallstr = "2";
sys.threadname( "killme" );
thread ambientplayer();
sys.killthread( "killme" );
}
}
void station_3(entity button, boolean bOpen, boolean bLocked, boolean bInterrupted)
{
if(bOpen)
{
button.Close();
buttoncall = 3;
buttoncallstr = "3";
sys.threadname( "killme" );
thread ambientplayer();
sys.killthread( "killme" );
}
}
// The buttons should continue like this for as many "stations" as you want in your map.
In a future edition I hope to explain all the parts of this script in more detail here.
Tips and Issues
General Tips
- It probably goes without saying, but be sure to test your set up by running back and forth across two zones over and over, to make sure the ambients turn on and off when they're supposed to and it doesn't break or the player gets through without it triggering.
- If there is a problem, you can put the StartingPoint in the blueroom to push command buttons and make sure the sounds turn on, and replace each other as they should, and that the first button stops all sound.
- Some typical problems might be, if the buttons work but the triggers don't, make sure the trigger spells the name of the button correctly in its "target" property. Also make sure you spell the buttons' "state_change_callback" value correctly.
- The lines sys.print("Radio station X is now/already playing."); are there so you can go into the console and see exactly when and how the triggers are working. If you don't see that line in the console when you touch a trigger, it means the script isn't even being called on, so check things like the trigger's target spells the command button's name correctly, and the command button spells the state_change_callback value properly. Once you have it set up and working, you can add "//" in front of the sys.print lines to turn them off.
Custom Soundshaders
- As I mentioned above, I've created an soundshader for general use with this method you can download from the link above (or here: Custom_ambient_trig). But you may want to change some of its instructions or add custom ambients, so I'll describe a little about soundshaders here. You can browse some soundshaders in the Darkmod/sound folder to get an idea of how they look.
- A typical entry looks like this:
alien01_loop_trig
{
description "Made by Muze"
global
omnidirectional
looping
sound/ambient/ambience/alien01_loop.ogg
}
- For reference, the functions in the soundshader are:
- Shader name. The first line is the name of the shader that you will use as your snd_station* values, as you can tell by looking at the example property/value I gave above.
- "global" means it will be heard everywhere in the map.
- "omnidirectional" means it will be heard from all directions (i.e., not from the direction of the speaker itself).
- "looping" means when the ambient is finished, it will start over (not always appropriate for every ambient).
- Sound address. And the final line is the address of the actual sound file played. As you can see, you can listen to all the Darkmod ambients in the folder "sound/ambient/ambience" to help you choose one (if you have a soundplayer that can play .ogg's).
- If you want to modify some of these properties for your own purposes (e.g., deleting the looping, or adjusting the volume), or add a custom ambient sound, you can easily make a new soundshader by making a new text file with the file extension .sndshd and add an entry using the above example as a template, with your fan mission's name (or a simplified version) in the title, such as patent_ambient.sndshd (what I use; make sure you don't accidentally save it something like YourMapName_ambient.sndshd.txt). Be sure that if you do create your own ambient entry in a soundshader that you use a different name than one already used, e.g., if I modified the alien01_loop_trig entry and put the new entry in my own custom soundshader, I would name it alien01_loop_patent so it doesn't overlap. I will tell where to put it in the next section.
- DON'T just modify the soundshader I made, or any tdm_* soundshader, and re-save it because you need to be in the habit of having all your custom sound-set ups in your own soundshader unique to your own fan mission. There are lots of tdm_* soundshaders that you don't want to alter, but you might use their sounds in your own way by making new entries in your own soundshader with the same sounds.
- A typical modification would be the volume. To change the volume, add a new line "volume" with a number (positive or negative), such as "volume 10" or "volume -10". You can test the different volumes by changing your custom soundshader, then starting in the blueroom pushing command buttons to listen to it in-game.
Custom Ambient Sounds
- If you wanted to use a custom ambient, and note it has to be in either .ogg or .wav format (see the wiki entry on sound formats for more info), just make a custom soundshader with an entry for it using the template above, with its own unique name, and the address pointing to where your custom soundfile will be when packaged for release.
- When you're packaging your mission in a .pk4 file (just a .zip file renamed .pk4), create a folder named "sound", and inside that folder create another folder with your FM's name or an abbreviated form, e.g., in my case "patent". Put the sound file inside that second folder. (During testing, when you are running the game through the console, you can temporarily put it inside darkmod/sound/YourFMname.)
- The soundshader should be placed in the "sound" folder. (Again, during testing, when you are running the game through the console, you can temporarily put it inside darkmod/sound.)
- So for one of my custom ambients called "ambient_frozen.ogg", my soundshader, called "patently_dangerous.sndshd" has a line that says "sound/patent/ambient_frozen.ogg". Like this:
ambient_frozen
{
description "Made by alacazam"
global
omnidirectional
looping
volume -10
sound/patent/ambient_frozen.ogg
}
- The ambient_frozen.ogg file itself is just where it says, "patently_dangerous.pk4/sound/patent/ambient_frozen.ogg".
- And the soundshader is in "patently_dangerous.pk4/sound/patently_dangerous.sndshd".
- Once you modify or have a custom sound in your own soundshader, make sure you use sound's name in the soundshader for the value in the ambient_player entity to use it (or any other speaker with the property s_shader, for that matter).
- In my case, the "ambient_player" entity in my map has the spawnarg/value of "snd_station3" / "ambient_frozen".
Ambient Fades and Blends Using Speakers
- Using speakers in combination with the triggers was the old method I thought of for doing fades and blends. It may be obsoleted now by the new script, but there might be a situation you want to use it. So below is more detail on that for the record.
- To start, instead of shaping the trigger_multiples like 2 walls on each side of the entrance, you can shape them like a box (three walls around the entrance, with an inside layer, for nosound, and an outside layer, for your ambient, so 6 brushes altogether), or even easier, and probably just as effective in many cases, just back the trigger-wall up a ways from the entrance, and in either setup place a normal speaker inside the middle area ("waitfortrigger"/0, "global"/0, "looping"/1, "omni"/1, "s_mindistance" & "s_maxdistance" set appropriately, remember there is a button in Dark Radiant to see the speaker radius).
- Here's an image of that for reference: [2]
- It just uses 2 walls (four brushes), with the third being a brick wall.
- The speaker will fade the ambient in, the trigger will turn on the ambient, and as the speaker fades out the ambient continues. One issue, the ambient sound will duplicate in the overlap between speaker and trigger-on. Depending on the ambient that may sound strange. You may adjust the speaker radius and trigger locations to minimize that, though (better than I did for my screenshot above). But for other ambients you won't notice and it works great.
- In testing this, I found it good to have the ambient trigger be the outside layer and a "nosound" trigger (i.e., station0) the inside, the speaker radius does the fade in the nosound area in the middle. Then if you wanted the area on the other side of that door to have its own ambient, you'd create another 3 walls on the other side of that doorway with the ambient trigger an outside layer and a "nosound" trigger an inside layer, and its own speaker fading in its middle (basically just mirroring the screenshot I took for the other side of the doorway). Then the two speakers could even overlap in the middle so you get a nice blending gradient between the two ambients.
- (Also when testing this, I had some problems creating three walls as one trigger_multiple, which is possible but it didn't trigger, at least not in my testing, and had better luck creating three separate trigger_multiples, each as a single wall-brush. I don't know if that's a real problem or just a quirk I ran into and actually you can use multiple brushes as one trigger_multiple. It's just something to keep in mind).