• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Trackables and custom UIs

Status
Not open for further replies.
Level 19
Joined
Mar 18, 2012
Messages
1,716
I want to share a small algorithm I'm using successfully since some time.

It concerns the trackable handle, more specifically a wrapper type to trackables named Track.

Trackable objects can only be created, not destroyed. Also they are if used in a more advanced way player based objects.

Normally only one custom UI can be opened per player. Hence if that UI
has a MUI design ( Inventory, Skill tree, ... ) and the UI is 100% similar
for all units ( buttons on the same position ) only one set of trackables
has to be used for all units of a player.

I'm using a tile size of 128 like it is defined in the blizzard.j,
but actually you can lower it which can be useful if you use 4x4 or 8x8 trackables.

Also a TableArray[MAX_PLAYERS] would be enough instead of a seperate hashtable.

Example: Using this algorithm, my custom inventory requires ~50 trackables for 1 unit of player 1.
And also needs only ~50 trackables for 1000 units of player 1.
Without this design it would be 50000 trackables.

JASS:
        //**
        //*  Track search & save:
        //*  ====================
        //*
        //* Trackables are poorly supported in Warcraft III. In fact we can't
        //* remove handles of type "trackable" once they are no longer used.
        //* Therefore I came up with an algorithm to search out of "all ever created Tracks in UIWindows"
        //* these which match 100% to the one we wish to create and use that Track instance instead.
        //*
        //*  library TileDefinition creates unique integers per tile in your map.
        //* Tracks are saved in stacks on the tile id in an hashtable.
        //* With an 64x64 track model the loop will exit after (1 - 3) * players loops.
        //* For bigger model it will be 1 * players, for smallers the loopup time will increase.
        //*
        //* Due to an hashtable lookup during both trackable events ( hover & click ),
        //* a Track instance can evaluate any UIWindow onHover/onClick stub method without
        //* running into code collision with other UIs which use the same Track instance.
        
        //*  Credits to IcemanBo:
        //*  ====================
        static if not LIBRARY_TileDefinition then
            private static integer WorldTiles_X = 0
            private static integer WorldTiles_Y = 0
            private static method setVars takes nothing returns nothing
                set WorldTiles_X = R2I(WorldBounds.maxX - WorldBounds.minX) / 128 + 1
                set WorldTiles_Y = R2I(WorldBounds.maxY - WorldBounds.minY) / 128 + 1
            endmethod
            private static method GetTileId takes real x, real y returns integer
                local integer xI = R2I(x - WorldBounds.minX + 64) / 128
                local integer yI = R2I(y - WorldBounds.minY + 64) / 128
                if ((xI < 0) or (xI >= .WorldTiles_X) or (yI < 0) or (yI >= .WorldTiles_Y)) then
                    return -1
                endif
                return (yI * .WorldTiles_X + xI)
            endmethod
        endif
        
        private method saveTrack takes Track track returns Track
            local integer id   = GetTileId(track.x, track.y)
            local integer size = LoadInteger(TRACKER, id, 0) + 1
            call SaveInteger(TRACKER, id,  size, track) 
            call SaveInteger(TRACKER, id, -size, userId) 
            call SaveInteger(TRACKER, id,  0,    size) 
            return track
        endmethod
        
        private method searchTrack takes string model, real x, real y, real z, real face returns Track
            local integer id   = GetTileId(x, y)
            local integer size = LoadInteger(TRACKER, id, 0)
            local Track t  
            loop
                exitwhen 0 == size
                if (LoadInteger(TRACKER, id, -size) == userId) then
                    set t = LoadInteger(TRACKER, id, size)
                    if (t.x == x) and (t.y == y) and (t.z == z) and (t.facing == face) and (t.model == model) then
                        return t
                    endif
                endif
                set size = size - 1
            endloop
            return saveTrack(Track.createForPlayer(model, x, y, z, face, user))
        endmethod

Edit: Here is the onHover and onClick interface for better understanding.
JASS:
        //*  Stub methods:
        //*  =============
        public stub method onHover    takes UIScreen whichScreen, UIButton hovered returns nothing
        endmethod
        public stub method onClick    takes UIScreen whichScreen, UIButton clicked returns nothing
        endmethod

        //*  UIButton API:
        //*  =============
        //*  Adds an UIButton to your custom UI. The function uses a search & save algorithm for Tracks
        //* ( read above ) to optimize the trackable handle count in your map.
        method addDestButton takes string model, real x, real y, real z, real face, integer objectId, real scale returns UIButton
            local integer typeId = getType()
            local integer size   = getButtonCount()
            local Track track    = searchTrack(model, originX + x, originY + y, z, face)  
            debug call ThrowError(invalid, "UIWindow", "addButton", "invalid", this, "Can't add further UIButtons, once method addPages is called!")
            set cells[size]      = UIButton.createDest(typeId, user, objectId, track, scale*screen.scale, false)
            set cells[TABLE_SIZE]= size + 1
            set pageSize         = size + 1
            call SaveInteger(TABLE, screen, -track, this)
            call SaveInteger(TABLE, screen,  track, size)
            return cells[size]
        endmethod
        
        method addImageButton takes string model, real x, real y, real z, real face, string file, real scale returns UIButton
            local integer typeId = getType()
            local integer size   = getButtonCount()
            local Track track    = searchTrack(model, originX + x, originY + y, z, face)       
            debug call ThrowError(invalid, "UIWindow", "addButton", "invalid", this, "Can't add further UIButtons, once method addPages is called!")
            set cells[size]      = UIButton.createImg(typeId, user, file, track, scale*screen.scale, false)
            set cells[TABLE_SIZE]= size + 1
            set pageSize         = size + 1
            call SaveInteger(TABLE, screen, -track, this)
            call SaveInteger(TABLE, screen,  track, size)
            return cells[size]
        endmethod
        
        //*  Trackable events:
        //*  =================
        private static method onAnyHover takes nothing returns boolean
            local UIScreen UI      = GetPlayerUI(Track.tracker)
            local thistype this    = LoadInteger(TABLE, UI, -Track.instance)
            local integer  index   = LoadInteger(TABLE, UI,  Track.instance) + (pageSize*(page - 1))
            local UIButton hovered = cells[index]
            if (this != 0) and (hovered.enabled) then
                call onHover(UI, hovered)
            endif      
            return false
        endmethod
        
        private static method onAnyClick takes nothing returns boolean
            local UIScreen UI      = GetPlayerUI(Track.tracker)
            local thistype this    = LoadInteger(TABLE, UI, -Track.instance)
            local integer  index   = LoadInteger(TABLE, UI,  Track.instance) + (pageSize*(page - 1)) 
            local UIButton clicked = cells[index]
            if (0 != this) and (clicked.enabled) then
                call onClick(UI, clicked)
                set lastClick = UI_GetElapsedTime()
            endif      
            return false
        endmethod
        
        private static method init takes nothing returns nothing
            call Track.registerAnyClick(function thistype.onAnyClick)
            call Track.registerAnyHover(function thistype.onAnyHover)
            static if not LIBRARY_TileDefinition then
                call thistype.setVars()
            endif
        endmethod
        implement UIInit
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
"my custom inventory requires ~50 trackables for 1 unit of player 1.
And also needs only ~50 trackables for 1000 units of player 1.
Without this design it would be 50000 trackables. "

(I take it that you want a single player map... 600k would be a better assumption as neutrals dont care about trackables.)
That means you have a terrible inventory.

The UI of the inventory would be the same for all units, maybe the borders, background, icons, etc would be slightly different, but apart from that, nothing should be different at all.
The button to exit the window should be on the same place, the inventory should be on the same place, etc, etc, etc.
So you would only need trackables for the command buttons (exit, drop, buy/sell, learn, upgrade, etc) and trackables for the items in your inventory/tree.
That will mean that with your "reasonable" 50k trackables, you want to have an inventory of 49,972 items at least... and that on the screen at the same time.
If you can scroll, you can have multiple items on the same place (same trackable).

Even if you dont do that, you can make a tilegrid of trackables of size 8x8 and tile the entire window.
So a 1024 by 768 window would have 12288 trackables at MAX... that... for all windows you will EVER have in your entire game... it is like a workaround for "GetMousePosition" or a "MouseMoved" event in addition to a pretty accurate "MouseClick" event.
It does require quite a different kind of data structure to create everything but it can make pretty cool things.

So... the idea is good, and execution nice, but it needs better a reason ;)
 
Status
Not open for further replies.
Top