• 🏆 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!

[vJass] Damage over Time

Status
Not open for further replies.
Level 13
Joined
Mar 19, 2010
Messages
870
Hi hivers,

is there a leakless and with good performance Damage over Time System written in jass/vJass out there?

I checked the Spell section and there is one from Dynasti but in some comments the people said it's not good. Has someone a good implemented version?

Best Reg.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Well... one without buffs is very easy to make:
JASS:
library DamageOverTime uses TimerIndex
    
    globals
        
        constant real   DOT_INTERVAL_DURATION   = 0.1
        
        unit array          DOT_Source
        widget array        DOT_Target
        real array          DOT_Damage
        real array          DOT_Duration
        attacktype array    DOT_AttackType
        damagetype array    DOT_DamageType
        weapontype array    DOT_WeaponType
        
    endglobals
    
    function DamageOverTime_Callback takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local integer id = GetTimerData(t)
        
        if DOT_Duration[id] < DOT_INTERVAL_DURATION then
            call UnitDamageTarget(DOT_Source[id], DOT_Target[id], DOT_Damage[id] * (DOT_Duration[id] / DOT_INTERVAL_DURATION), true, false, DOT_AttackType[id], DOT_DamageType[id], DOT_WeaponType[id])
            call ReleaseIndexedTimer(t)
            set DOT_Source[id] = null
            set DOT_Target[id] = null
            set DOT_Damage[id] = 0
            set DOT_Duration[id] = 0
            set DOT_AttackType[id] = null
            set DOT_DamageType[id] = null
            set DOT_WeaponType[id] = null
        else
            call UnitDamageTarget(DOT_Source[id], DOT_Target[id], DOT_Damage[id], true, false, DOT_AttackType[id], DOT_DamageType[id], DOT_WeaponType[id])
            set DOT_Duration[id] = DOT_Duration[id] - DOT_INTERVAL_DURATION
            if DOT_Duration[id] == 0 then
                call ReleaseIndexedTimer(t)
                set DOT_Source[id] = null
                set DOT_Target[id] = null
                set DOT_Damage[id] = 0
                set DOT_Duration[id] = 0
                set DOT_AttackType[id] = null
                set DOT_DamageType[id] = null
                set DOT_WeaponType[id] = null
            endif
        endif
        
        set t = null
    endfunction
    function ApplyDamageOverTime takes unit source, widget target, real totalAmount, real duration, attacktype at, damagetype dt, weapontype wt returns nothing
        local timer t = NewIndexedTimer()
        local integer id = GetTimerData(t)
        
        set DOT_Source[id] = source
        set DOT_Target[id] = target
        set DOT_Damage[id] = totalAmount * DOT_INTERVAL_DURATION / duration
        set DOT_Duration[id] = duration
        set DOT_AttackType[id] = at
        set DOT_DamageType[id] = dt
        set DOT_WeaponType[id] = wt
        
        call TimerStart(t, DOT_INTERVAL_DURATION, true, function DamageOverTime_Callback)
        set t = null
    endfunction
    
endlibrary

This should work... I havent tested it though.
You require a unit indexer, I recommend Bribe's.
You require TimerUtils, which is also a very basic resource.
And you require TimerIndex, which is something I wrote simply to make a generic index number.

Modified Bribe's Unit Indexer:
JASS:
//  
// This is the most important function - it provides an index for units as they enter the map
//  
function IndexUnit takes unit whichUnit returns boolean
    local integer pdex = udg_UDex
    //  
    // You can use the boolean UnitIndexerEnabled to protect some of your undesirable units from being indexed
    // - Example:
    // -- Set UnitIndexerEnabled = False
    // -- Unit - Create 1 Dummy for (Triggering player) at TempLoc facing 0.00 degrees
    // -- Set UnitIndexerEnabled = True
    //  
    // You can also customize the following block - if conditions are false the (Matching unit) won't be indexed.
    //  
    if udg_UnitIndexerEnabled then
        //  
        // Generate a unique integer index for this unit
        //  
        if udg_UDexRecycle == 0 then
            set udg_UDex = udg_UDexGen +1
            set udg_UDexGen = udg_UDex
        else
            set udg_UDex = udg_UDexRecycle
            set udg_UDexRecycle = udg_UDexNext[udg_UDex]
        endif
        //  
        // Link index to unit, unit to index
        //  
        set udg_UDexUnits[udg_UDex] = whichUnit
        call SetUnitUserData(udg_UDexUnits[udg_UDex], udg_UDex)
        //  
        // Use a doubly-linked list to store all active indexes
        //  
        set udg_UDexPrev[udg_UDexNext[0]] = udg_UDex
        set udg_UDexNext[udg_UDex] = udg_UDexNext[0]
        set udg_UDexNext[0] = udg_UDex
        //  
        // Fire index event for UDex
        //  
        set udg_UnitIndexEvent = 0.00
        set udg_UnitIndexEvent = 1.00
        set udg_UnitIndexEvent = 0.00
        set udg_UDex = pdex
    endif
    return false
endfunction

//  
// The next function is called each time a unit enters the map
//  
function IndexNewUnit takes nothing returns boolean
    local integer pdex = udg_UDex
    local integer ndex
    local unit u
    //  
    // Recycle indices of units no longer in-play every (15) units created
    //  
    set udg_UDexWasted = udg_UDexWasted +1
    if udg_UDexWasted == 15 then
        set udg_UDexWasted = 0
        set udg_UDex = udg_UDexNext[0]
        loop
            exitwhen udg_UDex == 0
            if GetUnitUserData(udg_UDexUnits[udg_UDex]) == 0 then
                //  
                // Remove index from linked list
                //  
                set ndex = udg_UDexNext[udg_UDex]
                set udg_UDexNext[udg_UDexPrev[udg_UDex]] = ndex
                set udg_UDexPrev[ndex] = udg_UDexPrev[udg_UDex]
                set udg_UDexPrev[udg_UDex] = 0
                //  
                // Fire deindex event for UDex
                //  
                set udg_UnitIndexEvent = 2.00
                set udg_UnitIndexEvent = 0.00
                //  
                // Recycle the index for later use
                //  
                set udg_UDexUnits[udg_UDex] = null
                set udg_UDexNext[udg_UDex] = udg_UDexRecycle
                set udg_UDexRecycle = udg_UDex
                set udg_UDex = ndex
            else
                set udg_UDex = udg_UDexNext[udg_UDex]
            endif
        endloop
        set udg_UDex = pdex
    endif
    
    //  
    // Handle the entering unit (Matching unit)
    // 
    set u = GetFilterUnit()
    if GetUnitUserData(u) == 0 then
        call IndexUnit(u)
    endif
    set u = null
    return false
endfunction
    //  
    // The next function initializes the core of the system
    //  
function InitializeUnitIndexer takes nothing returns nothing
    local integer i = 0
    local region re = CreateRegion()
    local rect r = GetWorldBounds()
    local unit FoG
    local group g = CreateGroup()
    
    set udg_UnitIndexerEnabled = true
    call RegionAddRect(re, r)
    call TriggerRegisterEnterRegion(CreateTrigger(), re, Filter(function IndexNewUnit))
    call RemoveRect(r)
    set re = null
    set r = null
    
    loop
        call GroupEnumUnitsOfPlayer(g, Player(i), null)
        loop
            set FoG = FirstOfGroup(g)
            exitwhen FoG == null
            call GroupRemoveUnit(g, FoG)
            
            call IndexUnit(FoG)
        endloop
        
        set i = i + 1
        exitwhen i == 16
    endloop
    call DestroyGroup(g)
    set g = null
    call DestroyTimer(GetExpiredTimer())
    
    //  
    // This is the "Unit Indexer Initialized" event, use it instead of "Map Initialization" for best results
    //  
    set udg_UnitIndexEvent = 3.00
    set udg_UnitIndexEvent = 0.00
endfunction

//===========================================================================
function InitTrig_Unit_Indexer_JASS takes nothing returns nothing
    call TimerStart(CreateTimer(), 0.01, false, function InitializeUnitIndexer)
endfunction
TimerUtils
JASS:
library TimerUtils initializer init
//*********************************************************************
//* TimerUtils (red+blue+orange flavors for 1.24b+) 2.0
//* ----------
//*
//*  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)
//* set t=NewTimerEx(x)   : Get a timer (alternative to CreateTimer), call
//*                            Initialize timer data as x, instead of 0.
//*
//* 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      = true
        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.
        
        private boolean       didinit = false
    endglobals
    private keyword init

    //==========================================================================================
    // I needed to decide between duplicating code ignoring the "Once and only once" rule
    // and using the ugly textmacros. I guess textmacros won.
    //
    //! textmacro TIMERUTIS_PRIVATE_NewTimerCommon takes VALUE
    // On second thought, no.
    //! endtextmacro

    function NewTimerEx takes integer value returns timer
        if (tN==0) then
            if (not didinit) then 
                //This extra if shouldn't represent a major performance drawback
                //because QUANTITY rule is not supposed to be broken every day. 
                call init.evaluate()
                set tN = tN - 1
            else
                //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")
                set tT[0]=CreateTimer()
                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")
                    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
            endif
        else
            set tN=tN-1
        endif
        call SetTimerData(tT[tN],value)
     return tT[tN]
    endfunction
    
    function NewTimer takes nothing returns timer
        return NewTimerEx(0)
    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
        if ( didinit ) then
            return
        else
            set didinit = true
        endif
     
        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
TimerIndex
JASS:
library TimerIndex uses TimerUtils
//*********************************************************************
//* TimerIndex 1.0
//* ----------
//*
//*  This library creates a unique index for the TimerData of timerUtils.
//*  A timer that is created with an index must also be released as an indexed timer.
//*
    
    globals
        integer udg_NextTimerIndex = 0
        boolean array udg_TimerIndex_Occupied
    endglobals
    
    function ReleaseIndexedTimer takes timer t returns nothing
        set udg_TimerIndex_Occupied[GetTimerData(t)] = false
        call ReleaseTimer(t)
    endfunction
    function NewIndexedTimer takes nothing returns timer
        loop
            set udg_NextTimerIndex = udg_NextTimerIndex + 1
            if udg_NextTimerIndex > 8190 then
                set udg_NextTimerIndex = 1
            endif
            
            exitwhen not udg_TimerIndex_Occupied[udg_NextTimerIndex]
        endloop
        
        set udg_TimerIndex_Occupied[udg_NextTimerIndex] = true
        return NewTimerEx(udg_NextTimerIndex)
    endfunction
    
endlibrary
 
Level 13
Joined
Mar 19, 2010
Messages
870
I use a DoT system which creates a Timer for each target and when you damage lot of units at the same time the game lags because of too many started timers with an interval of 0.1s.

Here is it...click

Isn't it not better to use only 1 Timer which loops over all targets?

As i can see u do it the same way... for each target 1 timer.... You know what i mean?
 
DoT damage is really cheap, and that system does a fine approach. If it is causing frame loss when applied to multiple units, you should look into the effects you've used (e.g. do they have high particle counts?).

In JASS, the significant performance drops are almost always from rendering. :) If you remove the effects from the DoT, I'm pretty sure you won't get any significant fps drop (test it and see!). Sadly, the wc3 engine is out of our control so we simply have to optimize for it (avoid too many effects, avoid effects with high particle count, avoid creating units/destructables unnecessarily or too often, etc.).
 
Status
Not open for further replies.
Top