Offender 2 – Programming A Composer

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″]

(Download source here!)

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″]

(Download source here!)

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);