• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Using sounds: the example of projectiles

Level 21
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.

89820d1284974008-using-sounds-example-projectiles-sanstitre12650449.jpg


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

89821d1284974008-using-sounds-example-projectiles-sanstitre12650451.jpg


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

89822d1284974008-using-sounds-example-projectiles-sanstitre12650453.jpg


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

89823d1284974008-using-sounds-example-projectiles-sanstitre12650454.jpg

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”.


89824d1284974008-using-sounds-example-projectiles-sanstitre12650456.jpg


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

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

  • Sanstitre12650449.jpg
    Sanstitre12650449.jpg
    24 KB · Views: 3,082
  • Sanstitre12650451.jpg
    Sanstitre12650451.jpg
    11.6 KB · Views: 2,924
  • Sanstitre12650453.jpg
    Sanstitre12650453.jpg
    28.5 KB · Views: 2,836
  • Sanstitre12650454.jpg
    Sanstitre12650454.jpg
    58.3 KB · Views: 2,811
  • Sanstitre12650456.jpg
    Sanstitre12650456.jpg
    44.4 KB · Views: 2,926
Last edited:
Top