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

[System] DummyUnitStack

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,531
DummyUnitStack

Preface

This system is designed to accommodate spell designers who want to use spam-loads of effects, are trying to use dummy casters but static global ones aren't behaving properly, and anything else you can think of for having pre-loaded dummy units on the fly.

Design Explanation

A dummy unit stack is a simple concept that has been executed before. Indeed, caching dummy units was a primary design paradigm in Vexorian's xe. But what if you don't want all that extra garbage? DummyUnitStack is as simple as it gets. It preloads you some units, you request them when you want them, and release them when you're done. DummyUnitStack isn't safe - you have to obey some simple rules when you use it, and it isn't perfect - there are some corners cut in its design in order to squeeze every bit of performance out possible.

The FPS drop associated with unit counts is complex, and depends on many factors including model poly counts, animation complexity, quantity on screen, and quantity on map. By preventing some of these factors we create a ton of units without FPS drop.

Limitations:

  • This resource requires vJass to compile.
  • This resource is not idiot-proof. You must not release a unit to the stack that doesn't belong there, and you must destroy anything attached to the dummy handle before releasing it yourself. Some attributes like owner, scale, fly height, and position are reset.
  • * The stack design pattern implies that getting a unit handle might have an effect attached to it whose death animation is still playing (you can see this in the test map) - at some point I'd like to write a linked-list implementation which removes the problem, and compare the performance. My instinct is that the linked list would perform at most 10% worse, and the benefits would be worth-while.
  • The unit facing angles are not stored in the system and cannot be changed instantly, so for large effects whose angle is important, this can lead to bad visual effects.

API

local unit u = DummyUnitStack.get()

call DummyUnitStack.release(u)

No more explanation necessary. If you want to fiddle with the tweaker settings, you're on your own :)

The script
JASS:
//*
//* DummyUnitStack is a library designed to give spell developers immediate
//* access to any number of dummy units at any time by hacking around the
//* engine limitations associated with CreateUnit() - essentially, all we do
//* is create units in a deferred manner which not only caches dummies, but
//* also prevents simultaneous unit instantiations on a more global scale.
//* In other words, we create a few units, very often, until we have a lot.
//*
//* The public API for DummyUnitStack is simple - just get and release:
//*		local unit u = DummyUnitStack.get()
//*		call DummyUnitStack.release(u)
//*
//* The more complex part is the preloading options available. For most cases
//* the default options will more than suffice, but if you really want to get
//* aggressive with your optimizations, you can do some maths yourself and
//* decide on every limit used in the system.
//*
//* Enjoy,
//*		-Cokemonkey11
//*
library DummyUnitStack
	// Just a shim for the API, the struct is not actually instanciated.
    struct DummyUnitStack extends array
	
	
		//*********************************************************************
		//* CUSTOMIZABLE SECTION
		//*********************************************************************
		
		// This bit is important - make sure this refers to the dummy unit
		// in the test map
        private static constant integer DUMMY_UNIT_ID='u000'
		
		// Whenever the preloading process turns on, we set a minimum number
		// of units to load, to prevent turning the system on and off too
		// often - here you can choose the minimum count (default 10)
        private static constant integer MIN_PRELOAD_BLOCK_SIZE=10
        
		// This value is the initialization preload value. Higher numbers
		// will increase map load time, but give the best performance. The
		// ideal value should be something between the number of dummies used
		// in the first few minutes, and the maximum number of dummies used
		// in theory. Additionally, you can turn the feature off entirely.
		// (default 200)
        private static constant integer PRELOAD_INIT_COUNT=200
        private static constant boolean DO_PIC=true
        
		// When a series of dummies are retrived using .get(), the stack size
		// falls and approaches 0 - with this setting, we can turn on the
		// preloader whenever the stack size falls to an arbitrary number.
		// Again, this feature can be turned off. (default 20)
        private static constant integer DYNAMIC_DEFERRED_PRELOAD_COUNT=20
        private static constant boolean DO_DDPC=true
        
		// When the map starts, the preloader turns on right away, preloading
		// more units - this can be tweaked to reduce loading time without
		// losing dummies available at the beginning of the game. (default 20)
        private static constant integer INITIAL_DEFERRED_PRELOAD_COUNT=20
        private static constant boolean DO_IDPC=true
        
		// If a dummy unit is released and the stack exceeds this size, the unit
		// will instead be removed.
        private static constant integer MAX_PRELOADED_UNITS=5000
        private static constant boolean DO_MPU=true
        
		// Here we have another "hack-like" design choice. The developer must
		// specify an (x,y) coordinate pair where dummy units can be moved on
		// release - we do this because there is a bug associaed with ShowUnit()
		// and the locust ability. :(
        private static constant real SAFE_LOC_X=-3580.
        private static constant real SAFE_LOC_Y=3460.
		
		// Normally a not-so-useful tweaking variable, clock period in dummy unit
		// stack is actually quite powerful because we're not relying on any
		// kind of smooth movement for releasing/getting. (default 1./60.)
        private static constant real CLOCK_PERIOD=1./60.
		
		//*********************************************************************
		//* END CUSTOMIZABLE SECTION
		//*********************************************************************
		
		
        private static unit array dummyStack
        private static integer stackIndex=-1
        private static integer deferredTodoCount=0
        private static timer clock=CreateTimer()
        
		//* Private method used for creating a unit by the system
        private static method add takes boolean preloading returns unit
            set bj_lastCreatedUnit = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE),thistype.DUMMY_UNIT_ID,thistype.SAFE_LOC_X,thistype.SAFE_LOC_Y,0.)
            call UnitAddAbility(bj_lastCreatedUnit,'Aloc')
            call UnitAddAbility(bj_lastCreatedUnit,'Arav')
            if preloading then
                set thistype.stackIndex=thistype.stackIndex+1
                set thistype.dummyStack[thistype.stackIndex]=bj_lastCreatedUnit
            else
                return bj_lastCreatedUnit
            endif
            return null
        endmethod
        
		//* Private periodic method used for preloading
        private static method deferredPreloader takes nothing returns nothing
            set thistype.deferredTodoCount=thistype.deferredTodoCount-1
            call thistype.add(true)
            if thistype.deferredTodoCount<1 then
                call PauseTimer(thistype.clock)
            endif
        endmethod
        
		//* Public method used to get a dummy. Use this at any time, as much
		//* as you want.
        public static method get takes nothing returns unit
            local unit result
            if thistype.stackIndex!=-1 then
                set thistype.stackIndex=thistype.stackIndex-1
                set result=thistype.dummyStack[thistype.stackIndex+1]
            else
                set result=thistype.add(false)
            endif
            static if DO_DDPC then
                if thistype.stackIndex<(thistype.DYNAMIC_DEFERRED_PRELOAD_COUNT-1) and thistype.deferredTodoCount<1 then
                    set thistype.deferredTodoCount=thistype.MIN_PRELOAD_BLOCK_SIZE
                    call TimerStart(thistype.clock,thistype.CLOCK_PERIOD,true,function thistype.deferredPreloader)
                endif
            endif
            return result
        endmethod
        
		//* Public method for releasing a dummy. Make sure you don't release
		//* units that don't come from DummyUnitStack. Make sure you destroy
		//* effects and anything else attached the unit because its handle
		//* will be RECYCLED.
        public static method release takes unit u returns nothing
            static if DO_MPU then
                if thistype.stackIndex>=thistype.MAX_PRELOADED_UNITS then
                    call RemoveUnit(u)
                    return
                endif
            endif
            call SetUnitFlyHeight(u,0.,0.)
            call SetUnitScale(u,1.,1.,1.)
            call SetUnitOwner(u,Player(PLAYER_NEUTRAL_PASSIVE),true)
            call SetUnitX(u,thistype.SAFE_LOC_X)
            call SetUnitY(u,thistype.SAFE_LOC_Y)
            set thistype.stackIndex=thistype.stackIndex+1
            set thistype.dummyStack[thistype.stackIndex]=u
        endmethod
        
		//* herpderp #programming
        private static method onInit takes nothing returns nothing
            local integer index=1
            static if DO_PIC then
                loop
                    exitwhen index>thistype.PRELOAD_INIT_COUNT
                    call thistype.add(true)
                    set index=index+1
                endloop
            endif
            static if DO_IDPC then
                set thistype.deferredTodoCount=thistype.INITIAL_DEFERRED_PRELOAD_COUNT
                call TimerStart(thistype.clock,thistype.CLOCK_PERIOD,true,function thistype.deferredPreloader)
            endif
        endmethod
    endstruct
endlibrary

Installation
  1. Copy the dummy unit from the test map
  2. Copy this script in its entirety into an empty trigger (custom script) and name it DummyUnitStack.
  3. (If necessary) adjust the 'CUSTOMIZABLE SECTION' to your liking.

Change Log

2014.01.10 - Initial submission to Hive: Jass Resources: Submissions

Special Thanks:

  • -Kobas- for creating a map submission template, which turned out useful for system submissions as well.
  • Vexorian for developing JassHelper. Without vJass, I almost certainly would not still be scripting for wc3. He also designed xe and the dummy.mdx used in this map, which I appreciate, etc.
  • The various developers of JNGP including PitzerMike and MindworX. Integrating JassHelper and TESH is a godsend.
 

Attachments

  • (1)DummyUnitStack001.w3x
    32.5 KB · Views: 165
Last edited:
Yes but it's sad that you don't change the angle of the units.
So this cannot be used by any missile engine so far.

You should not forget that it's impossible to make an unit face to a specific angle instantly. It's always with delay. That's why Nes' built his library to calculate which is the unit with the closest angle from what is searched.
 

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,531
Yes but it's sad that you don't change the angle of the units.
So this cannot be used by any missile engine so far.

Yes, I forgot about this limitation - I'll add it to the original post. It can still be used for missile engine (I do), but for large effects it will look quite bad.

You should not forget that it's impossible to make an unit face to a specific angle instantly. It's always with delay. That's why Nes' built his library to calculate which is the unit with the closest angle from what is searched.

I didn't know Nes made that. Did it perform well? (eg, did nestharus keep making it or get rid of it)

You can change the angle of effects using Dummy.mdx by iNfraNe.

http://www.wc3c.net/showpost.php?p=802916&postcount=3

Yes, dummy.mdl is used in the test map, but this will not allow you to (easily) change the facing angle instantly. Also, the angle of attack animation set is limited to 180 degrees of rotation.
 
Do you mean the release method should have set u = null at the end, and add method should have set u = null above return null ? That might be leaking, I'm not sure to be honest.

It does prevent the handle ID from being recycled (the reference count will continue to go up), but it shouldn't matter since you're recycling the dummies. It only matters when you kill/remove the unit (when MAX_PRELOADED_UNITS is reached). If you want to fix it, you would just use globals for "result" and "u" instead of the locals.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Dummy organized units by angle and returned the unit for an angle in O(1) ;). The behavior is similar to a minimum binary heap, in which the angle with the least amount of units is at the top, but unlike a binary heap, it does all of its operations in O(1) instead of O(log n).

However, I'm just happy that people are submitting resources again :)
 

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,531
It does prevent the handle ID from being recycled (the reference count will continue to go up), but it shouldn't matter since you're recycling the dummies. It only matters when you kill/remove the unit (when MAX_PRELOADED_UNITS is reached). If you want to fix it, you would just use globals for "result" and "u" instead of the locals.

Would the following not also suffice?

JASS:
        public static method release takes unit u returns nothing
            static if DO_MPU then
                if thistype.stackIndex>=thistype.MAX_PRELOADED_UNITS then
                    call RemoveUnit(u)
                    set u = null //this line was added
                    return
                endif
            endif
            call SetUnitFlyHeight(u,0.,0.)
            call SetUnitScale(u,1.,1.,1.)
            call SetUnitOwner(u,Player(PLAYER_NEUTRAL_PASSIVE),true)
            call SetUnitX(u,thistype.SAFE_LOC_X)
            call SetUnitY(u,thistype.SAFE_LOC_Y)
            set thistype.stackIndex=thistype.stackIndex+1
            set thistype.dummyStack[thistype.stackIndex]=u
            set u = null //this line was added
        endmethod

along with

JASS:
        private static method add takes boolean preloading returns unit
            local unit u=CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE),thistype.DUMMY_UNIT_ID,thistype.SAFE_LOC_X,thistype.SAFE_LOC_Y,0.)
            call UnitAddAbility(u,'Aloc')
            call UnitAddAbility(u,'Arav')
            if preloading then
                set thistype.stackIndex=thistype.stackIndex+1
                set thistype.dummyStack[thistype.stackIndex]=u
            else
                return u
            endif
            set u = null //this line added
            return null
        endmethod

Dummy organized units by angle and returned the unit for an angle in O(1) ;). The behavior is similar to a minimum binary heap, in which the angle with the least amount of units is at the top, but unlike a binary heap, it does all of its operations in O(1) instead of O(log n).

I'm not really interested in the time complexity of the system (try analyzing DummyUnitStack), I'm more interested in the amortized and maximum comparisons between the system and the baseline (CreateUnit).

In other words, how much faster is your system, in practice, than CreateUnit() ? How about compared to DummyUnitStack?

Also, if you use UnitIndexer, it's worthless to me
 

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,531
For .add(), it doesn't null it before return u, so the reference count will go up.

Are you sure the reference is not simply sent to the parent function? I mean, you're positive this would leak?

I'm not trying to argue, just trying to understand before I fix it so I don't need to do multiple edits.

Thanks,
 

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,531
Thanks guys, I've updated it.

Can someone check the release method and verify for me that this won't cause any problems if someone does:

JASS:
call DummyUnitStack.release(u)
call DestroyEffect(AddSpecialEffectTarget(path,u,"overhead"))

In other words, does the reference passed as an argument to release get nulled, or is it copied inernally?
 
Top