• 🏆 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] Efficient use of dummy units

Status
Not open for further replies.
Level 10
Joined
Jun 6, 2007
Messages
392
Hello! I've been wondering what's the most efficient way to create spells, that require a lot of dummy units. I'll give an example:

The caster channels a spell. Every 0.33 seconds, a projectile is shot at random direction damaging enemies in the way. When the projectile reaches its destination, it explodes and shoots different projectiles at 5 directions.

With my current style of making spells, this would require 3 structs: One for the spell and 2 for dummy units. These structs must contain partly same information (for example caster). I believe there's a better way to do this. Is there any good model to make this kind of spells?
 
Level 10
Joined
Jun 6, 2007
Messages
392
I haven't made that spell, and I'm not actually even planning to. I only used it as an example, because it would require different types of dummy units.

I usually have 1 or 2 triggers per spell, one that triggers on cast, and another to detect stop/finish (if required).
 
As Maker said, it depends on how similar the projectiles are. In your case, though, I'd choose 3 structs. One for the spell (0.33 timer), one to create a projectile (created every 0.33 sec), and one to shoot the five projectiles.

The way I see it, the other approach would be to have 2 structs, and have the projectile struct call itself when it needs to shoot the five projectiles. However, the problem I see is that they have two different end behaviors--one is supposed to explode into 5 projectiles while the others are just supposed to go to the end of their paths. Now, it is certainly possible to code this with some nifty booleans and whatnot, but it is very messy.

I'd choose 3 structs just because they are neater. You'd have a much better time.
 
Level 10
Joined
Jun 6, 2007
Messages
392
Alright, thanks, that clarified it.

Edit: I made an example spell. It's basically like a channelled volcano: the caster throws x rocks every y seconds around him. This is my first attempt to use methods, instead of using structs only for data storage, the spell is based of Magtheridon96's spell model. I feel like I'm over complicating things, so I'd like to know, how to make the spell more efficient. It can get quite laggy, but I don't know if it's because of the coding or caused by tha number of effects. Here's the trigger:
JASS:
scope Eruption

    struct Dummy

        private static constant integer UNIT_CODE = 'h00J'
        private static constant real AREA = 700
        private static constant real SPEED = 1.7
        private static constant real HEIGHT = 600
        private static constant string LANDING_EFFECT = "Abilities\\Spells\\Orc\\LiquidFire\\Liquidfire.mdl"

        private thistype next
        private thistype prev

        private static integer count = 0

        //dummy unit
        private unit dummy
        //the distance the dummy unit is about to travel
        private real dist
        //already travelled distance
        private real travelled
        private real dx
        private real dy
        private unit caster
        private real damage

        private method destroy takes nothing returns nothing
            call this.deallocate()
            set this.next.prev = this.prev
            set this.prev.next = this.next
            set count = count - 1
        endmethod

        static method move takes nothing returns boolean
    
            local thistype this = thistype(0).next
            local real x
            local real y
            local group g
            local unit u
            loop
                exitwhen this == 0
            
                //move dummy units
                call SetUnitX(this.dummy, GetUnitX(this.dummy) + this.dx)
                call SetUnitY(this.dummy, GetUnitY(this.dummy) + this.dy)
                call SetUnitFlyHeight(this.dummy, Parabola(this.dist, HEIGHT, this.travelled), 0)
                set this.travelled = this.travelled + SquareRoot(this.dx*this.dx + this.dy*this.dy)        
                set x = GetUnitX(this.dummy)
                set y = GetUnitY(this.dummy)
            
                //if dummy unit has reached its destination, destroy it and damage area
                if this.travelled > this.dist then
                    call DestroyEffect(AddSpecialEffect(LANDING_EFFECT, x, y))
                    set g = CreateGroup()
                    call GroupEnumUnitsInRange(g, x, y, 100, null)
                    loop
                        set u = FirstOfGroup(g)
                        exitwhen u == null
                        if IsUnitEnemy(u, GetOwningPlayer(this.caster))  and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and GetUnitState(u, UNIT_STATE_LIFE) > 0 then
                            call UnitDamageTarget(this.caster, u, this.damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
                        endif  
                        call GroupRemoveUnit(g, u)
                    endloop
                    call DestroyGroup(g)
            
                    call KillUnit(this.dummy)
                    call this.destroy()
                endif
            
                set this = this.next
            
            endloop
        
            set u = null
            set g = null
        
            //return true if number of dummy units is 0
            return count == 0
    
        endmethod
    
        static method new takes unit caster, real x, real y, real damage returns nothing
            local thistype this = thistype.allocate()
            local real angle
            set this.next = 0
            set this.prev = thistype(0).prev
            set thistype(0).prev.next = this
            set thistype(0).prev = this
            set count = count + 1

            set angle = GetRandomReal(0, 2*bj_PI)
            set this.dist = GetRandomReal(10, AREA)
            set this.dummy = CreateUnit(GetOwningPlayer(caster), UNIT_CODE, x, y, angle)
            set this.travelled = 0
            set this.damage = damage
            set this.caster = caster
            set this.dx = Cos(angle) * this.dist * SPEED * 0.01
            set this.dy = Sin(angle) * this.dist * SPEED * 0.01
        endmethod
    endstruct



    struct Spell

        private static constant integer ABIL_CODE = 'A02E'

        private static constant real TIMEOUT = 0.04
        //dummy units will be created every Nth iteration
        private static constant integer DUMMY_INTERVAL = 20
        //number of dummy units created at time
        private static constant integer DUMMY_NUMBER = 10

        private thistype next
        private thistype prev

        private static timer iterator = CreateTimer()
        private static integer count = 0

        private unit caster
        private real damage
        private boolean channelling = true
        private integer dummyInterval = 0
        private real x
        private real y

        private method destroy takes nothing returns nothing
            call this.deallocate()
            set this.next.prev = this.prev
            set this.prev.next = this.next
            set count = count - 1
            set this.caster = null
        endmethod

        private static method periodic takes nothing returns nothing
            local thistype this = thistype(0).next
            local boolean spellEnd = false
            local integer i
            loop
                exitwhen this == 0
            
                //Once every (DUMMY_INTERVAL*TIMEOUT), create DUMMY_NUMBER dummy units
                if this.dummyInterval == DUMMY_INTERVAL then
                    set i = 0
                    loop
                        exitwhen i == DUMMY_NUMBER
                        call Dummy.new(this.caster, this.x, this.y, this.damage)
                        set i = i + 1
                    endloop
                    set this.dummyInterval = 0
                else
                    set this.dummyInterval = this.dummyInterval + 1
                endif
            
                if not this.channelling then
                    call this.destroy()
                endif
            
                set this = this.next
            
            endloop
        
            //move all dummy units
            set spellEnd = Dummy.move()
        
            //if there are no dummy units and no units casting the spell, stop timer
            if spellEnd and count == 0 then
                call PauseTimer(iterator)
            endif
        
        endmethod

        private static method run takes nothing returns nothing
            local thistype this = thistype.allocate()
        
            set this.next = 0
            set this.prev = thistype(0).prev
            set thistype(0).prev.next = this
            set thistype(0).prev = this
            set count = count + 1
            if count == 1 then
                call TimerStart(iterator, TIMEOUT, true, function thistype.periodic)
            endif

            set this.caster = GetTriggerUnit()
            set this.damage = (1000 + GetUnitState(this.caster, UNIT_STATE_LIFE)) * (2 + GetUnitAbilityLevel(GetTriggerUnit(), ABIL_CODE)) / 100
            set this.x = GetUnitX(this.caster)
            set this.y = GetUnitY(this.caster)
        endmethod

        private static method stop takes nothing returns nothing
        local thistype this = thistype(0).next
            loop
                exitwhen GetTriggerUnit() == this.caster
                set this = this.next
            endloop
            set this.channelling = false
        endmethod
    
        private static method conditions takes nothing returns boolean
            if GetSpellAbilityId() == ABIL_CODE then
                return true
            endif
            return false
        endmethod
    
        private static method onInit takes nothing returns nothing
            local trigger t = CreateTrigger()
            local trigger t2 = CreateTrigger()        
            call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
            call TriggerAddCondition(t, Condition(function thistype.conditions))
            call TriggerAddAction(t, function thistype.run)
        
            call TriggerRegisterAnyUnitEventBJ(t2, EVENT_PLAYER_UNIT_SPELL_FINISH)
            call TriggerRegisterAnyUnitEventBJ(t2, EVENT_PLAYER_UNIT_SPELL_ENDCAST)
            call TriggerAddCondition(t2, Condition(function thistype.conditions))
            call TriggerAddAction(t2, function thistype.stop)
            set t = null
            set t2 = null
        endmethod
    endstruct

endscope

The parabola function is from my math library:
JASS:
    //height is the max height of the parabola, and dist is the distance between roots
    function Parabola takes real dist, real height, real x returns real
        local real a = 4*height/dist
        return a*x*(-x/(dist)+1)
    endfunction
 
Last edited:
Level 14
Joined
Nov 18, 2007
Messages
1,084
I'd say it's more likely the effects combined with the large amount of projectiles you spam every second. You can test this by typing "/fps" in-game and seeing if the fps drops when your spell is on the screen compared to looking somewhere else in the map.

I don't think you should be following the model that Mag's tutorial presents; there's a lot of details it shows to make the code efficient, but your spell should still perform fine without worrying about that.
I would recommend making more use of the large amount of vJass libraries to simplify coding spells. In your case, using a timer system like TimerUtils and Table to keep track of spell instances can work.

As a sidenote, EVENT_PLAYER_UNIT_SPELL_ENDCAST will fire with EVENT_PLAYER_UNIT_SPELL_FINISH so you should only use the former event which also fires upon getting interrupted. The first event can also fire if the player doesn't cast the spell so make sure you check for that in stop().
 
Level 10
Joined
Jun 6, 2007
Messages
392
Thanks, I didn't know about that fps command. The fps is normally around 80, and 60 when casting the spell on screen.

I've used TimerUtils for some spells. However, in this case, wouldn't it create a new timer for every caster? Anyway, I'll have to check the JASS section, I'm sure many of the libraries would fit my purposes.

Ahh, I thought that finish event only runs when a spell is successfully finished (not interrupted). I'll remove stop event.
 
Level 16
Joined
Aug 7, 2009
Messages
1,403
You don't need TU for a constant timer, as long as you only start/pause it. Table is an awesome library, I agree, though I prefer a unit indexer + GetUnitUserData solution over it when it comes to attaching data to units, since an array read > hashing.

Also, don't use trigger actions, just call the appropriate method(s) from the condition. That makes the whole thing a lot faster. (Don't forget to return false, though, as the trigger itself not firing is what makes it better).

You should also check out SpellEffectEvent (it's in the JASS section somewhere), it's a great library - not only does it easy the event registration, but it also makes it so that only 1 trigger fires when a spell is cast (that fires another one, though, but it's still only two), instead of having all the spell effect triggers you've created fire, which amount may skyrocket on a large map, creating lag spikes on spell cast.
 
Status
Not open for further replies.
Top