1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Participate in Blizzard's Public Test Realm to give them feedback on the upcoming patches. Info is here.
    Dismiss Notice
  3. Take part in forum poll and decide the winner of Icon Contest #16!
    Dismiss Notice
  4. Congratulate the winners of the first ATC contest!
    Dismiss Notice
  5. Zwiebelchen is hosting a special UI texturing contest. Whomever wins will get a $150 reward through paypal! Come along and draw your texturing tools for the Fantastic Adventurer UI contest.
    Dismiss Notice
  6. Sneak, pickpocket and assassinate! Create a stealth map in Mini-Mapping Contest #14!
    Dismiss Notice
  7. Music Contest #8 - Hive Soundtrack is up! Create the soundtrack for the upcoming videos of Hive Workshop's YouTube Channel.
    Dismiss Notice

[System] DestructableHider

Discussion in 'JASS Resources' started by Zwiebelchen, Jul 19, 2012.

  1. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Code (vJASS):
    library DestructableHider initializer init

        /*
            by Zwiebelchen      v1.3
       
                Destructables create an enormous amount of overhead on warcraft III maps, almost the same as units, especially walkable destructables.
                Thus, a large amount of destructables creates a huge drop of FPS in the game, even on fast machines, due to the poor engine of WC3.
                This effect is fairly noticable at an amount of even less than 1000 destructables, which is reached very fast when using invisible platforms.
                Warcraft III automaticly hides units outside the screen to save performance, however it does not do so for destructables, for unknown reasons.
               
                The purpose of this fully automatic system is to hide all those destructables, that are currently not viewed anyway, to save a lot of processing time.
                To do that, the entire map is splitted into tiles of an editable size and all destructables within those tiles are stored into a table, to allow fast access.
                When a tile is viewed, all destructables on adjacent tiles will also be shown, so that moving the camera doesnt create ugly popup effects when the center of the view is on the edge of a tile.
               
                However, there are some rules you need to consider, in order to make your map work without desyncs in multiplayer:
                - never hide destructables units or players can interact with (attackable, selectable or destructable)
                - hiding destructables that block pathing is safe, as hiding the destructable will not change its pathing
                - hiding destructables that need to be enumed is safe; hidden destructables can be enumerated
                - hiding walkable platforms is also safe, as long as you dont get a location Z for a location placed on that destructable globally; this is not 100% safe anyway, as
                  the returned value of GetLocationZ() is dependant on the render state of the destructable
               
               
                API:
                       
                    private function filt returns boolean
                        - add custom filter code for the automatic enumeration of destructables on map init inside
                        - if all destructables should be added to the system, let it return true
                        - example:
                            private function filt returns boolean
                                return GetDestructableMaxLife(GetFilterDestructable()) == 1
                            endfunction
                            -> automaticly adds all destructables on the map with a maximum life of 1 on map init to the system
               
               
                Optional:
               
                    public function register takes destructable returns nothing
                        - adds a destructable to the system, also hides/shows the destructable depending on the position of the camera
                   
                    public function unregister takes destructable returns nothing
                        - removes a destructable from the system, also unhides the destructable in case it was hidden
               
               
        */

       
    globals
        //==== CONFIGURABLES ====
        private constant real INTERVAL = 0.1 //Update interval in seconds.
                                             //[in multiplayer, the camera positions will only get updated every 0.05-0.1 seconds, so setting it to a lower value than 0.05 makes no sense]
                                             //[update frequency can be much higher in single player mode!]
        private constant integer DRAW_DISTANCE = 512 //the radius around the camera target in which the tiles are considered visible; should be about the same as sight radius (not diameter) of the camera; for 3d cams, use the FarZ value
                                                     //Use multiples of 1024 for maximum efficiency on square division. Recommended value: 5120
        private constant integer TILE_RESOLUTION = 4 //amount of tiles spread over DRAW_DISTANCE
                                            //- higher resolution = more overhead to incrementing loop variables, but less amounts of destructables checked when moving the camera
                                            //- lower resolution = less overhead to incrementing loop variables, but higher amounts of destructables checked when moving the camera
                                            //-> Recommended value: 8-12
        //==== END OF CONFIGURABLES ====
       
        private hashtable hash = InitHashtable()
        private integer columns = 0
        private integer rows = 0
        private integer lastrow = 0
        private integer lastcolumn = 0
        private integer lastid = 0
        private real mapMinX = 0
        private real mapMinY = 0
        private constant integer TILESIZE = DRAW_DISTANCE/TILE_RESOLUTION
    endglobals

    private function filt takes nothing returns boolean
        //Add code for the enum filter of the automatic registration of destructables on map init
       
        //example:
        //return GetDestructableMaxLife(GetFilterDestructable()) == 1
        //-> automaticly adds all destructables on the map with a maximum life of 1 on map init to the system
        return true
    endfunction

    public function register takes destructable d returns nothing
        local integer id = R2I((GetDestructableY(d)-mapMinY)/TILESIZE)*columns + R2I((GetDestructableX(d)-mapMinX)/TILESIZE)
        local integer count = LoadInteger(hash, id, 0)+1
        call SaveInteger(hash, id, 0, count)
        call SaveDestructableHandle(hash, id, count, d)
        call ShowDestructable(d, LoadBoolean(hash, id, -1)) //match visibility state
        call SaveInteger(hash, GetHandleId(d), 0, count) //store the list position for fast lookup
    endfunction

    public function unregister takes destructable d returns nothing
        local integer id = R2I((GetDestructableY(d)-mapMinY)/TILESIZE)*columns + R2I((GetDestructableX(d)-mapMinX)/TILESIZE)
        local integer count = LoadInteger(hash, id, 0)
        local integer a = LoadInteger(hash, GetHandleId(d), 0)
        local destructable temp
        if a < count then //move the last in list up to this slot
            set temp = LoadDestructableHandle(hash, id, count)
            call SaveDestructableHandle(hash, id, a, temp)
            call SaveInteger(hash, GetHandleId(temp), 0, a) //update list position
            set temp = null
        endif
        call RemoveSavedHandle(hash, id, count) //clean up the deserted slot
        call SaveInteger(hash, id, 0, count-1)
        call FlushChildHashtable(hash, GetHandleId(d)) //clean up list position
        call ShowDestructable(d, true) //make sure its shown again in case it was hidden
    endfunction

    private function autoregister takes nothing returns nothing
        local destructable d = GetEnumDestructable()
        local integer id = R2I((GetDestructableY(d)-mapMinY)/TILESIZE)*columns + R2I((GetDestructableX(d)-mapMinX)/TILESIZE)
        local integer count = LoadInteger(hash, id, 0)+1
        call SaveInteger(hash, id, 0, count)
        call SaveDestructableHandle(hash, id, count, d)
        call ShowDestructable(d, false) //initially hide everything
        call SaveInteger(hash, GetHandleId(d), 0, count) //store the list position for fast lookup
        set d = null
    endfunction

    private function EnumGrid takes integer x1, integer x2, integer y1, integer y2, boolean show returns nothing
        local integer a = x1
        local integer b
        local integer j
        local integer id
        local integer count
        loop
            set b = y1
            exitwhen a > x2
            loop
                exitwhen b > y2
                set id = b*columns+a
                call SaveBoolean(hash, id, -1, show)
                set count = LoadInteger(hash, id, 0)
                set j = 0
                loop
                    exitwhen j >= count
                    set j = j + 1
                    call ShowDestructable(LoadDestructableHandle(hash, id, j), show)
                endloop
                set b = b + 1
            endloop
            set a = a + 1
        endloop
    endfunction

    private function ChangeTiles takes integer r, integer c, integer lr, integer lc returns nothing
        local integer AminX = c-TILE_RESOLUTION
        local integer AmaxX = c+TILE_RESOLUTION
        local integer AminY = r-TILE_RESOLUTION
        local integer AmaxY = r+TILE_RESOLUTION
        local integer BminX = lc-TILE_RESOLUTION
        local integer BmaxX = lc+TILE_RESOLUTION
        local integer BminY = lr-TILE_RESOLUTION
        local integer BmaxY = lr+TILE_RESOLUTION
        //border safety:
        if AminX < 0 then
            set AminX = 0
        endif
        if AminY < 0 then
            set AminY = 0
        endif
        if BminX < 0 then
            set BminX = 0
        endif
        if BminY < 0 then
            set BminY = 0
        endif
        if AmaxX >= columns then
            set AmaxX = columns-1
        endif
        if AmaxY >= rows then
            set AmaxX = rows-1
        endif
        if BmaxX >= columns then
            set BmaxX = columns-1
        endif
        if BmaxY >= rows then
            set BmaxX = rows-1
        endif
       
        if BmaxX < AminX or AmaxX < BminX or BmaxY < AminY or AmaxY < BminY then
            call EnumGrid(AminX, AmaxX, AminY, AmaxY, true)
            call EnumGrid(BminX, BmaxX, BminY, BmaxY, false)
        else
            if c >= lc then
                if c != lc then
                    call EnumGrid(BmaxX+1, AmaxX, AminY, AmaxY, true)
                    call EnumGrid(BminX, AminX-1, BminY, BmaxY, false)
                endif
                if AminY < BminY then
                    call EnumGrid(AminX, BmaxX, AmaxY+1, BmaxY, false)
                    call EnumGrid(AminX, BmaxX, AminY, BminY-1, true)
                elseif BminY < AminY then
                    call EnumGrid(AminX, BmaxX, BmaxY+1, AmaxY, true)
                    call EnumGrid(AminX, BmaxX, BminY, AminY-1, false)
                endif
            else
                call EnumGrid(AminX, BminX-1, AminY, AmaxY, true)
                call EnumGrid(AmaxX+1, BmaxX, BminY, BmaxY, false)
                if AminY < BminY then
                    call EnumGrid(BminX, AmaxX, AminY, BminY-1, true)
                    call EnumGrid(BminX, AmaxX, AmaxY+1, BmaxY, false)
                elseif BminY < AminY then
                    call EnumGrid(BminX, AmaxX, BminY, AminY-1, false)
                    call EnumGrid(BminX, AmaxX, BmaxY+1, AmaxY, true)
                endif
            endif
        endif
    endfunction

    private function periodic takes nothing returns nothing
        local integer row = R2I((GetCameraTargetPositionY()-mapMinY)/TILESIZE)
        local integer column = R2I((GetCameraTargetPositionX()-mapMinX)/TILESIZE)
        local integer id = row*columns + column
        if id == lastid then //only check for tiles if the camera has left the last tile
            return
        endif
        call ChangeTiles(row, column, lastrow, lastcolumn)
        set lastrow = row
        set lastcolumn = column
        set lastid = id
    endfunction

    private function init takes nothing returns nothing
        set mapMinX = GetRectMinX(bj_mapInitialPlayableArea)
        set mapMinY = GetRectMinY(bj_mapInitialPlayableArea)
        set lastrow = R2I((GetCameraTargetPositionY()-mapMinY)/TILESIZE)
        set lastcolumn = R2I((GetCameraTargetPositionX()-mapMinX)/TILESIZE)
        set rows = R2I((GetRectMaxY(bj_mapInitialPlayableArea)-mapMinY)/TILESIZE)+1
        set columns = R2I((GetRectMaxX(bj_mapInitialPlayableArea)-mapMinX)/TILESIZE)+1
        if lastcolumn <= columns/2 then //to make sure the game starts with a full make-visible enum of all destructables on screen
            set lastcolumn = columns-1
        else
            set lastcolumn = 0
        endif
        if lastrow <= rows/2 then
            set lastrow = rows-1
        else
            set lastrow = 0
        endif
        set lastid = lastrow*columns + lastcolumn
        call EnumDestructablesInRect(bj_mapInitialPlayableArea, Filter(function filt), function autoregister)
        call TimerStart(CreateTimer(), INTERVAL, true, function periodic)
        call periodic() //to make sure the destructables on screen after the map loading process finishes are initially shown
    endfunction

    endlibrary


    EDIT:
    I forgot to change the documentation inside the demo map. Hiding destructables that block pathing is SAFE! Don't worry about it, the pathing is not affected at all!
     

    Attached Files:

    Last edited: May 7, 2013
  2. PurgeandFire

    PurgeandFire

    Code Moderator

    Joined:
    Nov 11, 2006
    Messages:
    7,214
    Resources:
    5
    Icons:
    1
    Spells:
    4
    Resources:
    5
    Ah, so that is the method you were talking about. I was considering something similar, using rects. However, I see that you hashed them to avoid enumeration (so you don't need rects), gj creative.

    However, for a system like this we'll probably need someone to run some benchmarks with /fps. (of any kind, it doesn't necessarily have to be a stress-test) I tried doing something similar to this on a map with like 5000 invisible destructables and the results were kinda so-so, they were usually within 3-4 fps of not having the system at all (either above or below). Although, your technique most likely has a lot less overhead since it doesn't constantly enumerate, so the results may be different. :)

    Good job overall though.
     
  3. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    That was basically what i have in mind.
    That's probably enough in the current state but you could even improve by using a MAX number : if the number of not hidden destructables is lower than this constant number, then you don't have to bother to hide destructables.
     
  4. Magtheridon96

    Magtheridon96

    Joined:
    Dec 12, 2008
    Messages:
    6,017
    Resources:
    9
    Maps:
    1
    Spells:
    8
    Resources:
    9
    I'd totally recommend changing things like this:
    i+1
    to this:
    i + 1
    because it makes the system a bit more readable :p

    edit
    Placing line breaks in between blocks of code (if blocks, loops, etc..) would also make things more readable.
     
  5. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Updated to v1.1; fixed a small bug that caused the last center tile not to hide correctly.
    Also fixed comments (south is north and north is south, due to the tiles starting from the bottom left corner of the map, not the top left corner). Also added a debug mode counter that shows how many destructables got hidden and shown each update.
     
  6. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Just to prove what this thing is capable of:

    I implemented it in Gaias Retaliation, edge size 6144, 0.05s ticks.
    Almost 900 destructables, 750 of them platforms. 480x256 map size, 50.000 doodads.

    The system only hides those destructables, nothing else. This is what happens in the opening scene (you know, when you spawn your level 1 hero at the inn):

    Left picture: without the system. Right picture: with the system. Note that those values are pretty stable, with a fluctuation of less than +/- 3 fps on my machine. Also, what this doesn't show: without the system, there are short random freezes due to fps drop spikes. With the system activated, this is totally gone; camera movement is 100% smooth, no fps spikes at all.

    [​IMG]

    For rpgs, this thing is nothing less than a miracle.

    I don't think that would actually be neccesary. This thing sucks almost no performance at all, as it only updates if you leave the current tile you are looking at. And everything is simple maths operations. No powers, no squareroots, no handles.
    Well I could improve it by splitting the hide and show process over seperate ticks/threads. The amount of processing time would be the same, but it evens out over time better than the current approach. Not that anyone would ever notice a difference...

    EDIT:
    I also did some further testing. It seems that the pathing of hidden destructables remains intact, so this is safe to use on pathing blockers aswell.
     

    Attached Files:

    Last edited: Jul 19, 2012
  7. PurgeandFire

    PurgeandFire

    Code Moderator

    Joined:
    Nov 11, 2006
    Messages:
    7,214
    Resources:
    5
    Icons:
    1
    Spells:
    4
    Resources:
    5
    Nice!

    Does this work in multiplayer?
     
  8. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    You just asked for moar performance, now just like i said, it should be enough in the current state.

    Also, you should probably give a way to create/remove destructables in game with custom functions (hooks are not enough)
     
  9. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    It works fine - as long as you exclude targetable destructables.

    You can always do so by using ordinary natives. No need for custom functions here.
     
  10. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    No, destructables are "picked up" only once, on init.
    The destructables which are created later are not considered, plus destructables which are removed are not removed of the lists.
     
  11. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Hmm, correct. Then again, when would you ever need that? I can probably write an unregister function and make the register function public, if you wish.
     
  12. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    Don't ask me, i hardly open the editor these times, and since months or even years in fact.

    It's just something easy to add and could be useful.

    Oh and btw it should be a mistake but the function "register" is public, while it should be private.
     
  13. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Well, at first I wanted to add register and unregister functions as public functions so that people can manually add the destructables they like. Then I came up with the maxlife check idea so it became obsolete. Looks like it could be useful again.
     
  14. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    If i would had made a such resource i would use a config function where you manually put each rawcode of destructable.
    Now using directly the object editor, i'm just not sure the max life is an appropriate choice, i mean if you open the map (especially if it's since a while you have not opened it) that wouldn't be obvious.
    The script way seems more reliable imho, but maybe more annoying, idk.
     
  15. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    Sure, it might not be a clean method to set up the registry, but it's very elegant and efficient to do so. Well, if I leave it to the player to register/unregister the destructables, there's no need for this anymore anyway.
     
  16. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    What do you mean by "set up the registry" ?

    I think there is a need to this automatic behavior, just because i suppose most of these destructables if not all would be "preplaced" most of times.
    The reason i suggest a way to handle destructables which are created/removed later is just because you can, not because i think about a useful case.
     
  17. Magtheridon96

    Magtheridon96

    Joined:
    Dec 12, 2008
    Messages:
    6,017
    Resources:
    9
    Maps:
    1
    Spells:
    8
    Resources:
    9
    The max life thing is the only thing I hate here :/

    Registering types would be much better in my opinion ^_^
    Registering specific destructables /could/ be useful.

    edit
    register shouldn't be a public function, you should have a function called RegisterDestructableType or something that takes a raw code and another function called RegisterDestructable that takes a specific destructable.

    Your current register function could be changed to take a destructable so that you can make your enum function call register(GetEnumDestructable()).
    You wouldn't notice a speed difference because these are done on map init.
     
  18. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,667
    Resources:
    6
    Models:
    4
    Maps:
    1
    Spells:
    1
    Resources:
    6
    UPDATE.
    Added a register and unregister function.

    I like it for how it avoids useless registry masses. Simply click the destructable you like in the object editor a change a value. Easy as that. Much better than having annoying long registry lists, if you ask me - especially as you can't change the maxlife of a destructable ingame anyway, so it's kind of a static value that never changes throughout the game and thus relyable.

    Also, its way more GUI friendly. And I think that should be a valid point, as the system is full-automatic and because of that interesting for GUIers.
     
  19. Troll-Brain

    Troll-Brain

    Joined:
    Apr 27, 2008
    Messages:
    2,372
    Resources:
    0
    Resources:
    0
    It's still some kind of "magic" value, i mean the link is not obvious.
    Plus, you won't see quickly which ones are considered or not (unless you don't change the max life for other destructables).
    But meh i won't argue more about it.

    EDIT : Aha the GUI argument, the worst one ever :p
     
  20. Magtheridon96

    Magtheridon96

    Joined:
    Dec 12, 2008
    Messages:
    6,017
    Resources:
    9
    Maps:
    1
    Spells:
    8
    Resources:
    9
    The deadliest one too.

    How about you add a vJASS version for people Nes and I who like to do things the hard way? :3

    edit
    Then again, people like Nes and I can write these things ourselves =o

    edit
    I would totally rename the constants if I were you. CONSTANTS_ARE_WRITTEN_LIKE_THIS by convention =o