HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

Damage Detection System Optimization

12-10-2007, 09:17 PM#1
Karawasa
I'm using the DDS below for a TD that I am making. The problem is that it is causing creeps (non-structure units) to stutter. This stuttering occurs only when creeps are taking damage (getting attacked or being hit by spells) leading me to believe that the DDS is simply causing too much commotion for the map. I am coming here for help in optimizing the code in anyway possible, thereby reducing the commotion and increasing performance.

Collapse JASS:
library DamageDetection initializer DamDetect_Init, needs Tables, CasterSystem

//******************************************************************************************
//*
//* emjlr3's Damage Detection Engine
//*
//*
//*  A fairly simple system that allows you to detect when units
//*  in your map take damage, and do with them what you want.  Use
//*  the function DamDetect_Attacks and DamDetect_Spells to que your
//*  functions to be ran at attack or other damage.
//*
//*  Requires:
//*    - "emjlr3s CS" setup correctly in your map
//*    - The dummy orb ability in this map and its current buff updated in your map
//*    - This trigger, ofcourse
//*
//******************************************************************************************

//=============================================================================================================================
    //  Configuration :
    //
    globals
        // Your dummy orb abilities rawcode
        private constant integer     DamDetect_Abil       = 'A004'
      
        // Your dummy orb abilities buffs rawcode
        private constant integer     DamDetect_Buff       = 'B000'
  
    endglobals

//=============================================================================================================================
//  Damage Detection Engine functions.  Don't mess down here unless you know what you are doing!!!

globals
    //needed globals:
    private integer AttackCount = 0
    private integer SpellCount = 0
    private string array Attacks
    private string array Spells
    private string DamageType
  
    private unit U
endglobals


//**********Main function to run the functions that need to be ran for each damage type
function DamDetect_Damaged takes nothing returns boolean
    local integer i = 1
    local string funcName
    set U = GetTriggerUnit()
  
    if GetEventDamage()>.1 then  
        if GetUnitAbilityLevel(U, DamDetect_Buff) > 0 then    
            call UnitRemoveAbility(U, DamDetect_Buff)      
            set DamageType = "attack"
            loop
                exitwhen i > AttackCount
                call ExecuteFunc(Attacks[i])
                set i = i + 1
            endloop
        else      
            set DamageType = "spell"      
            loop
                exitwhen i > SpellCount
                call ExecuteFunc(Spells[i])
                set i = i + 1
            endloop
        endif
    endif
    
    return false
endfunction
//Returns the damage cause, incase you need to know
function DamDetect_GetDamageEventCause takes nothing returns string
    return DamageType
endfunction
//Use this to ignore the damage done by the unit, avoids infinite loops in some cases
function DamDetect_Dummy takes unit u, string whichState returns nothing
    if whichState=="ignore" then
        call SetTableInt("DamDetect_"+I2S(CS_H2I(u)),"DamDetect_Dummy",1)      
    elseif whichState=="allow" then
        call SetTableInt("DamDetect_"+I2S(CS_H2I(u)),"DamDetect_Dummy",0)      
    endif
endfunction  

//**********Sets up the triggers to run when a unit takes damage, call either of these in the triggers init trigger
//for it to run when a unit takes damage
function DamDetect_Attacks takes string funcName returns nothing
    //used for attack damage
    set AttackCount = AttackCount + 1
  
    set Attacks[AttackCount] = funcName  
endfunction
function DamDetect_Spells takes string funcName returns nothing
    //used for spell damage
    set SpellCount = SpellCount + 1
  
    set Spells[SpellCount] = funcName  
endfunction  

//**********Base functions for the system
//Filter for units you do not want to be checked to add or remove the ability when they enter the map, or when they die
function DamDetect_Filter takes nothing returns boolean
    set U = GetTriggerUnit()
    if GetUnitTypeId(U)==CasterUnitId then
        return false
    endif
    if GetUnitTypeId(U)=='n02G' or GetUnitTypeId(U)=='e006' or GetUnitTypeId(U)=='e005' or GetUnitTypeId(U)=='h03J' then
        return false
    endif
    return true
endfunction
//Add damage triggers for units who enter map
function DamDetect_AddTriggers takes nothing returns nothing
    local trigger dam_trig = CreateTrigger()
    set U = GetTriggerUnit()
  
    if IsUnitType(U,UNIT_TYPE_STRUCTURE)==true then
        call UnitAddAbility(U, DamDetect_Abil)
        call UnitMakeAbilityPermanent(U,true,DamDetect_Abil)
    else
        call TriggerRegisterUnitEvent(dam_trig, U,EVENT_UNIT_DAMAGED)  
        call TriggerAddCondition(dam_trig,Condition(function DamDetect_Damaged))
        call SetTableObject("DamDetect_"+I2S(CS_H2I(U)),"dam_trig",dam_trig)  
    endif
  
    set dam_trig = null
endfunction
//Remove triggers from units who die
function DamDetect_RemoveTriggers takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local string s
  
    if GetWidgetLife(u)<.405 and GetUnitAbilityLevel(u,'A00U')==0 and IsUnitType(u,UNIT_TYPE_STRUCTURE)==false then
        set s = "DamDetect_"+I2S(CS_H2I(u))
        call DestroyTrigger(GetTableTrigger(s,"dam_trig"))
        call FlushStoredMission(cscache,s)
        set u = null
        return
    endif
endfunction  
function DamDetect_Upgrades takes nothing returns nothing
    local integer id
  
    set U = GetTriggerUnit()
    set id = GetUnitTypeId(U)
  
    if id=='h00X' or id=='h01A' or id=='h01B' or id=='h01C' then //etc., for a tower that does not currently have the ability, add it
        call UnitAddAbility(U, DamDetect_Abil)
        call UnitMakeAbilityPermanent(U,true,DamDetect_Abil)
    endif
  
    if id=='h002' or id=='h00Y' or id=='h02X' or id=='h012' or id=='h014' or id=='h02F' or id=='h02P' or id=='n02G' then //etc., for a tower that has it, but we do not want it to anymore
        call UnitMakeAbilityPermanent(U,false,DamDetect_Abil)
        call UnitRemoveAbility(U,DamDetect_Abil)
    endif  
endfunction
//Basically just sets up the system for all units preplaced that may be used,
//for units that ever enter the map, and removes triggers when units die
function DamDetect_Init takes nothing returns nothing
    local trigger entmap_trig = CreateTrigger()
    local trigger death_trig = CreateTrigger()  
    local trigger upg_trig = CreateTrigger()
    local group g = CreateGroup()
  
    call GroupEnumUnitsInRect(g,bj_mapInitialPlayableArea,Condition(function DamDetect_Filter))
    call ForGroup(g,function DamDetect_AddTriggers)
  
    call TriggerRegisterEnterRectSimple(entmap_trig, bj_mapInitialPlayableArea)
    call TriggerAddCondition(entmap_trig,Condition(function DamDetect_Filter))
    call TriggerAddAction(entmap_trig,function DamDetect_AddTriggers)

    call TriggerRegisterAnyUnitEventBJ(death_trig,EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(death_trig,Condition(function DamDetect_Filter))
    call TriggerAddAction(death_trig,function DamDetect_RemoveTriggers)
  
    call TriggerRegisterAnyUnitEventBJ(upg_trig,EVENT_PLAYER_UNIT_UPGRADE_FINISH)
    call TriggerAddAction(upg_trig,function DamDetect_Upgrades)  
  
    set entmap_trig = null
    set death_trig = null
    set upg_trig = null
    call DestroyGroup(g)
    set g = null
endfunction

endlibrary

Thanks in advance.
12-10-2007, 09:40 PM#2
Toadcop
well i have quickly look through your code.
and... it's allmost max optimized. you can use EvaluateTrigger() instead of ExecuteFunc() this also will bust performance. but in whole really nice.

ahhh ^^ and well you could DON'T use gc =) it will also inc perf. (really much)

// do i need to rewrite the code for you or something ? =)
12-10-2007, 09:52 PM#3
Karawasa
If there is a way of accomplishing the exact same thing (i.e. all the functionality) but with a different method (without GC for instance) that increases performance significantly, that would be very much welcomed.

This isn't my code, it was written by emjlr3. Thus, if someone could rewrite the code (since I do not know how) with the more efficient method, that would be greatly appreciated. Just in case, I should mention that emjlr3 will not mind if you do so. The code would only be used in my map, so it will not be an issue.

Normally I wouldn't ask such a favor, but this trigger is fundamental to the map, and so optimizing it will really help out a lot of things.
12-10-2007, 10:09 PM#4
Toadcop
ok tommorow or something i will send you a PM...
gl ^^

+ i will write my sys =) (from scratch) // funcs
12-10-2007, 10:14 PM#5
cohadar
Maybe use this?
12-10-2007, 10:30 PM#6
Karawasa
Quote:
Originally Posted by cohadar

Your system doesn't have all the functionality that emjlr3's does (although I'm sure it is highly optimized). For instance, your system does not differentiate between physical and spell damage. Could it be used to trigger percent chance on attack abilities (not the crappy GUI, when an attack goes through, so stop spam abuse is impossible)?
12-10-2007, 10:47 PM#7
rain9441
You are calling ExecuteFunc("") N times every single time a unit takes damage, where N = number of different types of "orb effects"/"spell effects". Not good. Consider using an array of function interfaces or an array of interfaces which you extend and contain an onDamage function. These in turn call TriggerEvaluate/TriggerExecute (I believe) on premade triggers, which is more practical. This will boost performance drastically

Unrelated:
You are removing the trigger which fires when a unit takes damage when the unit dies. A) not all units die, B) Units come back to life (reincarnation, heroes being reawakened, raise dead, ..., ..., ...) One call to RemoveUnit and you are leaking a trigger.

Why create 1 trigger per unit in the game? Why not have one global trigger and add UnitTakesDamage events to that trigger as units enter the game?
12-10-2007, 10:57 PM#8
cohadar
Quote:
Originally Posted by Karawasa
Your system doesn't have all the functionality that emjlr3's does (although I'm sure it is highly optimized). For instance, your system does not differentiate between physical and spell damage. Could it be used to trigger percent chance on attack abilities (not the crappy GUI, when an attack goes through, so stop spam abuse is impossible)?

Differentiating damage types is overrated.
It is also ugly because it requires you to add hidden orb effect attach to all units on the map.

Better way is to use a simple OnDamage system and orb abilities only on units for witch you know that need differentiating.
12-10-2007, 11:19 PM#9
PandaMine
PUI is just a method of 'attaching' stuff to units that is much more efficient then GC, you can still do stuff like detecting what type of damage
12-11-2007, 01:02 AM#10
emjlr3
make sure you do not remove units from the game, this only clears our leaks when units die, so it is imperative that they do

btw, after looking over it again, I now see that I am creating the damage trigger for all units who enter map, but only set it up and store it on units, thus this will leak for all towers, this will not stop lag when units are damaged, but it may help in the long run, forgot to update that when we changed it to only add trigger to units and orb effects to towers

that needs to be fixed, removing the GC check in the damage trigger will work IMO, as I see you have done, to help a bit, other then that though, I see no other real way, other then same crazy algorithm I know nothing about

Quote:
You are calling ExecuteFunc("") N times every single time a unit takes damage, where N = number of different types of "orb effects"/"spell effects". Not good. Consider using an array of function interfaces or an array of interfaces which you extend and contain an onDamage function. These in turn call TriggerEvaluate/TriggerExecute (I believe) on premade triggers, which is more practical. This will boost performance drastically

Unrelated:
You are removing the trigger which fires when a unit takes damage when the unit dies. A) not all units die, B) Units come back to life (reincarnation, heroes being reawakened, raise dead, ..., ..., ...) One call to RemoveUnit and you are leaking a trigger.

Why create 1 trigger per unit in the game? Why not have one global trigger and add UnitTakesDamage events to that trigger as units enter the game?

i do not understand that method
with that one global trigger, would the events not add up or some shit and lag..., i remember reading about that somewhere, or something like that, and could adding the even that a unit takes damage, twice, with the same unit index, with different actual units, botch it somehow?

maybe Grim can make some better way, or something, the limited GC is not the cause of any lag, btw
12-11-2007, 01:12 AM#11
cohadar
Did I mention that PUI is able to clean up stuff even if you remove units?
12-11-2007, 02:02 AM#12
rain9441
Actually, Karawasa, for your purposes there may be a way to speed it up from O(n) to O(1).

Limitations: All unit TYPES will have specific onDamage actions, there will be no single unit onDamage actions (ie wont work for items and such, all of unit X will have the same abilities). I assume this works for you because its a TD? I suppose with some heavy additions you COULD make this work on a per unit basis but, thats alot more work i wont get into right now.

Implementation:
In the Damaged function (called when unit takes damage), instead of looping through an array of size N and ExecuteFunc/TriggerEvaluate the array elements, you build a hash table key value pair of unit type->function. How to do this? Simple: Gamecache.

Create a trigger.
Add your trigger to an array (we'll assume it is index #4)
Add a condition to your trigger (the condition you want to run when unit takes damage).
Store the index into gamecache with key of the unit type.

In your OnUnitTakesDamage trigger...

set trg = MyTriggerArray[GetStoredInteger(cache, "OnDamage", I2S(GetUnitTypeId(GetEventDamageSource()))]
call TriggerEvaluate(trg)

Some code that might work, havent tried compiling, but gets you the jist of it.
Collapse JASS:
globals
    integer AttackCount = 1 // 0 should be null
    trigger array Attacks
endglobals

function AddDamageEvent takes integer iUnitType, code func returns nothing
    local integer index = AttackCount
    local trigger trg

    set AttackCount = AttackCount + 1

    set trg = CreateTrigger()
    set Attacks[index] = trg
    call TriggerAddCondition(trg, Condition(func))
    call StoreInteger(cscache, "OnDamageHash", I2S(iUnitType), index)
endfunction

function DamDetect_Damaged takes nothing returns boolean
    local integer index
    local unit uDamaged = GetTriggerUnit()
    local unit uDamager = GetEventDamageSource()
  
    if GetEventDamage()>.1 and uDamager != null and GetUnitTypeId(uDamager) != 0 then
        set index = GetStoredInteger(cscache, "OnDamageHash", I2S(GetUnitTypeId(uDamager)))
        if ( index > 0 ) then
            call TriggerEvaluate(Attacks[index])
        endif
    endif
    
    return false
endfunction

Regardless of what anyone tells you, a single gamecache call is going to be faster than a linear array loop. You can now have 8000 different onDamage effects with very little penalty to speed.

Just make sure any code you pass to AddDamageEvent is a valid Condition (takes nothing returns boolean) or you'll desync macs.

Whats really interesting is this actually will work with an orb of lightning 100% chance to acid bomb (buff being your global buff variable)! Splash orbs? I dunno :P
12-11-2007, 02:19 AM#13
cohadar
That is a very nice idea rain9441, good thinking,
of course limitations are just too grate for a general purpose onDamage sys.

But maybe a hybrid version of that might do the trick.
12-11-2007, 06:13 AM#14
grim001
I threw together this DamageDetection system when I read this thread to demonstrate how to use structs to create an O(1) system with per-unit events.

It uses UnitUserData, but it is easy enough to edit and replace with your attachment system of choice. For a TD map this should be the best way to make use of UnitUserData though, you won't get a bigger performance boost using it elsewhere.

EDIT: attachment removed, updated version in my next post.
12-11-2007, 08:59 AM#15
cohadar
Ok lets begin this optimization process.
You will need to install PUI in your map.

Are you are using UnitUserData for something in your map?
If you are you will have to replace it with PUI

Expand JASS:

We will proceed to the next step when you get this working.
Attached Files
File type: w3xETDD_v1.0.w3x (23.0 KB)