- Joined
- Mar 19, 2009
- Messages
- 444
I/ Introduction
Abstract: a member of a board was not able to hear the sound he wants for a unit (turret of a Tower Defence) when the turret attacks an other unit (a creeps).
He wants the attack sound of the Marine Unit (same sound than the attack 1 of the gyrocopter) to be played, in order to get the sound of a machinegun.
Tutorial objbective : propose several ways to play sounds for players through the example of the unit attack. I will then follow the things I did to help the map maker, with strengths and weaknesses of each way.
There is no general requirement to read this tutorial, but I will add those requirements depending on the way used.
Note : once again, the example shows how to hear a machinegun sound during the attack of a creeps by a tower. But the different ways do work for any kind of 3D sound.
Note 2 : in order to simplify this tutorial, we will take a more precise example : the tower will use the model of the Circle of Power and the missile will use the model of the attack 1 of the gyrocopter.
The Tower:
Unit/Neutral/Campaign/Buildings/Power of Circle
Integer ID in the editor: ‘ncop’
Path in the MPQ:
Buildings\Other\CircleOfPower\CircleOfPower.mdx
The missile:
Units-Missiles/Gyrocopter (the first one)
Path in the MPQ:
Abilities\Weapons\GyroCopter\GyroCopterImpact.mdx
You can do the test, if you have a tower with this model by giving it as an attack the missile, you will have the visual effect, but no sound.
Now, let’s see how we will hear the sound which seems not to exist (in the tower as well as in the missile) through several ways.
II/ Modify the model of the unit
Requirements
The tool Warcraft 3 Model Editor
Tips
Read the tutorial by Mechanical Man
Okay, let’s start by opening Warcraft III Model Editor. I hope you can do this.

Do not loose time : the goal is to find the model of the circle of power.
Just go click on Window/MPQ Browser. This menu allows to check any file in the MPQ archive of Warcraft III

We need the good MPQ. I will help you: we will find our model in “War 3” (MPQ of Reign of Chaos). Then, we need to find our model, which is there: Buildings\Other\CircleOfPower\CircleOfPower.mdx

Once the model is opened, you can register it somewhere by clicking on File/ Save as like in any tool.

Let’s start the serious things.
In a Warcraft III model, we can find several properties called « Nodes ». A Nodes can be a cloud of points (bones), a particle emitter, a sound, etc.
We will so open the W3 Model Editor module called “Node Manager”.

You should see the same window than in the screen print.
There are several objects called “Plane” with a bone symbole. Those are the Bones; generally they are used to group the points called “Vertex” in cloud of points, making the design of the model.
Omni01 is a light: it will create a light source on the model.
Dummy01, with an exclamation mark, is called a “Helper”; we can consider it as a Bone, and we still do not care of it at the moment.
Sprite Rallye Point Ref, with a paperclip, its an attachement. If you cast a spell with Infography-Target set to the attachement Sprite Rallye Point Ref, the effect of the spell will be created on this part of the model. But it is still not what we need in order to attach a sound.
The box called Collision is not what we need.
But where the hell is the sound of the unit ?
The answer is: the unit has no sound. We will then have to create the sound.
Pros
The sound is integrated in the model, so it is played for all units using this model on the map, in real time.
Cons
If you do not know how to edit a model, you will need to learn how to create Event Objects via Warcraft III Model Editor for example. More, it requires to import models on the map, and then to raise it size.
III/ Modify the model of the missile
You need to add an Event Object on the model of the missile you want to use. Try with this model http://communots.free.fr/upload/GyroCopterImpact12650250.mdx
Pros
The sound is integrated in the model, so it is played for all units using this model on the map, in real time.
Contrary to the importation of a complete unit, the models of projectiles raise generally less the size of the map. (Since a unit is around 80ko and a missile 5-10 ko average).
Cons
If you do not know how to edit a model, you will need to learn how to create Event Objects via Warcraft III Model Editor for example.
IV/ Replace an existing sound for an existing model
You already use a model which uses a sound, and you replace this sound used by the projectile by the sound you want.
So, you have to import your new sound and give it the path of the raw sound used by checking, in the import manager, “custom path”. Then, just copy-past the path of the original sound.
Pros
Quite simple to do : you do not need to modify any model and to import them.
Cons
The size of the imported sounds is generally quite high for a useless result. It forces you to find a missile existing in the game for which you do not care about the sound and that you will not use later. For example, if you use Holy Bolt, each time this model will be used, he will get the sound you have imported (and in our example, a machinegun sound).
More, you will have to find the exact path of the sound you want to replace by importing a new sound.
V/ Play the sound with a GUI trigger
You can play a sound by detecting the attack of the tower. You are supposed to be able to code in GUI, so the only solution proposed in GUI is:
-
Untitled Trigger 001
-
Events
-
Unit - A unit Is attacked
-
-
Conditions
-
(Unit-type of (Attacking unit)) Equal to Goblin Sulfate-Machine
-
-
Actions
-
Sound - Play Sulfateuse <gen> at 100.00% volume, attached to (Attacking unit)
-
-
-
[/Gui]
The lightest method : nothing to modify (model, importation), very short trigger.
Cons
Warcraft III is not really good to manage sounds. Then, by using those functions, you can only have 1 3D sound of the same kind used at the same instant. What it means, is that if you get 30 turrets firing missiles with an attack rate at 0.05 second, most of the machinegun sounds will not be played.
More, the sound is played only when the target of the tower is attacked. So if you use slow missiles and large distances, the sound of the gun will be played on the missile impact. Hearing a bullet launch 4 seconds after it is really launched by the tower is quite weird.
VI/ Play the sound with a VJass Trigger
There, you need to learn the Jass, the Vjass, and to use the library SoundsUtils.
It requires the JassNewGenPack, the library Stack and the library TimerUtils.
To set up a library: create a trigger. Select it, click on Edition/Convert into custom text. Delete the content of the converted trigger. Copy-past the code of the library.
Copy SoundUtils
JASS:
library SoundUtils requires Stack, TimerUtils
//******************************************************************************
//* BY: Rising_Dusk
//*
//* Sounds are a very picky datatype in WC3. They have many quirks that one must
//* account for in order to use them, and simply using the internal WE Sound
//* Editor isn't enough because the sounds it makes can't be played multiple
//* times at once. 3-D sounds are also very tricky because there are different
//* WC3 sound options that a user can have activated where certain sounds will
//* or will not work. This library attempts to streamline the handling of sounds
//* so that it is less likely to confuse you or cause problems.
//*
//* The .mp3 format can be used for 3-D sounds, but there is one problem that
//* must be noted. If your computer supports the "Dolby Surround" sound option
//* in WC3 and you have it selected, then .mp3 files will work for 3-D sounds.
//* If you don't, however, they may not work depending on what you do have
//* selected and what is available for your computer. The .wav format works on
//* all possible settings, making them excellent for general use. This library
//* can interface with sounds of either type.
//*
//* Known issues with sounds that this library resolves:
//* - A given sound variable can only be played once at a time. In order to
//* play a sound type multiple times at once, you need multiple variables.
//* - A sound cannot be played at the same instant that it is created.
//*
//* The DefineSound function defines a sound type based on some basic parameters
//* the user provides. DefineSoundEx is available if the user wants control over
//* all possible parameters, though they won't have an impact most of the time.
//* The duration parameter for DefineSound and DefineSoundEx is in milliseconds,
//* which is consistent with Blizzard's natives. To get the duration of a given
//* sound, open up the WE's Sound Editor, navigate to your sound, and select
//* "Add as Sound." In doing so, it will show its duration in seconds. Multiply
//* that number by 1000 and use it as the duration argument.
//*
//* This library returns a sound variable with RunSound that you can change the
//* settings of using the standard JASS sound API. The library assigns default
//* values to the parameters for 2-D and 3-D sounds, that way they will run
//* without any further help.
//*
//* The library automatically allocates, runs, and recycles a sound when you
//* call RunSound. This library will not automatically recycle looping sounds,
//* so you will need to call ReleaseSound on the looping sound when you want it
//* to end.
//*
//******************************************************************************
//*
//* > function DefineSound takes string fileName, integer duration, ...
//* boolean looping, boolean is3D returns integer
//*
//* This function defines a sound type with a short list of parameters. The
//* returned integer serves as a SOUND_TYPE for running this type of sound at
//* any other point in a map.
//*
//* > function DefineSoundEx takes string fileName, integer duration, ...
//* boolean looping, boolean is3D, boolean stopwhenoutofrange, ...
//* integer fadeInRate, integer fadeOutRate, string eaxSetting ...
//* returns integer
//*
//* This function serves an identical purpose to DefineSound, but gives the user
//* full control over the entire list of parameters. Similar to DefineSound, the
//* returned integer serves as a SOUND_TYPE for running this type of sound.
//*
//* > function RunSound takes integer soundRef returns sound
//*
//* This function runs a sound with the parameters held within the soundRef
//* integer argument. The soundRef argument is the returned value of DefineSound
//* or DefineSoundEx.
//*
//* > function RunSoundOnUnit takes integer soundRef, unit whichUnit returns sound
//*
//* The same as RunSound, just this function runs a sound of a given type on a
//* specified unit.
//*
//* > function RunSoundAtPoint takes integer soundRef, real x, real y, real z returns sound
//*
//* The same as RunSound, just this function runs a sound of a given type at a
//* specified point in 3D space.
//*
//* > function RunSoundForPlayer takes integer soundRef, player p returns sound
//*
//* The same as RunSound, just this function runs a sound of a given type only
//* for the specified player.
//*
//* > function ReleaseSound takes sound s returns boolean
//*
//* This function need only be called on looping sounds. If a sound is not
//* looping, it will be released and recycled on its own. This function should
//* be used on looping sounds when you want them to end.
//*
//* Example usage:
//* set SOUND_TYPE = DefineSound("Sound\\Path.wav", 300, false, true)
//* call RunSound(SOUND_TYPE)
//* call RunSoundOnUnit(SOUND_TYPE, SomeUnit)
//* call RunSoundAtPoint(SOUND_TYPE, x, y, z)
//* call RunSoundForPlayer(SOUND_TYPE, Player(5))
//* call ReleaseSound(SomeLoopingSound)
//*
globals
private hashtable ht = InitHashtable() //Attach sound types to sounds
private hashtable st = InitHashtable() //Sound hashtable
private hashtable rt = InitHashtable() //Attach soundrecyclers to sounds
private hashtable kt = InitHashtable() //Attach StopSound data
endglobals
//Struct for each sound type
private struct soundhelper
//Stack associated to each struct
Stack sta
//Sound Settings for this sound type
string fileName = ""
integer duration = 0
boolean looping = false
boolean is3D = false
boolean stopwhenoutofrange = false
integer fadeInRate = 0
integer fadeOutRate = 0
string eaxSetting = ""
static method create takes string fileName, integer duration, boolean looping, boolean is3D, boolean stopwhenoutofrange, integer fadeInRate, integer fadeOutRate, string eaxSetting returns soundhelper
local soundhelper sh = soundhelper.allocate()
//Load the parameters so the sound can be created later as necessary
set sh.fileName = fileName
set sh.duration = duration
set sh.looping = looping
set sh.is3D = is3D
set sh.stopwhenoutofrange = stopwhenoutofrange
set sh.fadeInRate = fadeInRate
set sh.fadeOutRate = fadeOutRate
set sh.eaxSetting = eaxSetting
//Create the stack for the struct
set sh.sta = Stack.create()
return sh
endmethod
endstruct
//Struct for holding data for the sound recycling
private struct soundrecycler
timer t = null
sound s = null
integer sh = 0
boolean stopped = false //Only gets used if StopSound is called on a new sound
static method create takes sound whichSound, integer soundRef returns soundrecycler
local soundrecycler sr = soundrecycler.allocate()
set sr.t = NewTimer()
set sr.s = whichSound
set sr.sh = soundRef
call SetTimerData(sr.t, integer(sr))
//Hook the value to the soundRef and whichSound
call SaveInteger(rt, soundRef, GetHandleId(whichSound), integer(sr))
return sr
endmethod
private method onDestroy takes nothing returns nothing
call RemoveSavedInteger(rt, .sh, GetHandleId(.s))
call ReleaseTimer(.t)
endmethod
endstruct
//******************************************************************************
private function HookStopSound takes sound soundHandle, boolean killWhenDone, boolean fadeOut returns nothing
local integer id = GetHandleId(soundHandle)
local integer soundRef = 0
local soundrecycler sr = 0
if HaveSavedInteger(ht, 0, id) then //Sound is from stacks
set soundRef = LoadInteger(ht, 0, id)
if HaveSavedInteger(rt, soundRef, id) then //Sound has a recycler
set sr = soundrecycler(LoadInteger(rt, soundRef, id))
set sr.stopped = true
endif
if killWhenDone then
debug call BJDebugMsg(SCOPE_PREFIX+"Warning: (StopSound) Destroying a sound in the stack")
endif
endif
endfunction
hook StopSound HookStopSound
private function HookKillSoundWhenDone takes sound soundHandle returns nothing
if HaveSavedInteger(ht, 0, GetHandleId(soundHandle)) then
call BJDebugMsg(SCOPE_PREFIX+"Warning: (KillSoundWhenDone) Destroying a sound in the stack")
endif
endfunction
debug hook KillSoundWhenDone HookKillSoundWhenDone
//******************************************************************************
function DefineSoundEx takes string fileName, integer duration, boolean looping, boolean is3D, boolean stopwhenoutofrange, integer fadeInRate, integer fadeOutRate, string eaxSetting returns integer
return integer(soundhelper.create(fileName, duration, looping, is3D, stopwhenoutofrange, fadeInRate, fadeOutRate, eaxSetting))
endfunction
function DefineSound takes string fileName, integer duration, boolean looping, boolean is3D returns integer
return DefineSoundEx(fileName, duration, looping, is3D, true, 10, 10, "CombatSoundsEAX")
endfunction
function ReleaseSound takes sound s returns boolean
local integer id = GetHandleId(s)
local integer soundRef = 0
local soundhelper sh = 0
local soundrecycler sr = 0
if s == null then
debug call BJDebugMsg(SCOPE_PREFIX+"Error: Cannot recycle a null sound")
return false
elseif not HaveSavedInteger(ht, 0, id) then
debug call BJDebugMsg(SCOPE_PREFIX+"Error: Cannot recycle a sound not allocated by RunSound")
return false
endif
set soundRef = LoadInteger(ht, 0, id)
set sh = soundhelper(soundRef)
call StopSound(s, false, true) //Stop the sound
call sh.sta.push(id) //Return it to the stack
call SaveSoundHandle(st, soundRef, id, s) //Save it to hashtable
if not sh.looping then
//soundrecycler only exists for non-looping sounds
set sr = soundrecycler(LoadInteger(rt, soundRef, id))
call sr.destroy() //Destroy recycler helper
endif
return true
endfunction
private function Recycle takes nothing returns nothing
local soundrecycler sr = soundrecycler(GetTimerData(GetExpiredTimer()))
local soundhelper sh = soundhelper(sr.sh)
local integer id = GetHandleId(sr.s)
call StopSound(sr.s, false, true) //Stop the sound
call sh.sta.push(id) //Return it to the stack
call SaveSoundHandle(st, integer(sh), id, sr.s) //Save it to hashtable
call sr.destroy() //Destroy recycler helper
endfunction
private function Run takes nothing returns nothing
local soundrecycler sr = soundrecycler(GetTimerData(GetExpiredTimer()))
local soundhelper sh = soundhelper(sr.sh)
if not sr.stopped then
call StartSound(sr.s) //Play sound here
endif
if not sh.looping and not sr.stopped then
call TimerStart(sr.t, sh.duration*0.001, false, function Recycle)
else
call sr.destroy()
endif
endfunction
function RunSound takes integer soundRef returns sound
local sound s = null
local integer i = 0
local soundhelper sh = soundhelper(soundRef)
local soundrecycler sr = 0
if soundRef <= 0 then
debug call BJDebugMsg(SCOPE_PREFIX+"Error: Cannot run sound of undefined type")
return null
endif
//Check if the stack is empty
if sh.sta.peek() == Stack.EMPTY then
//Create a new sound for the stack
set s = CreateSound(sh.fileName, sh.looping, sh.is3D, sh.stopwhenoutofrange, sh.fadeInRate, sh.fadeOutRate, sh.eaxSetting)
//Attach the type to the sound for future reference
call SaveInteger(ht, 0, GetHandleId(s), integer(sh))
call SetSoundDuration(s, sh.duration)
//Stuff that must be performed immediately upon creation of sounds
call SetSoundChannel(s, 5)
call SetSoundVolume(s, 127)
call SetSoundPitch(s, 1.)
if sh.is3D then
//These are settings necessary for 3-D sounds to function properly
//You can change them at will outside of this function
call SetSoundDistances(s, 600., 10000.)
call SetSoundDistanceCutoff(s, 3000.)
call SetSoundConeAngles(s, 0., 0., 127)
call SetSoundConeOrientation(s, 0., 0., 0.)
endif
//Start sound after a delay because it was created here
set sr = soundrecycler.create(s, soundRef)
call TimerStart(sr.t, 0.001, false, function Run)
else
//Allocate a sound from the stack
set i = sh.sta.pop()
if not HaveSavedHandle(st, soundRef, i) then
debug call BJDebugMsg(SCOPE_PREFIX+"Error: No sound in given stack member")
return null
endif
set s = LoadSoundHandle(st, soundRef, i)
call RemoveSavedInteger(st, soundRef, i)
call SetSoundVolume(s, 127) //Start volume at max
//Start it here since it wasn't created here
call StartSound(s)
//Recycle the sound in a timer callback after it's finished if nonlooping
if not sh.looping then
set sr = soundrecycler.create(s, soundRef)
call TimerStart(sr.t, sh.duration*0.001, false, function Recycle)
endif
endif
return s
endfunction
function RunSoundOnUnit takes integer soundRef, unit whichUnit returns sound
local sound s = RunSound(soundRef)
call AttachSoundToUnit(s, whichUnit)
return s
endfunction
function RunSoundAtPoint takes integer soundRef, real x, real y, real z returns sound
local sound s = RunSound(soundRef)
call SetSoundPosition(s, x, y, z)
return s
endfunction
function RunSoundForPlayer takes integer soundRef, player p returns sound
local sound s = RunSound(soundRef)
if GetLocalPlayer() != p then
call SetSoundVolume(s, 0)
else
call SetSoundVolume(s, 127)
endif
return s
endfunction
endlibrary
SoundsUtils requires Stack (idem)
JASS:
library Stack
//*****************************************************************
//* STACK
//*
//* written by: Anitarf
//*
//* This is an efficient implementation of a stack in vJass. Since
//* it is based on a linked list, I decided to add common list
//* methods to the data structure so it can function both as
//* a stack and a simple linked list.
//*
//* As a linked list, it has less functionality than Ammorth's
//* LinkedList, but is considerably faster. Note only that most of
//* the list methods have O(n) time complexity so they may not be
//* suitable for operations on very large lists, however due to
//* their simplicity the list would need to be really large for
//* this to become a problem.
//*
//* All stack methods are of course O(1) and as fast as possible.
//* If you just need a stack, this is definitely the best choice.
//*
//* set s=Stack.create() - Instanceates a new Stack object.
//* call s.destroy() - Destroys the Stack.
//*
//* Stack syntax:
//* call s.push(123) - Pushes the value 123 on the stack.
//* A stack may contain multiple
//* instances of the same value.
//* set i=s.peek() - Reads the top value of the stack
//* and stores it to the variable i.
//* set i=s.pop() - Removes the top value from the stack
//* and stores it to the variable i.
//* s.peek()==Stack.EMPTY - Checks if the stack is empty.
//*
//* List syntax:
//* call s.add(123) - Adds the value 123 to the list.
//* A list may contain multiple
//* instances of the same value.
//* s.size - The total number of values on the list.
//* s.contains(123) - Checks if the value 123 is on the list.
//* set n=s.count(123) - Stores the number of times the value
//* 123 is on the list to the variable n.
//* call s.remove(123) - Removes one instance of the value 123
//* from the list. Returns false if
//* the value was not found on the list.
//* call s.purge(123) - Removes all instances of the value 123
//* from the list. Returns the number of
//* values that were removed.
//* set i=s.first - Reads the first value from the list
//* and stores it to the variable i.
//* set i=s.random - Reads a random value from the list
//* and stores it to the variable i.
//* set s2=s.copy() - Makes a copy of the list and stores
//* it to the variable s2.
//* call s.enum(Func,b) - Calls function Func for all values
//* on the list. The function must follow
//* the Enum function interface.
//* b is a boolean value, if it is true
//* then the values will be enumerated
//* top to bottom, if false then bottom
//* to top.
//*****************************************************************
public function interface Enum takes integer value returns nothing
struct Stack
// Use a totally random number here, the more improbable someone uses it, the better.
// This is the value that is returned by .pop and .peek methods and .first and .random operators when called on an empty stack.
public static constant integer EMPTY=0x28829022
// End of calibration.
readonly integer size = 0
private integer top = 0
private static integer free = 1
private static integer array next
private static integer array value
method push takes integer i returns nothing
// Get an index from the list of free indexes.
local integer n=Stack.free
set Stack.free=Stack.next[n]
// Extend the list of free indexes if needed.
if Stack.free==0 then
set Stack.free=n+1
endif
// Store the value to the index.
set Stack.value[n]=i
// Add index to the top of the stack.
set Stack.next[n]=.top
set .top=n
set .size=.size+1
endmethod
method pop takes nothing returns integer
// Get the top index of stack.
local integer n=.top
// Safety check in case a user pops an empty stack.
if n==0 then
debug call BJDebugMsg("stack warning: .pop called on an empty stack!")
return Stack.EMPTY
endif
// Remove the top index from stack.
set .top=Stack.next[n]
set .size=.size-1
// Add the index to the list of free indexes.
set Stack.next[n]=Stack.free
set Stack.free=n
// Return the value.
return Stack.value[n]
endmethod
method peek takes nothing returns integer
// Read the value of the top index.
return Stack.value[.top]
endmethod
method add takes integer value returns nothing
call .push(value)
endmethod
method contains takes integer value returns boolean
// Get the first index of the list.
local integer i=.top
// Search through the list.
loop
// Stop the search when the end of the list is reached.
exitwhen i==0
// Stop the search if the value is found.
if Stack.value[i]==value then
return true
endif
// Get the next index of the list.
set i=Stack.next[i]
endloop
return false
endmethod
method count takes integer value returns integer
local integer count=0
// Get the first index of the list.
local integer i=.top
// Search through the list.
loop
// Stop the search when the end of the list is reached.
exitwhen i==0
// Increase the count if the value is found.
if Stack.value[i]==value then
set count=count+1
endif
// Get the next index of the list.
set i=Stack.next[i]
endloop
return count
endmethod
method operator first takes nothing returns integer
return .peek()
endmethod
method operator random takes nothing returns integer
local integer r=GetRandomInt(1,.size)
// Get the first index of the list.
local integer i=.top
// Loop through the list.
loop
// Stop the loop after a random amount of repeats.
set r=r-1
exitwhen r==0 or i==0
// Get the next index of the list.
set i=Stack.next[i]
endloop
return Stack.value[i]
endmethod
method remove takes integer value returns boolean
// Get the first index of the list.
local integer i1=.top
local integer i2
// Check if the first index holds the value.
if Stack.value[i1]==value then
call .pop()
return true
endif
// Search through the rest of the list.
loop
set i2=Stack.next[i1]
// Stop the search when the end of the list is reached.
exitwhen i2==0
// Check if the next index holds the value.
if Stack.value[i2]==value then
// Remove the index from the stack.
set Stack.next[i1]=Stack.next[i2]
// Add the removed index to the list of free indexes.
set Stack.next[i2]=Stack.free
set Stack.free=i2
set .size=.size-1
return true
endif
set i1=i2
endloop
return false
endmethod
method purge takes integer value returns integer
local integer count=0
local integer i1
local integer i2
// If the first index holds the value, pop it.
loop
// If the list is empty, return.
if .top==0 then
return count
endif
// Repeat until the first index doesn't hold the value.
exitwhen Stack.value[.top]!=value
call .pop()
set count=count+1
endloop
// Get the first index of the list.
set i1=.top
// Search through the rest of the list.
loop
set i2=Stack.next[i1]
// Stop the search when the end of the list is reached.
exitwhen i2==0
// Check if the next index holds the value.
if Stack.value[i2]==value then
// Remove the index from the stack.
set Stack.next[i1]=Stack.next[i2]
// Add the removed index to the list of free indexes.
set Stack.next[i2]=Stack.free
set Stack.free=i2
set .size=.size-1
set count=count+1
else
set i1=i2
endif
endloop
return count
endmethod
method enum takes Enum f, boolean top2bottom returns nothing
local integer array value
// Get the first index of the list.
local integer i1=.top
local integer i2=0
// Populate the array.
loop
exitwhen i1==0
set value[i2]=Stack.value[i1]
set i2=i2+1
set i1=Stack.next[i1]
endloop
// Call the Enum function for each value in the array.
set i1=i2-1
loop
exitwhen i2==0
set i2=i2-1
// Enumerate in which direction?
if top2bottom then
call f.evaluate(value[i1-i2])
else
call f.evaluate(value[i2])
endif
endloop
endmethod
method copy takes nothing returns Stack
local Stack that=Stack.create()
// Get the first index of the list.
local integer i1=.top
local integer i2
// Add a dummy index to the top of the new list.
call that.push(0)
set i2=that.top
loop
exitwhen i1==0
// Get an index from the list of free indexes and add it at the end of the list.
set Stack.next[i2]=Stack.free
set i2=Stack.next[i2]
set Stack.free=Stack.next[i2]
// Extend the list of free indexes if needed.
if Stack.free==0 then
set Stack.free=i2+1
endif
// Copy the value to the new index.
set Stack.value[i2]=Stack.value[i1]
set i1=Stack.next[i1]
endloop
// End the new list correctly.
set Stack.next[i2]=0
// Remove the dummy index.
call that.pop()
// Copy the size value to the new list.
set that.size=this.size
return that
endmethod
method onDestroy takes nothing returns nothing
local integer n
// Remove all remaining indexes from the stack.
loop
// Get the top index.
set n=.top
exitwhen n==0
// Remove it from the stack.
set .top=Stack.next[n]
// Add it to the list of free indexes.
set Stack.next[n]=Stack.free
set Stack.free=n
endloop
endmethod
static method onInit takes nothing returns nothing
// Store the EMPTY value to index 0 to make .peek inline friendly.
set Stack.value[0]=Stack.EMPTY
endmethod
endstruct
endlibrary
And finally, you need TimerUtils.
JASS:
library TimerUtils initializer init
//*********************************************************************
//* TimerUtils (red+blue+orange flavors for 1.24b+)
//* ----------
//*
//* To implement it , create a custom text trigger called TimerUtils
//* and paste the contents of this script there.
//*
//* To copy from a map to another, copy the trigger holding this
//* library to your map.
//*
//* (requires vJass) More scripts: htt://www.wc3c.net
//*
//* For your timer needs:
//* * Attaching
//* * Recycling (with double-free protection)
//*
//* set t=NewTimer() : Get a timer (alternative to CreateTimer)
//* ReleaseTimer(t) : Relese a timer (alt to DestroyTimer)
//* SetTimerData(t,2) : Attach value 2 to timer
//* GetTimerData(t) : Get the timer's value.
//* You can assume a timer's value is 0
//* after NewTimer.
//*
//* Multi-flavor:
//* Set USE_HASH_TABLE to true if you don't want to complicate your life.
//*
//* If you like speed and giberish try learning about the other flavors.
//*
//********************************************************************
//================================================================
globals
//How to tweak timer utils:
// USE_HASH_TABLE = true (new blue)
// * SAFEST
// * SLOWEST (though hash tables are kind of fast)
//
// USE_HASH_TABLE = false, USE_FLEXIBLE_OFFSET = true (orange)
// * kinda safe (except there is a limit in the number of timers)
// * ALMOST FAST
//
// USE_HASH_TABLE = false, USE_FLEXIBLE_OFFSET = false (red)
// * THE FASTEST (though is only faster than the previous method
// after using the optimizer on the map)
// * THE LEAST SAFE ( you may have to tweak OFSSET manually for it to
// work)
//
private constant boolean USE_HASH_TABLE = false
private constant boolean USE_FLEXIBLE_OFFSET = false
private constant integer OFFSET = 0x100000
private integer VOFFSET = OFFSET
//Timers to preload at map init:
private constant integer QUANTITY = 256
//Changing this to something big will allow you to keep recycling
// timers even when there are already AN INCREDIBLE AMOUNT of timers in
// the stack. But it will make things far slower so that's probably a bad idea...
private constant integer ARRAY_SIZE = 8190
endglobals
//==================================================================================================
globals
private integer array data[ARRAY_SIZE]
private hashtable ht
endglobals
//It is dependent on jasshelper's recent inlining optimization in order to perform correctly.
function SetTimerData takes timer t, integer value returns nothing
static if(USE_HASH_TABLE) then
// new blue
call SaveInteger(ht,0,GetHandleId(t), value)
elseif (USE_FLEXIBLE_OFFSET) then
// orange
static if (DEBUG_MODE) then
if(GetHandleId(t)-VOFFSET<0) then
call BJDebugMsg("SetTimerData: Wrong handle id, only use SetTimerData on timers created by NewTimer")
endif
endif
set data[GetHandleId(t)-VOFFSET]=value
else
// new red
static if (DEBUG_MODE) then
if(GetHandleId(t)-OFFSET<0) then
call BJDebugMsg("SetTimerData: Wrong handle id, only use SetTimerData on timers created by NewTimer")
endif
endif
set data[GetHandleId(t)-OFFSET]=value
endif
endfunction
function GetTimerData takes timer t returns integer
static if(USE_HASH_TABLE) then
// new blue
return LoadInteger(ht,0,GetHandleId(t) )
elseif (USE_FLEXIBLE_OFFSET) then
// orange
static if (DEBUG_MODE) then
if(GetHandleId(t)-VOFFSET<0) then
call BJDebugMsg("SetTimerData: Wrong handle id, only use SetTimerData on timers created by NewTimer")
endif
endif
return data[GetHandleId(t)-VOFFSET]
else
// new red
static if (DEBUG_MODE) then
if(GetHandleId(t)-OFFSET<0) then
call BJDebugMsg("SetTimerData: Wrong handle id, only use SetTimerData on timers created by NewTimer")
endif
endif
return data[GetHandleId(t)-OFFSET]
endif
endfunction
//==========================================================================================
globals
private timer array tT[ARRAY_SIZE]
private integer tN = 0
private constant integer HELD=0x28829022
//use a totally random number here, the more improbable someone uses it, the better.
endglobals
//==========================================================================================
function NewTimer takes nothing returns timer
if (tN==0) then
//If this happens then the QUANTITY rule has already been broken, try to fix the
// issue, else fail.
debug call BJDebugMsg("NewTimer: Warning, Exceeding TimerUtils_QUANTITY, make sure all timers are getting recycled correctly")
static if( not USE_HASH_TABLE) then
debug call BJDebugMsg("In case of errors, please increase it accordingly, or set TimerUtils_USE_HASH_TABLE to true")
set tT[0]=CreateTimer()
static if( USE_FLEXIBLE_OFFSET) then
if (GetHandleId(tT[0])-VOFFSET<0) or (GetHandleId(tT[0])-VOFFSET>=ARRAY_SIZE) then
//all right, couldn't fix it
call BJDebugMsg("NewTimer: Unable to allocate a timer, you should probably set TimerUtils_USE_HASH_TABLE to true or fix timer leaks.")
return null
endif
else
if (GetHandleId(tT[0])-OFFSET<0) or (GetHandleId(tT[0])-OFFSET>=ARRAY_SIZE) then
//all right, couldn't fix it
call BJDebugMsg("NewTimer: Unable to allocate a timer, you should probably set TimerUtils_USE_HASH_TABLE to true or fix timer leaks.")
return null
endif
endif
endif
else
set tN=tN-1
endif
call SetTimerData(tT[tN],0)
return tT[tN]
endfunction
//==========================================================================================
function ReleaseTimer takes timer t returns nothing
if(t==null) then
debug call BJDebugMsg("Warning: attempt to release a null timer")
return
endif
if (tN==ARRAY_SIZE) then
debug call BJDebugMsg("Warning: Timer stack is full, destroying timer!!")
//stack is full, the map already has much more troubles than the chance of bug
call DestroyTimer(t)
else
call PauseTimer(t)
if(GetTimerData(t)==HELD) then
debug call BJDebugMsg("Warning: ReleaseTimer: Double free!")
return
endif
call SetTimerData(t,HELD)
set tT[tN]=t
set tN=tN+1
endif
endfunction
private function init takes nothing returns nothing
local integer i=0
local integer o=-1
local boolean oops = false
static if( USE_HASH_TABLE ) then
set ht = InitHashtable()
loop
exitwhen(i==QUANTITY)
set tT[i]=CreateTimer()
call SetTimerData(tT[i], HELD)
set i=i+1
endloop
set tN = QUANTITY
else
loop
set i=0
loop
exitwhen (i==QUANTITY)
set tT[i] = CreateTimer()
if(i==0) then
set VOFFSET = GetHandleId(tT[i])
static if(USE_FLEXIBLE_OFFSET) then
set o=VOFFSET
else
set o=OFFSET
endif
endif
if (GetHandleId(tT[i])-o>=ARRAY_SIZE) then
exitwhen true
endif
if (GetHandleId(tT[i])-o>=0) then
set i=i+1
endif
endloop
set tN = i
exitwhen(tN == QUANTITY)
set oops = true
exitwhen not USE_FLEXIBLE_OFFSET
debug call BJDebugMsg("TimerUtils_init: Failed a initialization attempt, will try again")
endloop
if(oops) then
static if ( USE_FLEXIBLE_OFFSET) then
debug call BJDebugMsg("The problem has been fixed.")
//If this message doesn't appear then there is so much
//handle id fragmentation that it was impossible to preload
//so many timers and the thread crashed! Therefore this
//debug message is useful.
elseif(DEBUG_MODE) then
call BJDebugMsg("There were problems and the new timer limit is "+I2S(i))
call BJDebugMsg("This is a rare ocurrence, if the timer limit is too low:")
call BJDebugMsg("a) Change USE_FLEXIBLE_OFFSET to true (reduces performance a little)")
call BJDebugMsg("b) or try changing OFFSET to "+I2S(VOFFSET) )
endif
endif
endif
endfunction
endlibrary
Okay, for people hating Jass and Vjass, do not be feared. You will not need to modify any line of the code of those libraries. What do you need is only to be able to use a sound. The idea is like the method with GUI trigger, excepted that you will use SoundsUtils. There is the way to use it.
You need a blank trigger made like this :
JASS:
scope MySoundsVariables initializer init
globals
integer SOUND_MACHINEGUN //The idea of your sound ; you can create as much sounds as you want
endglobals
public function init takes nothing returns nothing
set SOUND_MACHINEGUN = DefineSound("Units\\Human\\GyroCopter\\GyrocopterImpactHit1.wav",750, false, true)
//During the map loading, the sound will be initialized ; do not forget that the twin slash is compulsory for the path of an object in Jass (contrary to GUI, where 1 slash is enough).
//To sum up, you will get something like this
//Set the SOUND_MACHINEGUN to be a Define Sound (blabla)
//DefineSound wants the parameters :
//Custom Path of the sound
//Sound duration in miliseconds
//Sound to play a loop yes/no =>for you, never, the sound will be played one time per attack
// 3D Sound yes/ no : for you, it will be yes : the sound will be played for a player only if the camera is over the turret.
//To translate set SOUND_MACHINEGUN = DefineSound("Units\Human\GyroCopter\GyrocopterImpactHit1.wav",750, false, true)
//it is: set SOUND_MACHINEGUN like a define sound to play the file path "Units\Human\GyroCopter\GyrocopterImpactHit1.wav" during 750 miliseconds, not in loop, in 3D.
// You create as much different sounds as you want, but you have to set them like our example.
endfunction
endscope
Note: A scope is like a basic jass trigger, but with Vjass format.
Note: globals are like the Gui variables, but in Vjass.
And, exactly like for the “Gui method”, when the unit attack, the sound is played. There, I wrote as a coundition “Gun Tower”, but you can change it by the tower you want.
-
Untitled Trigger 001
-
Events
-
Unit - A unit Is attacked
-
-
Conditions
-
(Unit-type of (Attacking unit)) Equal to Tour canon
-
-
Actions
-
Custom script: call RunSoundOnUnit(SOUND_MACHINEGUN, GetAttacker() )
-
-
Pros
Contrary to the Gui Method, you can play any sound at the same time: SoundsUtils automatically create variables for any played sound in order to dodge the limits of the basic Warcraft III functions. Whereas in GUI, the same sound can only be played one time at the same instant.
It also has the same pros than Gui: nothing to import.
Cons
Okay, Vjass can fear people. Then, we use TimerUtils, which requires a bit more CPU usage than model-modification methods.
Same things than GUI: the sound is played only on projectile impact, and you need to find the path of your sound (path that you can check in the sound editor or in the import manager if it is not a raw sound of Warcraft III. More, you need to check the sound duration.
VII/ Conclusion
In a map where they are a lot of units firing with a high attack rate, there is no miracle. For my map Restricted Complex 601 I tested all methods and… Nothing is a “better solution”.
I first tried the model modification by adding Event Object in missile projectiles. But even with only 10 units firing every 0.4 seconds, most of the bullets sounds were not played, since the W3 engine can not deal with too much sounds at the same time.
Use SoundsUtils does not change anything to this raw limit. That’s why, in my own case, I just deleted the sounds of the missiles for most weapons, since they were not hearable for most units (too much sounds at the same time).
I hope this tutorial will help you. Of course, the example of the missile can be applied to any sound of this game: special effect, unit etc…
Attachments
Last edited: