Every time I program the soundtrack for one of my games I get inspired with how I could perfect the engine for music.
I know that a long term goal will be to program a composer. It’s my personal creative crusade. :) I want to create something that, with some degree of intelligence, really “makes” music while you play.
Although realizing that goal is still a ways off, I’m very proud of my current achievement.
Offender 2 development has covered a lot of ground since my last blog post update.
One of the advantages of creating your own music is that you can get really creative with how the engine will handle playing it. You can get as detailed as you want!
I’ve finished pretty much all audio work except for the final touch. In-game bunny dialogue. Given the nature of the game, this is very important.
Choosing the most manageable starting point for it, I began with the boss battle (The Tank).
Here’s an excerpt of it (Give it a listen! You won’t regret it.) :
I decided to start with this portion of the game because it was the most linear, or so it seemed.
I came up with the idea of having the bunnies bicker with eachother while trying to navigate the thing (listen to the above).
Then I decided to break up the dialogue, so that the engine could compile a random “bicker playlist”. Every-time you play against The Tank the conversation would take place in a different order. Appearing to be a new argument.
Here’s an example of it coded:
[swfobj src=”http://nathalielawhead.com/sourcefiles/Offender2/code_examples/Aud_BossTank.swf” height=”586″ width=”460″]
Code:
//test sound events that take presendence over the tank dialogue currently playing //when one is done dialogue is to resume from where it left off var arr_aud_testEvent:Array = new Array(new AUD_GP_Ship_AbductionVoice08(), new AUD_GP_Ship_AbductionVoice09(), new AUD_GP_Ship_AbductionVoice10(), new AUD_GP_Ship_AbductionVoice11(), new AUD_GP_Ship_AbductionVoice12(), new AUD_GP_Ship_AbductionVoice13()); var arr_chan_tankEvent:Array = new Array();//used for all events //dialogue for the boss tank var arr_aud_tankDialogue_default:Array = new Array(new AUD_BossTank_Dialogue_Default01(), new AUD_BossTank_Dialogue_Default02(), new AUD_BossTank_Dialogue_Default03(), new AUD_BossTank_Dialogue_Default04(), new AUD_BossTank_Dialogue_Default05(), new AUD_BossTank_Dialogue_Default06()); var arr_chan_tankDialogue_default:Array = new Array(); var num_aud_tankDialogue_default:Number = 0;//the array element (channel) currently playing var num_aud_tankDialogue_default_position:Number = 0;//the position that it was last on (for pausing and resuming) var trans_aud_tankDialogue:SoundTransform = new SoundTransform(1,0); //randomize the array (thanks stackoverflow!) function shuffleArray( a : *, b : * ){ return ( Math.random() > .5 ) ? 1 : -1; } //The Dialogue //Call this to start function evntInit_snd_BossTank(){ //randomize the array first arr_aud_tankDialogue_default = arr_aud_tankDialogue_default.sort(shuffleArray); num_aud_tankDialogue_default = 0;//set the initial starting array element value num_aud_tankDialogue_default_position = 0;//reset the position trace("The playback order will be: "+arr_aud_tankDialogue_default); evntStart_snd_BossTank(); }; //Start the main track (default) for the boss tank //Play the array starting from 1, and stopping at the end function evntStart_snd_BossTank(){ //start up the first sound arr_chan_tankDialogue_default[num_aud_tankDialogue_default] = Sound(arr_aud_tankDialogue_default[num_aud_tankDialogue_default]).play(num_aud_tankDialogue_default_position,0,trans_aud_tankDialogue); arr_chan_tankDialogue_default[num_aud_tankDialogue_default].addEventListener(Event.SOUND_COMPLETE,evntComplete_snd_BossTank); // trace("Now playing: "+arr_aud_tankDialogue_default[num_aud_tankDialogue_default]+" at position "+num_aud_tankDialogue_default_position+"."); // } function evntComplete_snd_BossTank(event:Event){ trace("Complete, start next."); //remove the listener SoundChannel(event.target).removeEventListener(event.type, evntComplete_snd_BossTank); num_aud_tankDialogue_default_position = 0;//reset the position //set the next one num_aud_tankDialogue_default += 1; //if you've reached the end reset and shuffle (randomize) the array again if (num_aud_tankDialogue_default>arr_aud_tankDialogue_default.length-1){ trace("You reached the end of the dialogue. Re-set, re-shuffle, and re-initiate."); evntInit_snd_BossTank(); }else{ //start! evntStart_snd_BossTank(); } } //resume the main track (default) if another event paused it function evntPause_snd_BossTank(){ //rememver the position it was stopped at num_aud_tankDialogue_default_position = arr_chan_tankDialogue_default[num_aud_tankDialogue_default].position; //now stop and remove all arr_chan_tankDialogue_default[num_aud_tankDialogue_default].stop(); arr_chan_tankDialogue_default[num_aud_tankDialogue_default].removeEventListener(Event.SOUND_COMPLETE,evntComplete_snd_BossTank); // trace(arr_aud_tankDialogue_default[num_aud_tankDialogue_default]+" was stopped at "+num_aud_tankDialogue_default_position+"."); } //The events //call to trigger a new sound event that should pause dialogue //tank receives damage, pukes, etc.. //call via: evntEvent_snd_BossTank(THE_SOUND_ARRAY, THE_ARRAY_ELEMENT); //random: evntEvent_snd_BossTank(THE_SOUND_ARRAY, Math.ceil(Math.random()*THE_SOUND_ARRAY.length)-1); function evntEvent_snd_BossTank(arr:Array, arr_element:Number){ trace("Stop the dialogue, and start the event sound."); //stop the last if(arr_chan_tankEvent.length>0){ arr_chan_tankEvent[0].stop(); } //stop the dialogue evntPause_snd_BossTank(); //start a new one arr_chan_tankEvent[0] = Sound(arr[arr_element]).play(0,0); arr_chan_tankEvent[0].addEventListener(Event.SOUND_COMPLETE, evntEnd_snd_BossTank); } //resume normal dialogue after the event function evntEnd_snd_BossTank(event:Event){ SoundChannel(event.target).removeEventListener(event.type, evntEnd_snd_BossTank); // evntStart_snd_BossTank(); } //Stop ALL boss sounds (clear and close) function evntStopAll_snd_BossTank(){ trace("\nALL SOUNDS STOPPED!\n"); //stop the dialogue if(arr_chan_tankDialogue_default.length!=0){ evntPause_snd_BossTank(); }; //stop the events (if an event has been called) try{ if(arr_chan_tankEvent.length>0){ arr_chan_tankEvent[0].stop(); }; arr_chan_tankEvent[0].removeEventListener(Event.SOUND_COMPLETE, evntEnd_snd_BossTank); }catch(e:Error){ //an event was never initiated } }
It’s a fun, simple, beginning. I really like how it turned out, so I want to have something similar in the main game (bunnies talking with each-other). One of the reasons is that it’s a great delivery platform for comedy. It’s the final touch! The game is going to be packed with humor when I’m finished.
There’s a LOT of sound to manage. I’m taking the liberty to experiment with all sorts of ways to incorporate it. I’m the most proud of the in-game music (example here)… It’s the closest thing yet to “programming a composer”… I love that term. One day I’ll actually do it!
So, to conclude this, I’m posting another source code excerpt. It plays a sound from a sound set (array), and only plays the next when the starting sound is passed a certain point. A bit like ON_COMPLETE… I hope that made sense. I’m super tired. :)
The result is that sounds don’t pile over each-other when multiple events with a sound linked to them take place (like damage, health, etc). They play in manageable intervals.
Here’s an interactive example (button-mash your mouse to test):
[swfobj src=”http://nathalielawhead.com/sourcefiles/Offender2/code_examples/Sound_onlyPlayOnComplete.swf” height=”200″ width=”480″]
import flash.events.MouseEvent; import flash.events.Event; import flash.media.Sound; var arr_snd_ship_damageHit:Array = new Array(new AUD_GP_Ship_DamageHit01(),new AUD_GP_Ship_DamageHit02(),new AUD_GP_Ship_DamageHit03(),new AUD_GP_Ship_DamageHit04(),new AUD_GP_Ship_DamageHit05(),new AUD_GP_Ship_DamageHit06(),new AUD_GP_Ship_DamageHit07()); var arr_chan_ship_damageHit:Array = new Array();//the sound channels for the above //alien damage var arr_snd_ship_damageVoice:Array = new Array(new AUD_GP_Ship_DamageVoice01(), new AUD_GP_Ship_DamageVoice02(),new AUD_GP_Ship_DamageVoice03(),new AUD_GP_Ship_DamageVoice04(),new AUD_GP_Ship_DamageVoice05(),new AUD_GP_Ship_DamageVoice06(),new AUD_GP_Ship_DamageVoice07(),new AUD_GP_Ship_DamageVoice08(),new AUD_GP_Ship_DamageVoice09(),new AUD_GP_Ship_DamageVoice10(),new AUD_GP_Ship_DamageVoice11(),new AUD_GP_Ship_DamageVoice12()); var arr_chan_ship_damageVoice:Array = new Array();//sound channels for the above //A sound is "complete" at a custom point -- (hope the following explanation makes sense, I'm too tired to think :) //randomly play sounds from an array (start one and hold starting the other) based on an interval parameter pased to it //if you want it to play the next sound half way through pass it 2, etc... //only one sound (element [0] of it's sound channel array) may play at a time //once the sound is at its end point (like 2 - half way through) the next one may play //you may also pass it a random interval so they trigger at more random //usage: evnt_snd_startAfterPos(arr_snd_ship_damageVoice, arr_chan_ship_damageVoice, 2, 1); function evnt_snd_startAfterPos(sndArray:Array, chanArray:Array, interval:Number, vol:Number){ var randSnd:Number = Math.ceil(Math.random()*sndArray.length)-1; var sndTrans:SoundTransform = new SoundTransform(vol,0); //if a sound is not already playing then play one if(chanArray.length<=0){ chanArray[0] = Sound(sndArray[randSnd]).play(0,0,sndTrans); addEventListener(Event.ENTER_FRAME, evnt_reset); } //if over interval through then clear the array and start over again function evnt_reset(event:Event){ //trace("Position: "+chanArray[0].position); //trace("Length: "+sndArray[randSnd].length); //if... if(Math.ceil(chanArray[0].position) >= Math.ceil(sndArray[randSnd].length/interval)){ removeEventListener(event.type, evnt_reset); chanArray.pop();//clear the array } } } // function test(event:MouseEvent){ //trigger next half way through evnt_snd_startAfterPos(arr_snd_ship_damageVoice, arr_chan_ship_damageVoice, 2, 1); //trigger next at random evnt_snd_startAfterPos(arr_snd_ship_damageHit, arr_chan_ship_damageHit, 8, Math.random()*1); } stage.addEventListener(MouseEvent.MOUSE_DOWN, test);