[Spell] MUI: Controlling SFX lifetime & best practices for instance cleanup

Level 16
Joined
Feb 22, 2025
Messages
315
1. Manipulating SFX lifetime (not me asking about SFX again 😅):

I’m creating effects(like Illidan footprints) every few steps. I destroy them immediately, but the death animation still plays for several seconds, which looks awkward.
I’ve tried Set Time Scale and color changes, but they don’t work.

  • Is there a way to speed up or shorten SFX death animation in MUI?
  • If not, are there recommended workarounds (e.g., swapping models, simpler particle effects, alternate sfx trails, other techniques used in spells to avoid long death animations)?
  • Any workarounds to achieve the desired effect? ( Couldn't find suitable models in resource section, wow converted sfx models usually have no visuals for death animation so destroying them right away would make them not visible at all.)

I suppose that changing the color of the sfx just plays with the how should i put it; pigmentation of the sfx, not the actual color. Also I hoped that by setting the given sfx to bigger time scale would speed up its death animation, but no, so this leads me to believe that specific sfx is hardcoded, and can't be manipulated by time scale.


2. Cleanup / nullifying variables:

In my loop, I remove units, destroy groups, and clear special effects.

  • Is it good practice to also nullify/reset all related variables/arrays before starting a new instance, or is just cleaning the units/groups sufficient?
  • Since Unit Group and Sfx variables cannot be nullified in GUI, if the action is indeed needed, is it a common practice to nullify them using JASS?

  • FW Loop
    • Events
      • Time - Every 0.03 seconds of game time
    • Conditions
    • Actions
      • -------- Loop through all active instances --------
      • For each (Integer FW_Loop_I) from 1 to FW_MaxIndex, do (Actions)
        • Loop - Actions
          • -------- Unit still exist ? --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • FW_Wolf[FW_Loop_I] Not equal to No unit
            • Then - Actions
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • FW_Phase[FW_Loop_I] Equal to 0
                • Then - Actions
                  • -------- Phase 0: Initial delay --------
                  • Set FW_Delay[FW_Loop_I] = (FW_Delay[FW_Loop_I] - 0.03)
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • FW_Delay[FW_Loop_I] Less than or equal to 0.01
                    • Then - Actions
                      • -------- Delay Finished Start Moving --------
                      • Set FW_Phase[FW_Loop_I] = 1
                      • Unit - Unhide FW_Wolf[FW_Loop_I]
                      • Animation - Change FW_Wolf[FW_Loop_I]'s animation speed to 150.00% of its original speed
                      • Special Effect - Create a special effect attached to the chest of FW_Wolf[FW_Loop_I] using Abilities\Spells\Orc\FeralSpirit\feralspirittarget.mdl
                      • Special Effect - Destroy (Last created special effect)
                      • Special Effect - Create a special effect attached to the origin of FW_Wolf[FW_Loop_I] using Abilities\Weapons\FireBallMissile\FireBallMissile.mdl
                      • Set FW_SFX1[FW_Loop_I] = (Last created special effect)
                    • Else - Actions
                • Else - Actions
                  • -------- Phase 1 Moving --------
                  • Set FW_TempPoint = (Position of FW_Wolf[FW_Loop_I])
                  • -------- // ----- Staggered trail effect (every 3 steps) ----- --------
                  • Set FW_Trail_Counter[FW_Loop_I] = (FW_Trail_Counter[FW_Loop_I] + 1)
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • FW_Trail_Counter[FW_Loop_I] Equal to 3
                    • Then - Actions
                      • Set FW_Trail_Counter[FW_Loop_I] = 0
                      • Set FW_TempReal = (Random real number between -35.00 and 35.00)
                      • Set FW_TempReal2 = (Random real number between -15.00 and 15.00)
                      • Set FW_TempReal3 = (FW_Angle[FW_Loop_I] + 90.00)
                      • Set FW_TempPoint3 = (FW_TempPoint offset by FW_TempReal towards FW_TempReal3 degrees.)
                      • Set FW_TempPoint3 = (FW_TempPoint3 offset by FW_TempReal2 towards FW_Angle[FW_Loop_I] degrees.)
                      • Special Effect - Create a special effect at FW_TempPoint3 using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
                      • Special Effect - Set Time Scale of (Last created special effect) to 5.00
                      • Special Effect - Set Color of (Last created special effect) to r: 255, g: 0, b: 0
                      • Special Effect - Destroy (Last created special effect)
                      • Custom script: call RemoveLocation( udg_FW_TempPoint3)
                    • Else - Actions
                  • -------- Move one step --------
                  • Set FW_TempPoint2 = (FW_TempPoint offset by FW_Step_Speed towards FW_Angle[FW_Loop_I] degrees.)
                  • Unit - Move FW_Wolf[FW_Loop_I] instantly to FW_TempPoint2
                  • Set FW_Distance[FW_Loop_I] = (FW_Distance[FW_Loop_I] + FW_Step_Speed)
                  • -------- // ----- Damage units in a 120 AoE ----- --------
                  • Set FW_Group = (Units within 120.00 of FW_TempPoint2 matching ((((Matching unit) belongs to an enemy of (Owner of FW_Caster[FW_Loop_I]).) Equal to True) and ((((Matching unit) is hidden) Equal to False) and (((Matching unit) is alive) Equal to True))).)
                  • Unit Group - Pick every unit in FW_Group and do (Actions)
                    • Loop - Actions
                      • Set FW_TempUnit = (Picked unit)
                      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                        • If - Conditions
                          • (FW_TempUnit is in FW_DMG_Group[FW_Loop_I].) Equal to False
                        • Then - Actions
                          • Unit Group - Add FW_TempUnit to FW_DMG_Group[FW_Loop_I]
                          • -------- DMG --------
                          • Unit - Cause FW_Caster[FW_Loop_I] to damage FW_TempUnit, dealing 200.00 damage of attack type Spells and damage type Normal
                          • -------- Root Dummy --------
                          • Set FW_TempPoint = (Position of FW_TempUnit)
                          • Unit - Create 1 Root Dummy for (Owner of FW_Caster[FW_Loop_I]) at FW_TempPoint facing Default building facing degrees
                          • Unit - Add a 1.00 second Generic expiration timer to (Last created unit)
                          • Unit - Order (Last created unit) to Night Elf Keeper Of The Grove - Entangling Roots FW_TempUnit
                          • -------- Armor Reduction Dummy --------
                          • Unit - Create 1 Armor Dummy for (Owner of FW_Caster[FW_Loop_I]) at FW_TempPoint facing Default building facing degrees
                          • Unit - Add a 1.00 second Generic expiration timer to (Last created unit)
                          • Unit - Order (Last created unit) to Night Elf Druid Of The Talon - Faerie Fire FW_TempUnit
                          • Special Effect - Create a special effect attached to the chest of FW_TempUnit using Objects\Spawnmodels\Undead\ImpaleTargetDust\ImpaleTargetDust.mdl
                          • Special Effect - Destroy (Last created special effect)
                        • Else - Actions
                  • Custom script: call DestroyGroup( udg_FW_Group)
                  • Custom script: // ----- Check if reached max distance -----
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • FW_Distance[FW_Loop_I] Greater than or equal to FW_Max_Distance
                    • Then - Actions
                      • -------- // ----- Clean up this instance ----- --------
                      • Unit - Remove FW_Wolf[FW_Loop_I] from the game
                      • Special Effect - Destroy FW_SFX1[FW_Loop_I]
                      • Custom script: call DestroyGroup( udg_FW_DMG_Group[udg_FW_Loop_I])
                      • -------- // ----- Swap the last active instance into the freed slot ----- --------
                      • Set FW_Caster[FW_Loop_I] = FW_Caster[FW_MaxIndex]
                      • Set FW_Wolf[FW_Loop_I] = FW_Wolf[FW_MaxIndex]
                      • Set FW_Angle[FW_Loop_I] = FW_Angle[FW_MaxIndex]
                      • Set FW_Distance[FW_Loop_I] = FW_Distance[FW_MaxIndex]
                      • Set FW_Phase[FW_Loop_I] = FW_Phase[FW_MaxIndex]
                      • Set FW_Delay[FW_Loop_I] = FW_Delay[FW_MaxIndex]
                      • Set FW_DMG_Group[FW_Loop_I] = FW_DMG_Group[FW_MaxIndex]
                      • Set FW_SFX1[FW_Loop_I] = FW_SFX1[FW_MaxIndex]
                      • -------- // ----- Clear the last slot??? --------
                      • Set FW_Caster[FW_MaxIndex] = No unit
                      • Set FW_Wolf[FW_MaxIndex] = No unit
                      • Set FW_Angle[FW_MaxIndex] = 0.00
                      • Set FW_Distance[FW_MaxIndex] = 0.00
                      • Set FW_Phase[FW_MaxIndex] = 0
                      • Set FW_Delay[FW_MaxIndex] = 0.00
                      • -------- // ----- Decrease max index and adjust loop counter ----- --------
                      • Set FW_MaxIndex = (FW_MaxIndex - 1)
                      • Set FW_Loop_I = (FW_Loop_I - 1)
                      • -------- Final effect at last know position --------
                      • Set FW_TempPoint = FW_TempPoint2
                      • Special Effect - Create a special effect at FW_TempPoint using Abilities\Spells\Orc\FeralSpirit\feralspirittarget.mdl
                      • Special Effect - Destroy (Last created special effect)
                      • Custom script: call RemoveLocation( udg_FW_TempPoint)
                    • Else - Actions
                  • -------- // ----- Clean up points used in this iteration ----- --------
                  • Custom script: call RemoveLocation( udg_FW_TempPoint)
                  • Custom script: call RemoveLocation( udg_FW_TempPoint2)
            • Else - Actions
      • -------- // ----- Turn off loop if no active instances ----- --------
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • FW_MaxIndex Equal to 0
        • Then - Actions
          • Trigger - Turn off (This trigger)
        • Else - Actions



Thank you for your time,
Regards, Axo
 
I attached a system for managing your Special Effects. It has all sorts of useful features.

See the three examples provided for some insight on how to use it.

For instance, you could use it to move your Illidan footprints out of sight before destroying them:
  • Actions
    • Special Effect - Create a special effect at FW_TempPoint3 using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
    • Set VariableSet SFX_DestroyDelay = 0.50
    • Set VariableSet SFX_Red = 255
    • Set VariableSet SFX_Green = 0
    • Set VariableSet SFX_Blue = 0
    • Set VariableSet SFX_MoveOutBeforeDestroy = true
    • Trigger - Run SES Run <gen> (ignoring conditions)
This will hide the Special Effect after 0.50 seconds. Pair this with the SFX_FadeDuration, SFX_AlphaStart, and SFX_AlphaEnd variables to make the effect fade out over time if you want a smoother transition.

Note that not every Special Effect (or rather Model) will be able to use all of these effect manipulation functions. Attached Special Effects are very strict when it comes to this.

Is it good practice to also nullify/reset all related variables/arrays before starting a new instance, or is just cleaning the units/groups sufficient?
Since Unit Group and Sfx variables cannot be nullified in GUI, if the action is indeed needed, is it a common practice to nullify them using JASS?
You don't have to null anything unless it would otherwise cause a memory leak. Nulling everything would be creating extra work for yourself for a microscopic reduction in memory usage.

To put things into perspective, let's say that the average user has 16 GB of memory.
Now let's say that a Special Effect variable costs 4 bytes of memory.
That means you can have about 4.2 billion Special Effect variables before hitting that limit.

The average user will probably have around let's say 50 Special Effect variables. Not even remotely close.

The reason that Memory leaks are problematic is because they pile up over time. It's that cumulative factor that make them dangerous.
 

Attachments

Last edited:
I attached a system for managing your Special Effects. It has all sorts of useful features.

See the three examples provided for some insight on how to use it.

For instance, you could use it to move your Illidan footprints out of sight before destroying them:
  • Actions
    • Special Effect - Create a special effect at FW_TempPoint3 using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
    • Set VariableSet SFX_DestroyDelay = 0.50
    • Set VariableSet SFX_Red = 255
    • Set VariableSet SFX_Green = 0
    • Set VariableSet SFX_Blue = 0
    • Trigger - Run SES Run <gen> (ignoring conditions)
This will hide the Special Effect after 0.50 seconds. Pair this with the SFX_FadeDuration, SFX_AlphaStart, and SFX_AlphaEnd variables to make the effect fade out over time if you want a smoother transition.

Note that not every Special Effect (or rather Model) will be able to use all of these manipulation functions, but you likely already know that. Attached Special Effects are very strict when it comes to this.


You don't have to null anything that doesn't cause a memory leak. You'd be creating extra work for yourself for a microscopic reduction in memory usage.

To put things into perspective, let's say that the average user has 16 GB of memory.
Now let's say that a Special Effect variable costs 4 bytes of memory.
That means you can have about 4.2 billion Special Effect variables before hitting that limit.

The average user will probably have around let's say 50 Special Effect variables. Not even remotely close.

The reason Memory leaks are problematic is because they pile up over time. It's that cumulative factor that make them dangerous.
Thanks a lot for the detailed explanation and for sharing the system, I really appreciate it.
The fade/delay approach makes sense, I’ll experiment with those variables and see how it fits the trail effect.

Also good to know I don’t need to go overboard with nullifying everything. My brain tends to go into polish the dishes instead of letting them dry mode with these things lol, so that clarification helps a lot.

Tnx for ur time!:cool:2
 
Thanks a lot for the detailed explanation and for sharing the system, I really appreciate it.
The fade/delay approach makes sense, I’ll experiment with those variables and see how it fits the trail effect.

Also good to know I don’t need to go overboard with nullifying everything. My brain tends to go into polish the dishes instead of letting them dry mode with these things lol, so that clarification helps a lot.

Tnx for ur time!:cool:2
No problem, also I forgot this variable in my Illidan example:
  • Set VariableSet SFX_MoveOutBeforeDestroy = true
Without that the system doesn't know to move the sfx out of sight prior to destroying it.

Also, you can copy and paste the system code into ChatGPT and have it add more features. That entire script was written by AI. Just be specific in your prompt:
"I'm using the latest patch of Warcraft 3. This script was designed using vJass and interfaces with GUI variables/triggers. Can you add X new feature?"
 
Last edited:
No problem, also I forgot this variable in my Illidan example:
  • Set VariableSet SFX_MoveOutBeforeDestroy = true
Without that the system doesn't know to move the sfx out of sight prior to destroying it.

Also, you can copy and paste that code into ChatGPT and have it add more features. That entire script was written by AI. Just be specific in your prompt:
"I'm using the latest patch of Warcraft 3. This script was designed using vJass and interfaces with GUI variables/triggers. Can you add X new feature?"
Just wanted to say the system works wonders. I tested it and at first I struggled a bit trying to get two consecutive SFX to spawn at the same position. Eventually I realized the system destroys the SFX_CreatePoint internally, so I needed to redeclare the point before spawning the second effect. Once I did that everything worked as it should.

I also noticed the script has room for some more advanced usage like the OnCreationTrigger which could be used to attach maybe sounds or additional effects when the SFX is created. The way the system handles optional parameters with the “unset” values is also really clean.

Overall it’s a very neat system and super convenient to use from GUI. Thanks for sharing it rly.


  • Then - Actions
    • Set FW_Trail_Counter[FW_Loop_I] = 0
    • Set FW_TempReal = (Random real number between -35.00 and 35.00)
    • Set FW_TempReal2 = (Random real number between -15.00 and 15.00)
    • Set FW_TempReal3 = (FW_Angle[FW_Loop_I] + 90.00)
    • Set FW_TempPoint3 = (FW_TempPoint offset by FW_TempReal towards FW_TempReal3 degrees.)
    • Set FW_TempPoint3 = (FW_TempPoint3 offset by FW_TempReal2 towards FW_Angle[FW_Loop_I] degrees.)
    • Set SFX_ModelPath = Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
    • Set SFX_CreatePoint = FW_TempPoint3
    • Set SFX_CreateDelay = 0.25
    • Set SFX_DestroyDelay = 1.25
    • Set SFX_AlphaStart = 255
    • Set SFX_Scale = 0.45
    • Set SFX_AlphaEnd = 0
    • Set SFX_FadeDuration = 0.50
    • Set SFX_MoveOutBeforeDestroy = False
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Set FW_TempPoint3 = (FW_TempPoint offset by FW_TempReal towards FW_TempReal3 degrees.)
    • Set FW_TempPoint3 = (FW_TempPoint3 offset by FW_TempReal2 towards FW_Angle[FW_Loop_I] degrees.)
    • Special Effect - Create a special effect at FW_TempPoint3 using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
    • Set SFX_DestroyDelay = 1.00
    • Set SFX_MoveOutBeforeDestroy = True
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Custom script: call RemoveLocation( udg_FW_TempPoint3)


 
No problem, I'm glad it's helpful.

And here's some updated code that fixes your CreatePoint issue. You'll want to replace the old code with this new code, and also create a new boolean variable called SFX_RemovePoint. This controls whether the system automatically calls "RemoveLocation" on CreatePoint or not. By default it's set to True for convenience sake, but for situations like this where you'd like to reuse CreatePoint you can Set it to False.
vJASS:
library GuiSpecialEffectSystem initializer Init

    globals
        // =========================================================
        // INTERNALS
        // =========================================================
        private constant real PERIOD = 0.03125
        private hashtable HT = null

        // =========================================================
        // GUI INPUT VARIABLES
        // =========================================================
        // string   udg_SFX_ModelPath              = ""
        // location udg_SFX_CreatePoint            = null
        // real     udg_SFX_CreateDelay            = 0.00
        // real     udg_SFX_DestroyDelay           = -1.00
        // real     udg_SFX_Scale                  = -1.00
        // real     udg_SFX_TimeScale              = -1.00
        // real     udg_SFX_Height                 = -10000.00
        // real     udg_SFX_FadeDuration           = -1.00
        // integer  udg_SFX_Data                   = 0
        // integer  udg_SFX_Red                    = -1
        // integer  udg_SFX_Green                  = -1
        // integer  udg_SFX_Blue                   = -1
        // integer  udg_SFX_AlphaStart             = -1
        // integer  udg_SFX_AlphaEnd               = -1
        // boolean  udg_SFX_MoveOutBeforeDestroy   = false
        // boolean  udg_SFX_RemovePoint            = true
        // trigger  udg_SFX_OnCreationTrigger      = null
    endglobals

    private function Clamp255 takes integer i returns integer
        if i < 0 then
            return 0
        endif
        if i > 255 then
            return 255
        endif
        return i
    endfunction

    struct SfxJob
        effect e
        timer t

        string model
        real x
        real y

        real createDelay
        real destroyDelay
        real fadeDuration
        real elapsed

        real height
        real scale
        real timeScale

        integer data
        integer r
        integer g
        integer b
        integer alphaStart
        integer alphaEnd

        boolean moveOutBeforeDestroy
        boolean removePoint

        boolean hasColor
        boolean hasFade
        boolean fadeFinished
        boolean startedLifecycle

        trigger onCreationTrig

        // ----------------------------
        // lifecycle helpers
        // ----------------------------
        method destroyEffect takes nothing returns nothing
            if this.e != null then
                if this.moveOutBeforeDestroy then
                    call BlzSetSpecialEffectZ(this.e, 5000)
                    call BlzSetSpecialEffectX(this.e, GetLocationX(udg_SFX___Offscreen))
                    call BlzSetSpecialEffectY(this.e, GetLocationY(udg_SFX___Offscreen))
                endif
                call DestroyEffect(this.e)
                set this.e = null
            endif
        endmethod

        method cleanup takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            call this.destroyEffect()
            set this.onCreationTrig = null
            call this.deallocate()
        endmethod

        method cleanupKeepEffect takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            set this.onCreationTrig = null
            set this.e = null
            call this.deallocate()
        endmethod

        method applyMods takes nothing returns nothing
            if this.e == null then
                return
            endif

            if this.scale >= 0.00 then
                call BlzSetSpecialEffectScale(this.e, this.scale)
            endif

            if this.timeScale >= 0.00 then
                call BlzSetSpecialEffectTimeScale(this.e, this.timeScale)
            endif

            if this.height > -9999.00 then
                call BlzSetSpecialEffectZ(this.e, this.height)
            endif

            if this.hasColor then
                call BlzSetSpecialEffectColor(this.e, this.r, this.g, this.b)
            endif

            if this.alphaStart >= 0 then
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(this.alphaStart))
            endif
        endmethod

        method createOrBindEffect takes nothing returns nothing
            if this.model != "" then
                set this.e = AddSpecialEffect(this.model, this.x, this.y)
                if this.onCreationTrig != null then
                    if udg_SFX_CreatePoint != null then
                        call RemoveLocation(udg_SFX_CreatePoint)
                    endif
                    set udg_SFX_Data = this.data
                    set udg_SFX_CreatePoint = Location(this.x, this.y)
                    call TriggerExecute(this.onCreationTrig)
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.e = bj_lastCreatedEffect
            endif
        endmethod

        method beginActiveLifecycle takes nothing returns nothing
            if this.e == null then
                call this.cleanup()
                return
            endif

            set this.startedLifecycle = true
            set this.elapsed = 0.00

            call this.applyMods()

            // No periodic work needed
            if this.destroyDelay < 0.00 and not this.hasFade then
                call this.cleanupKeepEffect()
                return
            endif

            call SaveInteger(HT, GetHandleId(this.t), 0, this)
            call TimerStart(this.t, PERIOD, true, function thistype.onPeriodic)
        endmethod

        static method onPeriodic takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)
            local real progress
            local integer a

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            if this.e == null then
                call this.cleanup()
                set expired = null
                return
            endif

            set this.elapsed = this.elapsed + PERIOD

            // Fade handling
            if this.hasFade and not this.fadeFinished and this.fadeDuration > 0.00 then
                set progress = this.elapsed / this.fadeDuration
                if progress > 1.00 then
                    set progress = 1.00
                endif

                set a = R2I(this.alphaStart + (this.alphaEnd - this.alphaStart) * progress)
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(a))

                if this.elapsed >= this.fadeDuration then
                    set this.fadeFinished = true
                endif
            endif

            // Destroy handling
            if this.destroyDelay >= 0.00 and this.elapsed >= this.destroyDelay then
                call this.cleanup()
            endif

            set expired = null
        endmethod

        static method onCreateDelayExpire takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            call PauseTimer(expired)

            call this.createOrBindEffect()
            call this.beginActiveLifecycle()

            set expired = null
        endmethod

        static method createFromGlobals takes nothing returns thistype
            local thistype this = thistype.allocate()

            if udg_SFX_CreatePoint != null then
                set this.x = GetLocationX(udg_SFX_CreatePoint)
                set this.y = GetLocationY(udg_SFX_CreatePoint)
                if this.removePoint then
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.x = 0.00
                set this.y = 0.00
            endif

            set this.model                = udg_SFX_ModelPath
            set this.createDelay          = udg_SFX_CreateDelay
            set this.destroyDelay         = udg_SFX_DestroyDelay
            set this.fadeDuration         = udg_SFX_FadeDuration
            set this.height               = udg_SFX_Height
            set this.scale                = udg_SFX_Scale
            set this.timeScale            = udg_SFX_TimeScale

            set this.data                 = udg_SFX_Data
            set this.r                    = udg_SFX_Red
            set this.g                    = udg_SFX_Green
            set this.b                    = udg_SFX_Blue
            set this.alphaStart           = udg_SFX_AlphaStart
            set this.alphaEnd             = udg_SFX_AlphaEnd

            set this.moveOutBeforeDestroy = udg_SFX_MoveOutBeforeDestroy
            set this.removePoint          = udg_SFX_RemovePoint
            set this.onCreationTrig       = udg_SFX_OnCreationTrigger

            set this.hasColor             = (udg_SFX_Red >= 0 and udg_SFX_Green >= 0 and udg_SFX_Blue >= 0)
            set this.hasFade              = (udg_SFX_FadeDuration > 0.00 and udg_SFX_AlphaStart >= 0 and udg_SFX_AlphaEnd >= 0)
            set this.fadeFinished         = false
            set this.startedLifecycle     = false

            set this.e = null
            set this.t = CreateTimer()

            return this
        endmethod

        method launch takes nothing returns nothing
            local integer timerId = GetHandleId(this.t)

            call SaveInteger(HT, timerId, 0, this)

            if this.createDelay > 0.00 then
                call TimerStart(this.t, this.createDelay, false, function thistype.onCreateDelayExpire)
            else
                call this.createOrBindEffect()
                call this.beginActiveLifecycle()
            endif
        endmethod
    endstruct

    // =============================================================
    // GUI ENTRY POINT
    //
    // GUI:
    // Custom script: call GuiSfx_Run()
    // =============================================================

    function GuiSpecialEffectSystem_Reset takes nothing returns nothing
        set udg_SFX_ModelPath = ""
        set udg_SFX_CreatePoint = null
        set udg_SFX_CreateDelay = 0.00
        set udg_SFX_DestroyDelay = -1.00
        set udg_SFX_Scale = -1.00
        set udg_SFX_TimeScale = -1.00
        set udg_SFX_Height = -10000.00
        set udg_SFX_FadeDuration = -1.00
        set udg_SFX_Red = -1
        set udg_SFX_Green = -1
        set udg_SFX_Blue = -1
        set udg_SFX_AlphaStart = -1
        set udg_SFX_AlphaEnd = -1
        set udg_SFX_RemovePoint = true
        set udg_SFX_MoveOutBeforeDestroy = false
        set udg_SFX_OnCreationTrigger = null
    endfunction

    function GuiSpecialEffectSystem_Run takes nothing returns nothing
        local SfxJob job = SfxJob.createFromGlobals()
        call job.launch()
        call GuiSpecialEffectSystem_Reset()
    endfunction

    private function Init takes nothing returns nothing
        set HT = InitHashtable()
    endfunction

endlibrary

Here's a version of your original Actions with my new changes applied:
  • Then - Actions
    • Set FW_Trail_Counter[FW_Loop_I] = 0
    • Set FW_TempReal = (Random real number between -35.00 and 35.00)
    • Set FW_TempReal2 = (Random real number between -15.00 and 15.00)
    • Set FW_TempReal3 = (FW_Angle[FW_Loop_I] + 90.00)
    • Set FW_TempPoint3 = (FW_TempPoint offset by FW_TempReal towards FW_TempReal3 degrees.)
    • Set SFX_CreatePoint = (FW_TempPoint3 offset by FW_TempReal2 towards FW_Angle[FW_Loop_I] degrees.)
    • Set SFX_ModelPath = Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
    • Set SFX_CreateDelay = 0.25
    • Set SFX_DestroyDelay = 1.25
    • Set SFX_FadeDuration = 0.50
    • Set SFX_AlphaStart = 255
    • Set SFX_AlphaEnd = 0
    • Set SFX_Scale = 0.45
    • Set SFX_RemovePoint = false
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Special Effect - Create a special effect at SFX_CreatePoint using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
    • Set SFX_DestroyDelay = 1.00
    • Set SFX_MoveOutBeforeDestroy = True
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Custom script: call RemoveLocation( udg_FW_TempPoint3)
Note that your original Actions leaked a Point with how you were using Point With Polar Offset. You can't Set TempPoint3 = TempPoint3... that'll leak the original TempPoint3. But now that we can prevent CreatePoint from being automatically removed, we can avoid that issue entirely.

Some other things to note:
1. SFX_MoveOutBeforeDestroy will default to False, so setting it to False was unnecessary.

2. SFX_OnCreationTrigger will run the desired Trigger upon creating the Special Effect.
Currently, you have two Variables you can reference inside of that trigger -> SFX_CreatePoint and SFX_Data.
It'll generate a brand new CreatePoint and Remove it afterwards, so don't worry about it becoming a memory leak.

3. SFX_Data can be used to represent Player Numbers and be combined with Array variables to track data over time. For example, let's say your Special Effect also spawns in a Zombie. If Player 2 created the Special Effect then you could set SFX_Data to 2 to represent this fact. Then when your SFX_OnCreationTrigger runs, you could Create the Zombie at SFX_CreatePoint and have it owned by Player(SFX_Data) which will be Player 2 in this case.

4. It might not be safe to run the system again from within a SFX_OnCreationTrigger. I believe it'd leak CreatePoint (if set). An entirely different Point variable could be created and used exclusively in SFX_OnCreationTrigger to avoid this issue (inside of the code).
 

Attachments

Last edited:
No problem, I'm glad it's helpful.

And here's some updated code that fixes your CreatePoint issue. You'll want to replace the old code with this new code, and also create a new boolean variable called SFX_RemovePoint. This controls whether the system automatically calls "RemoveLocation" on CreatePoint or not. By default it's set to True for convenience sake, but for situations like this where you'd like to reuse CreatePoint you can Set it to False.
vJASS:
library GuiSpecialEffectSystem initializer Init

    globals
        // =========================================================
        // INTERNALS
        // =========================================================
        private constant real PERIOD = 0.03125
        private hashtable HT = null

        // =========================================================
        // GUI INPUT VARIABLES
        // =========================================================
        // string   udg_SFX_ModelPath              = ""
        // location udg_SFX_CreatePoint            = null
        // real     udg_SFX_CreateDelay            = 0.00
        // real     udg_SFX_DestroyDelay           = -1.00
        // real     udg_SFX_Scale                  = -1.00
        // real     udg_SFX_TimeScale              = -1.00
        // real     udg_SFX_Height                 = -10000.00
        // real     udg_SFX_FadeDuration           = -1.00
        // integer  udg_SFX_Data                   = 0
        // integer  udg_SFX_Red                    = -1
        // integer  udg_SFX_Green                  = -1
        // integer  udg_SFX_Blue                   = -1
        // integer  udg_SFX_AlphaStart             = -1
        // integer  udg_SFX_AlphaEnd               = -1
        // boolean  udg_SFX_MoveOutBeforeDestroy   = false
        // boolean  udg_SFX_RemovePoint            = true
        // trigger  udg_SFX_OnCreationTrigger      = null
    endglobals

    private function Clamp255 takes integer i returns integer
        if i < 0 then
            return 0
        endif
        if i > 255 then
            return 255
        endif
        return i
    endfunction

    struct SfxJob
        effect e
        timer t

        string model
        real x
        real y

        real createDelay
        real destroyDelay
        real fadeDuration
        real elapsed

        real height
        real scale
        real timeScale

        integer data
        integer r
        integer g
        integer b
        integer alphaStart
        integer alphaEnd

        boolean moveOutBeforeDestroy
        boolean removePoint

        boolean hasColor
        boolean hasFade
        boolean fadeFinished
        boolean startedLifecycle

        trigger onCreationTrig

        // ----------------------------
        // lifecycle helpers
        // ----------------------------
        method destroyEffect takes nothing returns nothing
            if this.e != null then
                if this.moveOutBeforeDestroy then
                    call BlzSetSpecialEffectZ(this.e, 5000)
                    call BlzSetSpecialEffectX(this.e, GetLocationX(udg_SFX___Offscreen))
                    call BlzSetSpecialEffectY(this.e, GetLocationY(udg_SFX___Offscreen))
                endif
                call DestroyEffect(this.e)
                set this.e = null
            endif
        endmethod

        method cleanup takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            call this.destroyEffect()
            set this.onCreationTrig = null
            call this.deallocate()
        endmethod

        method cleanupKeepEffect takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            set this.onCreationTrig = null
            set this.e = null
            call this.deallocate()
        endmethod

        method applyMods takes nothing returns nothing
            if this.e == null then
                return
            endif

            if this.scale >= 0.00 then
                call BlzSetSpecialEffectScale(this.e, this.scale)
            endif

            if this.timeScale >= 0.00 then
                call BlzSetSpecialEffectTimeScale(this.e, this.timeScale)
            endif

            if this.height > -9999.00 then
                call BlzSetSpecialEffectZ(this.e, this.height)
            endif

            if this.hasColor then
                call BlzSetSpecialEffectColor(this.e, this.r, this.g, this.b)
            endif

            if this.alphaStart >= 0 then
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(this.alphaStart))
            endif
        endmethod

        method createOrBindEffect takes nothing returns nothing
            if this.model != "" then
                set this.e = AddSpecialEffect(this.model, this.x, this.y)
                if this.onCreationTrig != null then
                    if udg_SFX_CreatePoint != null then
                        call RemoveLocation(udg_SFX_CreatePoint)
                    endif
                    set udg_SFX_Data = this.data
                    set udg_SFX_CreatePoint = Location(this.x, this.y)
                    call TriggerExecute(this.onCreationTrig)
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.e = bj_lastCreatedEffect
            endif
        endmethod

        method beginActiveLifecycle takes nothing returns nothing
            if this.e == null then
                call this.cleanup()
                return
            endif

            set this.startedLifecycle = true
            set this.elapsed = 0.00

            call this.applyMods()

            // No periodic work needed
            if this.destroyDelay < 0.00 and not this.hasFade then
                call this.cleanupKeepEffect()
                return
            endif

            call SaveInteger(HT, GetHandleId(this.t), 0, this)
            call TimerStart(this.t, PERIOD, true, function thistype.onPeriodic)
        endmethod

        static method onPeriodic takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)
            local real progress
            local integer a

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            if this.e == null then
                call this.cleanup()
                set expired = null
                return
            endif

            set this.elapsed = this.elapsed + PERIOD

            // Fade handling
            if this.hasFade and not this.fadeFinished and this.fadeDuration > 0.00 then
                set progress = this.elapsed / this.fadeDuration
                if progress > 1.00 then
                    set progress = 1.00
                endif

                set a = R2I(this.alphaStart + (this.alphaEnd - this.alphaStart) * progress)
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(a))

                if this.elapsed >= this.fadeDuration then
                    set this.fadeFinished = true
                endif
            endif

            // Destroy handling
            if this.destroyDelay >= 0.00 and this.elapsed >= this.destroyDelay then
                call this.cleanup()
            endif

            set expired = null
        endmethod

        static method onCreateDelayExpire takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            call PauseTimer(expired)

            call this.createOrBindEffect()
            call this.beginActiveLifecycle()

            set expired = null
        endmethod

        static method createFromGlobals takes nothing returns thistype
            local thistype this = thistype.allocate()

            if udg_SFX_CreatePoint != null then
                set this.x = GetLocationX(udg_SFX_CreatePoint)
                set this.y = GetLocationY(udg_SFX_CreatePoint)
                if this.removePoint then
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.x = 0.00
                set this.y = 0.00
            endif

            set this.model                = udg_SFX_ModelPath
            set this.createDelay          = udg_SFX_CreateDelay
            set this.destroyDelay         = udg_SFX_DestroyDelay
            set this.fadeDuration         = udg_SFX_FadeDuration
            set this.height               = udg_SFX_Height
            set this.scale                = udg_SFX_Scale
            set this.timeScale            = udg_SFX_TimeScale

            set this.data                 = udg_SFX_Data
            set this.r                    = udg_SFX_Red
            set this.g                    = udg_SFX_Green
            set this.b                    = udg_SFX_Blue
            set this.alphaStart           = udg_SFX_AlphaStart
            set this.alphaEnd             = udg_SFX_AlphaEnd

            set this.moveOutBeforeDestroy = udg_SFX_MoveOutBeforeDestroy
            set this.removePoint          = udg_SFX_RemovePoint
            set this.onCreationTrig       = udg_SFX_OnCreationTrigger

            set this.hasColor             = (udg_SFX_Red >= 0 and udg_SFX_Green >= 0 and udg_SFX_Blue >= 0)
            set this.hasFade              = (udg_SFX_FadeDuration > 0.00 and udg_SFX_AlphaStart >= 0 and udg_SFX_AlphaEnd >= 0)
            set this.fadeFinished         = false
            set this.startedLifecycle     = false

            set this.e = null
            set this.t = CreateTimer()

            return this
        endmethod

        method launch takes nothing returns nothing
            local integer timerId = GetHandleId(this.t)

            call SaveInteger(HT, timerId, 0, this)

            if this.createDelay > 0.00 then
                call TimerStart(this.t, this.createDelay, false, function thistype.onCreateDelayExpire)
            else
                call this.createOrBindEffect()
                call this.beginActiveLifecycle()
            endif
        endmethod
    endstruct

    // =============================================================
    // GUI ENTRY POINT
    //
    // GUI:
    // Custom script: call GuiSfx_Run()
    // =============================================================

    function GuiSpecialEffectSystem_Reset takes nothing returns nothing
        set udg_SFX_ModelPath = ""
        set udg_SFX_CreatePoint = null
        set udg_SFX_CreateDelay = 0.00
        set udg_SFX_DestroyDelay = -1.00
        set udg_SFX_Scale = -1.00
        set udg_SFX_TimeScale = -1.00
        set udg_SFX_Height = -10000.00
        set udg_SFX_FadeDuration = -1.00
        set udg_SFX_Red = -1
        set udg_SFX_Green = -1
        set udg_SFX_Blue = -1
        set udg_SFX_AlphaStart = -1
        set udg_SFX_AlphaEnd = -1
        set udg_SFX_RemovePoint = true
        set udg_SFX_MoveOutBeforeDestroy = false
        set udg_SFX_OnCreationTrigger = null
    endfunction

    function GuiSpecialEffectSystem_Run takes nothing returns nothing
        local SfxJob job = SfxJob.createFromGlobals()
        call job.launch()
        call GuiSpecialEffectSystem_Reset()
    endfunction

    private function Init takes nothing returns nothing
        set HT = InitHashtable()
    endfunction

endlibrary

Here's a version of your original Actions with my new changes applied:
  • Then - Actions
    • Set FW_Trail_Counter[FW_Loop_I] = 0
    • Set FW_TempReal = (Random real number between -35.00 and 35.00)
    • Set FW_TempReal2 = (Random real number between -15.00 and 15.00)
    • Set FW_TempReal3 = (FW_Angle[FW_Loop_I] + 90.00)
    • Set FW_TempPoint3 = (FW_TempPoint offset by FW_TempReal towards FW_TempReal3 degrees.)
    • Set SFX_CreatePoint = (FW_TempPoint3 offset by FW_TempReal2 towards FW_Angle[FW_Loop_I] degrees.)
    • Set SFX_ModelPath = Abilities\Spells\Other\BreathOfFire\BreathOfFireDamage.mdl
    • Set SFX_CreateDelay = 0.25
    • Set SFX_DestroyDelay = 1.25
    • Set SFX_FadeDuration = 0.50
    • Set SFX_AlphaStart = 255
    • Set SFX_AlphaEnd = 0
    • Set SFX_Scale = 0.45
    • Set SFX_RemovePoint = false
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Special Effect - Create a special effect at SFX_CreatePoint using Objects\Spawnmodels\Other\IllidanFootprint\IllidanSpawnFootPrint1.mdl
    • Set SFX_DestroyDelay = 1.00
    • Set SFX_MoveOutBeforeDestroy = True
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • Custom script: call RemoveLocation( udg_FW_TempPoint3)
Note that your original Actions leaked a Point with how you were using Point With Polar Offset. You can't Set TempPoint3 = TempPoint3... that'll leak the original TempPoint3. But now that we can prevent CreatePoint from being automatically removed, we can avoid that issue entirely.

Some other things to note:
1. SFX_MoveOutBeforeDestroy will default to False, so setting it to False was unnecessary.

2. SFX_OnCreationTrigger will run the desired Trigger upon creating the Special Effect.
Currently, you have two Variables you can reference inside of that trigger -> SFX_CreatePoint and SFX_Data.
It'll generate a brand new CreatePoint and Remove it afterwards, so don't worry about it becoming a memory leak.

3. SFX_Data can be used to represent Player Numbers and be combined with Array variables to track data over time. For example, let's say your Special Effect also spawns in a Zombie. If Player 2 created the Special Effect then you could set SFX_Data to 2 to represent this fact. Then when your SFX_OnCreationTrigger runs, you could Create the Zombie at SFX_CreatePoint and have it owned by Player(SFX_Data) which will be Player 2 in this case.

4. It might not be safe to run the system again from within a SFX_OnCreationTrigger. I believe it'd leak CreatePoint (if set). An entirely different Point variable could be created and used exclusively in SFX_OnCreationTrigger to avoid this issue.
I just woke up to what looks like a new version of the SFX Manager. xx

I ended up redeclaring TempPoint3, mostly out of instinct since I sometimes do something similar with temporary integer/real variables (assigning the current value to another temp variable before reusing it).

It worked, but your change makes the whole thing much cleaner, thanks for pointing out the point leak.

Really appreciate the update and the detailed explanation, I’ll see in which ways I can expand the script code itself, but even now it already is an overkill.


Update 1:

I've updated the special effect system with a few enhancements:

  • Full orientation control – You can now set SFX_Yaw, SFX_Pitch, and SFX_Roll (in radians) to rotate effects any way you want.
  • Ground‑relative height – When you create an effect with a point (SFX_CreatePoint), SFX_Height now acts as an offset above the terrain. The system automatically fetches the ground elevation and adds your height, so effects no longer sink into cliffs.
Update 2:

-----------------------------------
Set up your effect as usual (model, point, delays, etc.).
Choose an animation type SFX_AnimationType (0 = none, 1 = birth, 2 = stand, 3 = death, 4 = attack, 5 = spell, 6 = decay)
Optionally set SFX_AnimationPeriod to repeat the animation every X seconds.
Call Custom script: call GuiSpecialEffectSystem_Run()
! Note ! : This will work only with effects that have named animations.
----------
Tnx Uncle <3
-------------------------------------




JASS:
library GuiSpecialEffectSystem initializer GuiSfx_Init

    globals
        // =========================================================
        // INTERNALS
        // =========================================================
        private constant real PERIOD = 0.03125
        private hashtable HT = null

        // =========================================================
        // GUI INPUT VARIABLES
        // =========================================================
        // string   udg_SFX_ModelPath              = ""
        // location udg_SFX_CreatePoint            = null
        // real     udg_SFX_CreateDelay            = 0.00
        // real     udg_SFX_DestroyDelay           = -1.00
        // real     udg_SFX_Scale                  = -1.00
        // real     udg_SFX_TimeScale              = -1.00
        // real     udg_SFX_Height                 = -10000.00
        // real     udg_SFX_FadeDuration           = -1.00
        // integer  udg_SFX_Data                   = 0
        // integer  udg_SFX_Red                    = -1
        // integer  udg_SFX_Green                  = -1
        // integer  udg_SFX_Blue                   = -1
        // integer  udg_SFX_AlphaStart             = -1
        // integer  udg_SFX_AlphaEnd               = -1
        // boolean  udg_SFX_MoveOutBeforeDestroy   = false
        // boolean  udg_SFX_RemovePoint            = true
        // trigger  udg_SFX_OnCreationTrigger      = null
        // ===== ANIMATION =====
        // integer  udg_SFX_AnimationType          = 0    // 0=none, 1=birth, 2=stand, 3=death, 4=attack, 5=spell, 6=decay
        // real     udg_SFX_AnimationPeriod        = -1.00 // if >0, plays that animation every period
        // ===== ORIENTATION =====
        // real     udg_SFX_Yaw                     = -9999.00
        // real     udg_SFX_Pitch                   = -9999.00
        // real     udg_SFX_Roll                    = -9999.00
    endglobals

    private function Clamp255 takes integer i returns integer
        if i < 0 then
            return 0
        endif
        if i > 255 then
            return 255
        endif
        return i
    endfunction

    // Maps GUI animation type to WC3 animtype constant
    private function GetSfxAnimType takes integer t returns animtype
        if t == 1 then
            return ANIM_TYPE_BIRTH
        elseif t == 2 then
            return ANIM_TYPE_STAND
        elseif t == 3 then
            return ANIM_TYPE_DEATH
        elseif t == 4 then
            return ANIM_TYPE_ATTACK
        elseif t == 5 then
            return ANIM_TYPE_SPELL
        elseif t == 6 then
            return ANIM_TYPE_DECAY
        endif
        return ANIM_TYPE_STAND   // default
    endfunction

    struct SfxJob
        effect e
        timer t

        string model
        real x
        real y
        boolean hasValidPoint

        real createDelay
        real destroyDelay
        real fadeDuration
        real elapsed

        real height
        real scale
        real timeScale
        real yaw
        real pitch
        real roll

        integer data
        integer r
        integer g
        integer b
        integer alphaStart
        integer alphaEnd
        integer animationType
        real    animationPeriod
        real    nextAnimationTime

        boolean moveOutBeforeDestroy
        boolean removePoint

        boolean hasColor
        boolean hasFade
        boolean fadeFinished
        boolean startedLifecycle

        trigger onCreationTrig

        // ----------------------------
        // lifecycle helpers
        // ----------------------------
        method destroyEffect takes nothing returns nothing
            if this.e != null then
                if this.moveOutBeforeDestroy then
                    call BlzSetSpecialEffectZ(this.e, 5000)
                    call BlzSetSpecialEffectX(this.e, GetLocationX(udg_SFX___Offscreen))
                    call BlzSetSpecialEffectY(this.e, GetLocationY(udg_SFX___Offscreen))
                endif
                call DestroyEffect(this.e)
                set this.e = null
            endif
        endmethod

        method cleanup takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            call this.destroyEffect()
            set this.onCreationTrig = null
            call this.deallocate()
        endmethod

        method cleanupKeepEffect takes nothing returns nothing
            if this.t != null then
                call PauseTimer(this.t)
                call FlushChildHashtable(HT, GetHandleId(this.t))
                call DestroyTimer(this.t)
                set this.t = null
            endif
            set this.onCreationTrig = null
            set this.e = null
            call this.deallocate()
        endmethod

        method applyMods takes nothing returns nothing
            local location loc
            local real terrainZ

            if this.e == null then
                return
            endif

            if this.scale >= 0.00 then
                call BlzSetSpecialEffectScale(this.e, this.scale)
            endif

            if this.timeScale >= 0.00 then
                call BlzSetSpecialEffectTimeScale(this.e, this.timeScale)
            endif

            if this.height > -9999.00 then
                if this.hasValidPoint then
                    set loc = Location(this.x, this.y)
                    set terrainZ = GetLocationZ(loc)
                    call BlzSetSpecialEffectZ(this.e, terrainZ + this.height)
                    call RemoveLocation(loc)
                else
                    call BlzSetSpecialEffectZ(this.e, this.height)
                endif
            endif

            if this.hasColor then
                call BlzSetSpecialEffectColor(this.e, this.r, this.g, this.b)
            endif

            if this.alphaStart >= 0 then
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(this.alphaStart))
            endif

            // Initial animation
            if this.animationType > 0 then
                call BlzPlaySpecialEffect(this.e, GetSfxAnimType(this.animationType))
            endif

            // Orientation
            if this.yaw > -9999.00 then
                call BlzSetSpecialEffectYaw(this.e, this.yaw)
            endif
            if this.pitch > -9999.00 then
                call BlzSetSpecialEffectPitch(this.e, this.pitch)
            endif
            if this.roll > -9999.00 then
                call BlzSetSpecialEffectRoll(this.e, this.roll)
            endif
        endmethod

        method createOrBindEffect takes nothing returns nothing
            if this.model != "" then
                set this.e = AddSpecialEffect(this.model, this.x, this.y)
                if this.onCreationTrig != null then
                    if udg_SFX_CreatePoint != null then
                        call RemoveLocation(udg_SFX_CreatePoint)
                    endif
                    set udg_SFX_Data = this.data
                    set udg_SFX_CreatePoint = Location(this.x, this.y)
                    call TriggerExecute(this.onCreationTrig)
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.e = bj_lastCreatedEffect
            endif
        endmethod

        method beginActiveLifecycle takes nothing returns nothing
            if this.e == null then
                call this.cleanup()
                return
            endif

            set this.startedLifecycle = true
            set this.elapsed = 0.00
            set this.nextAnimationTime = this.animationPeriod

            call this.applyMods()

            // No periodic work needed if no destroy, no fade, and no periodic animation
            if this.destroyDelay < 0.00 and not this.hasFade and this.animationPeriod <= 0.00 then
                call this.cleanupKeepEffect()
                return
            endif

            call SaveInteger(HT, GetHandleId(this.t), 0, this)
            call TimerStart(this.t, PERIOD, true, function thistype.onPeriodic)
        endmethod

        static method onPeriodic takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)
            local real progress
            local integer a

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            if this.e == null then
                call this.cleanup()
                set expired = null
                return
            endif

            set this.elapsed = this.elapsed + PERIOD

            // Periodic animation trigger
            if this.animationPeriod > 0.00 and this.animationType > 0 then
                if this.elapsed >= this.nextAnimationTime then
                    call BlzPlaySpecialEffect(this.e, GetSfxAnimType(this.animationType))
                    set this.nextAnimationTime = this.nextAnimationTime + this.animationPeriod
                endif
            endif

            // Fade handling
            if this.hasFade and not this.fadeFinished and this.fadeDuration > 0.00 then
                set progress = this.elapsed / this.fadeDuration
                if progress > 1.00 then
                    set progress = 1.00
                endif

                set a = R2I(this.alphaStart + (this.alphaEnd - this.alphaStart) * progress)
                call BlzSetSpecialEffectAlpha(this.e, Clamp255(a))

                if this.elapsed >= this.fadeDuration then
                    set this.fadeFinished = true
                endif
            endif

            // Destroy handling
            if this.destroyDelay >= 0.00 and this.elapsed >= this.destroyDelay then
                call this.cleanup()
            endif

            set expired = null
        endmethod

        static method onCreateDelayExpire takes nothing returns nothing
            local timer expired = GetExpiredTimer()
            local integer timerId = GetHandleId(expired)
            local thistype this = LoadInteger(HT, timerId, 0)

            if this == 0 then
                call PauseTimer(expired)
                call FlushChildHashtable(HT, timerId)
                call DestroyTimer(expired)
                set expired = null
                return
            endif

            call PauseTimer(expired)

            call this.createOrBindEffect()
            call this.beginActiveLifecycle()

            set expired = null
        endmethod

        static method createFromGlobals takes nothing returns thistype
            local thistype this = thistype.allocate()

            if udg_SFX_CreatePoint != null then
                set this.x = GetLocationX(udg_SFX_CreatePoint)
                set this.y = GetLocationY(udg_SFX_CreatePoint)
                set this.hasValidPoint = true
                if this.removePoint then
                    call RemoveLocation(udg_SFX_CreatePoint)
                endif
            else
                set this.x = 0.00
                set this.y = 0.00
                set this.hasValidPoint = false
            endif

            set this.model                = udg_SFX_ModelPath
            set this.createDelay          = udg_SFX_CreateDelay
            set this.destroyDelay         = udg_SFX_DestroyDelay
            set this.fadeDuration         = udg_SFX_FadeDuration
            set this.height               = udg_SFX_Height
            set this.scale                = udg_SFX_Scale
            set this.timeScale            = udg_SFX_TimeScale
            set this.yaw                  = udg_SFX_Yaw
            set this.pitch                = udg_SFX_Pitch
            set this.roll                 = udg_SFX_Roll

            set this.data                 = udg_SFX_Data
            set this.r                    = udg_SFX_Red
            set this.g                    = udg_SFX_Green
            set this.b                    = udg_SFX_Blue
            set this.alphaStart           = udg_SFX_AlphaStart
            set this.alphaEnd             = udg_SFX_AlphaEnd
            set this.animationType         = udg_SFX_AnimationType
            set this.animationPeriod       = udg_SFX_AnimationPeriod

            set this.moveOutBeforeDestroy = udg_SFX_MoveOutBeforeDestroy
            set this.removePoint          = udg_SFX_RemovePoint
            set this.onCreationTrig       = udg_SFX_OnCreationTrigger

            set this.hasColor             = (udg_SFX_Red >= 0 and udg_SFX_Green >= 0 and udg_SFX_Blue >= 0)
            set this.hasFade              = (udg_SFX_FadeDuration > 0.00 and udg_SFX_AlphaStart >= 0 and udg_SFX_AlphaEnd >= 0)
            set this.fadeFinished         = false
            set this.startedLifecycle     = false

            set this.e = null
            set this.t = CreateTimer()

            return this
        endmethod

        method launch takes nothing returns nothing
            local integer timerId = GetHandleId(this.t)

            call SaveInteger(HT, timerId, 0, this)

            if this.createDelay > 0.00 then
                call TimerStart(this.t, this.createDelay, false, function thistype.onCreateDelayExpire)
            else
                call this.createOrBindEffect()
                call this.beginActiveLifecycle()
            endif
        endmethod
    endstruct

    // =============================================================
    // GUI ENTRY POINT
    //
    // GUI:
    // Custom script: call GuiSfx_Run()
    // =============================================================

    function GuiSpecialEffectSystem_Reset takes nothing returns nothing
        set udg_SFX_ModelPath = ""
        set udg_SFX_CreatePoint = null
        set udg_SFX_CreateDelay = 0.00
        set udg_SFX_DestroyDelay = -1.00
        set udg_SFX_Scale = -1.00
        set udg_SFX_TimeScale = -1.00
        set udg_SFX_Height = -10000.00
        set udg_SFX_FadeDuration = -1.00
        set udg_SFX_Red = -1
        set udg_SFX_Green = -1
        set udg_SFX_Blue = -1
        set udg_SFX_AlphaStart = -1
        set udg_SFX_AlphaEnd = -1
        set udg_SFX_RemovePoint = true
        set udg_SFX_MoveOutBeforeDestroy = false
        set udg_SFX_OnCreationTrigger = null
        set udg_SFX_AnimationType = 0
        set udg_SFX_AnimationPeriod = -1.00
        set udg_SFX_Yaw = -9999.00
        set udg_SFX_Pitch = -9999.00
        set udg_SFX_Roll = -9999.00
    endfunction

    function GuiSpecialEffectSystem_Run takes nothing returns nothing
        local SfxJob job = SfxJob.createFromGlobals()
        call job.launch()
        call GuiSpecialEffectSystem_Reset()
    endfunction

    private function GuiSfx_Init takes nothing returns nothing
        set HT = InitHashtable()
    endfunction

endlibrary

Example:
  • Then - Actions
    • -------- Heal for total damage accumulated --------
    • Unit - Set life of WF_Caster[WF_TempInteger] to ((Life of WF_Caster[WF_TempInteger]) + WF_TotalDMG[WF_TempInteger])
    • -------- --------- --------
    • Special Effect - Create a special effect attached to the origin of WF_Caster[WF_TempInteger] using Heal Green.mdx
    • Special Effect - Set Scale of (Last created special effect) to 0.60
    • Special Effect - Destroy (Last created special effect)
    • -------- --------- --------
    • Set SFX_ModelPath = animeslashfinal.mdl
    • Set SFX_CreatePoint = ((Position of WF_Caster[WF_TempInteger]) offset by 80.00 towards (Facing of WF_Caster[WF_TempInteger]) degrees.)
    • Set SFX_RemovePoint = True
    • Set SFX_DestroyDelay = 1.00
    • Set SFX_Scale = 1.50
    • Set SFX_Green = 0
    • Set SFX_Red = 255
    • Set SFX_Height = 80.00
    • Set SFX_Blue = 0
    • Set SFX_MoveOutBeforeDestroy = False
    • Set SFX_Pitch = (Random real number between -20.00 and 20.00)
    • Set SFX_Yaw = ((Facing of WF_Caster[WF_TempInteger]) + (Random real number between -10.00 and 10.00))
    • Trigger - Run SES Run <gen> (ignoring conditions)
    • -------- --------- --------
    • Special Effect - Create a special effect attached to the origin of DamageEventTarget using Abilities\Spells\Other\Volcano\VolcanoMissile.mdl
    • Special Effect - Destroy (Last created special effect)
    • -------- knockback --------
    • Set KBA_Caster = WF_Caster[WF_TempInteger]
    • Set KBA_TargetUnit = DamageEventTarget
    • Set KBA_Level = 1
    • Set KBA_Speed = 12.00
    • Set KBA_DistancePerLevel = 300.00
    • Set KBA_DestroyTrees = False
    • Set WF_TempP = (Position of WF_Caster[WF_TempInteger])
    • Set KBA_StartingPosition = WF_TempP
    • Set KBA_SpecialEffects[1] = Abilities\Spells\Other\Volcano\VolcanoMissile.mdl
    • Set KBA_SpecialEffects[2] = Abilities\Spells\Human\FlakCannons\FlakTarget.mdl
    • Trigger - Run Cast A Knockback <gen> (checking conditions)
  • -------------
  • sfxTTest
    • Events
      • Time - Elapsed game time is 10.00 seconds
    • Conditions
    • Actions
      • Set SFX_ModelPath = Armor Stimulus Orange.mdl
      • Set SFX_CreatePoint = (Center of Rect 039 <gen>)
      • Set SFX_DestroyDelay = 6.00
      • Set SFX_RemovePoint = True
      • Set SFX_AnimationType = 3
      • Set SFX_AnimationPeriod = 0.50
      • Trigger - Run SES Run <gen> (ignoring conditions)



sfxTest.gif
 
Last edited:
Back
Top