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

[JASS] A mass push ability questions

Status
Not open for further replies.
Level 11
Joined
Dec 21, 2012
Messages
373
Hello. I've gotten into JASS quite recently, and currently, am working on push/knockback abilities. I think, at least, that I've gotten the hang of the ones for single units, but now, I've been trying to create a boss ability, that knocks all the units away from the caster - a mass push/knockback ability.

Eventually, I settled with this functionality:
> Units around the caster, that are alive and enemy of the caster, are placed into a group (b1KnockUnits)
> The units from the group are placed into Unit Array (using FirstOfGroup), and that array is looped through in Timer function.

I wanted to ask if using multiple loops inside a Timer Function, that is used every 0.04 secs, might cause problems, in terms of lag, for example?
Also, is there a more efficient way to do this? The ability seemingly does it's job, but it looks clumsy and awkward, especially talking about the vertical movement.


JASS:
function Trig_Begone_Conditions takes nothing returns boolean
    if ( GetSpellAbilityId() == 'A001' )  then
        return true
    endif
    return false
endfunction

// Condition used to find matching units for the b1KnockUnits group:
function KnockGroupConditions takes nothing returns boolean
    return GetBooleanAnd((GetUnitState(GetFilterUnit(), UNIT_STATE_LIFE) <= 0) == false, IsUnitAlly(GetFilterUnit(), GetOwningPlayer(udg_FW)) == false)
endfunction

// This is the Timer function, that is used every 0.04 secs. The Ifs and Loops are all inside.
function Timer_Function takes nothing returns nothing
    // Setup the local variables:
    local timer Clock = GetExpiredTimer()
    local real array knockAngle
    local location array movePoint
    local location array targetPoint
    local unit u
    // The integers below are used for Loops.
    local integer i = 0
    local integer j = 0
    local integer k = 0
    local integer l = 0
    local integer g = 0
    
    //> The "IF" statement below makes sure, that the commands(functions) inside that "IF" statement
    //> are used only once.
    if (udg_b1knockCheck < 1) then
        loop
            exitwhen k >= udg_b1GroupCount
            call UnitAddAbility(udg_KickUnit[k], 'Amrf')
            call UnitRemoveAbility(udg_KickUnit[k], 'Amrf')
            call PauseUnit(udg_KickUnit[k], true)
            call SetUnitPathing(udg_KickUnit[k], false)
            set k = k + 1
        endloop
        set udg_b1knockCheck = udg_b1knockCheck + 1
    endif
    
    //> This the main part of the function - the IF statement below. 
    //> It checks whether b1knockN (distance travelled) is higher than Maximum distance (b1knockDistance)
    //> The first part is if the condition returns true (stops the timer).
    //> The second part ("else" part) is if the condition is false (resumes the action).
    if (udg_b1knockN > udg_b1knockDistance) then
        //> The variable below is used to check how many units fell down the cliff.
        //> (Every unit, that falls down the cliff, is added to "b1ThrownU" Group)
        set udg_b1Check2 = CountUnitsInGroup(udg_b1ThrownU)
        // Timer is stopped and the trigger is being cleaned up:
        call PauseTimer(Clock)
        loop
            exitwhen j >= udg_b1GroupCount
            call PauseUnit(udg_KickUnit[j], false)
            call SetUnitPathing(udg_KickUnit[j], true)
            call SetUnitFlyHeight(udg_KickUnit[j], 0, 200.00 )
            call RemoveLocation(movePoint[j])
            call RemoveLocation(targetPoint[j])
            set udg_KickUnit[j] = null
            set j = j + 1
        endloop
        //> Checks if there ARE any units, that fell down the cliff (if b1Check2 > 0), and, 
        //> if so,  tells how many.
        if (udg_b1Check2 > 0) then
            call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, I2S(udg_b1Check2) + "|c00FF7F00 units were thrown off the Cliff!|r")
        endif
        // Final cleaning up:
        call RemoveLocation(udg_b1knockCastPoint)
        set udg_b1knockN = 0
        set udg_b1knockHN = 0
        set udg_b1Check2 = 0
        call DestroyTimer(Clock)
        call DestroyGroup(udg_b1KnockUnits)
    else // The second part of the if statement
        loop 
            //> Loop end when all the units in the unit array (KickUnit) are looped through.
            //> The size (b1GroupCount) of the Group (b1KnockUnits), that the unit array was made from, 
            //> is used to determine how many units to loop through in total.
            exitwhen i >= udg_b1GroupCount
            set targetPoint[i] = GetUnitLoc(udg_KickUnit[i])
            set knockAngle[i] = AngleBetweenPoints(udg_b1knockCastPoint, targetPoint[i])
            set movePoint[i] = PolarProjectionBJ(targetPoint[i], 20, knockAngle[i])
            call SetUnitPositionLoc(udg_KickUnit[i], movePoint[i])
            //> If statement below is used to check if a unit was thrown off the cliff.
            //> If yes, then that unit is killed and added to Thrown units Group (ThrownU).
            //> This is done to count how many were thrown off, as stated before.
            if (GetTerrainCliffLevel(GetLocationX(GetUnitLoc(udg_KickUnit[i])), GetLocationY(GetUnitLoc(udg_KickUnit[i]))) < 3) then
                call KillUnit(udg_KickUnit[i])
                if (IsUnitInGroup(udg_KickUnit[i], udg_b1ThrownU) == false) then
                    call GroupAddUnit(udg_b1ThrownU, udg_KickUnit[i])
                endif
            endif
            call RemoveLocation(movePoint[i])
            call RemoveLocation(targetPoint[i])
            set i = i + 1
        endloop
        // The If statement (and the loops inside it) below is used to move units vertically.
        if (udg_b1knockHN < 250) then
            loop
                exitwhen l >= udg_b1GroupCount
                call SetUnitFlyHeight(udg_KickUnit[l], 40.00, 1000.00 )
                set l = l + 1
            endloop
            set udg_b1knockHN = udg_b1knockHN + 40
        else
            loop
                exitwhen g >= udg_b1GroupCount
                call SetUnitFlyHeight( udg_KickUnit[g], ( GetUnitFlyHeight(udg_KickUnit[g]) - 40.00 ), 1000.00 )
                set g = g + 1
            endloop
            set udg_b1knockHN = udg_b1knockHN - 40
        endif
        set udg_b1knockN = udg_b1knockN + 20
    endif
endfunction


// The MainFunction.
function Trig_Begone_Actions takes nothing returns nothing
    local timer Clock = CreateTimer()
    local location B1PlayGround = GetRectCenter(gg_rct_FWBossArea)
    local integer i
    local integer j = 0
    local integer k = 0
    local unit u
    set udg_b1ThrownU = CreateGroup()
    set udg_b1knockCaster = GetSpellAbilityUnit()
    set udg_b1knockCastPoint = GetUnitLoc(udg_b1knockCaster)
    set udg_b1KnockUnits = GetUnitsInRangeOfLocMatching(500.00, udg_b1knockCastPoint, Condition(function KnockGroupConditions))
    set udg_b1GroupCount = CountUnitsInGroup(udg_b1KnockUnits)
    set udg_b1knockDistance = 500
    set udg_b1knockCheck = 0
    set udg_b1AreaHeightLevel = GetTerrainCliffLevel(GetLocationX(B1PlayGround), GetLocationY(B1PlayGround))
    
    //> This is where units from the Group(b1KnockUnits) are placed into Unit Array (KickUnit):
    set u = FirstOfGroup(udg_b1KnockUnits)
    loop
        exitwhen u == null
        call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, "a " + I2S(j))
        set udg_KickUnit[j] = u
        call GroupRemoveUnit(udg_b1KnockUnits, u)
        set u = FirstOfGroup(udg_b1KnockUnits)
        set j = j + 1
    endloop
    call TimerStart(Clock, 0.04, true, function Timer_Function)
    
endfunction

//===========================================================================
function InitTrig_Begone takes nothing returns nothing
    set gg_trg_Begone = CreateTrigger(  )
    call TriggerRegisterAnyUnitEventBJ( gg_trg_Begone, EVENT_PLAYER_UNIT_SPELL_CAST )
    call TriggerAddCondition( gg_trg_Begone, Condition( function Trig_Begone_Conditions ) )
    call TriggerAddAction( gg_trg_Begone, function Trig_Begone_Actions )
endfunction
 
Level 39
Joined
Feb 27, 2007
Messages
5,012
I wanted to ask if using multiple loops inside a Timer Function, that is used every 0.04 secs, might cause problems, in terms of lag, for example?
In general, no. Much more complicated things with many more operations have been done at faster timer intervals. However, you use a fair number of BJ functions (anything that shows up in red rather than purple), which will slow down your code execution), and you do leak a fair number of locations (which will be bad over time).
Also, is there a more efficient way to do this?
Yes, you should work with X/Y coordinates instead of locations. In general for code that runs frequently it's always to your advantage to remove as many handle-type variables from the code as possible. Handles are basically everything except the primitives: integer, real, string, and booleans. Handles are locations while reals (what you'd use for coordinates) are not; most functions that take a location as an argument also have an analogous version that only uses the coordinates instead. In this specific instance SetUnitX() and SetUnitY() both also ignore pathing/collision (whereas SetUnitPositionLoc() does not) so you don't have to disable unit pathing.

The way you count units and loop through them is also mostly unnecessary. You can just check when FirstOfGroup() is = null to know when the group is empty, no counting required. And you could also increase an integer variable each time a unit is moved over the cliff and killed rather than putting them into a group and then counting it at the end of the spell. Instead of CAST you should use EVENT_PLAYER_UNIT_SPELL_EFFECT; the first fires slightly before mana is used and the spell goes on cooldown whereas the second only happens the instant the spell is fully (successfully) cast.

If you're fully up-to-date and using the 1.30 World Editor or not fully updated but using WEX, you can make use of some of the basic vJASS functionality to declare all of the relevant global variables within the trigger itself rather than having to declare them separately in the variable editor (and thus inherit the udg_ prefix).

Your condition and filter functions can be simplified and reduce the number of function calls. There's no reason to use GetBooleanAnd() and compare == true or != false; just use the and, or, and not keywords as necessary. Units don't die at 0.00 life, they die when they reach 0.405 life, so check for that instead.
JASS:
function Trig_Begone_Conditions takes nothing returns boolean
   return GetSpellAbilityId() == 'A001'
endfunction

// Condition used to find matching units for the b1KnockUnits group:
function Begone_KnockGroupConditions takes nothing returns boolean
   local unit f = GetFilterUnit()
   local boolean b1 = GetUnitState(f, UNIT_STATE_LIFE) > 0.405)
   local boolean b2 = IsUnitAlly(f, GetOwningPlayer(udg_FW)) //you could also save GetOwningPlayer(udg_FW) in a variable before this filter is run to further increaese efficiency
   set f = null
   return b1 and not b2
endfunction

Since there's only ever one instance of this spell happening at a time, instead of creating/destroying new timers and groups you can instead just have a couple global ones that you re-use each time the boss does its knockback. Some things are going to always be constant, such as the cliff level at the center of the boss' rect, so you can also set that here. Here are the globals I would define and set up in your InitTrig:
JASS:
globals
   timer   BegoneClock = null
   group   BegoneGroup = null
   integer BegoneCliff = 0
   integer BegoneCountdown = 0
   integer BegoneThrownCount = 0
   integer BegoneFlipDZ = 0 //used to tell when to switch from going up to going down, it's half the initial BegoneCountdown rounded down

   
   integer    BegoneCount = 0
   unit array BegoneUnits   //the units being knocked back
   real array BegoneDX       //change in x per step for each unit
   real array BegoneDY       //change in y per step for each unit
   real array BegoneDZ       //change in z per step for each unit
   
   constant real Begone_RADIUS = 500.00   //radius of kb check
   constant real Begone_SPEED = 500.00       //how many units per sec the kb moves
   constant real Begone_DISTANCE = 500.00   //how far the kb moves units
   constant real Begone_HEIGHT = 250.00   //how high up the kb moves units
   constant real Begone_INTERVAL = 0.04   //how frequently the timer runs   
endglobals

//===========================================================================
function InitTrig_Begone takes nothing returns nothing
   set BegoneClock = CreateTimer()
   set BegoneGroup = CreateGroup()
   set BegoneCliff = GetTerrainCliffLevel(GetRectCenterX(gg_rct_FWBossArea), GetRectCenterY(gg_rct_FWBossArea))
   
   set gg_trg_Begone = CreateTrigger(  )
   call TriggerRegisterAnyUnitEventBJ( gg_trg_Begone, EVENT_PLAYER_UNIT_SPELL_CAST )
   call TriggerAddCondition( gg_trg_Begone, Condition( function Trig_Begone_Conditions ) )
   call TriggerAddAction( gg_trg_Begone, function Trig_Begone_Actions )
endfunction

Now for the cast function that sets up the knockback. Instead of the check for the first loop of the timer you do in the timer function, you can perform all of that stuff right when you set the units into the array. I would also suggest you precompute your dX, dY, and dZ values here that way you just have to do SetUnitX(unit, GetUnitX(unit)+dX) and such later. You probably actually want a parabolic knockback in the z direction, but that would require a bit more math and I just left it as a linear /\ up to the max height of 250.
JASS:
// The MainFunction.
function Trig_Begone_Actions takes nothing returns nothing
   local unit u = GetTriggerUnit()
   local real cx = GetUnitX(u)
   local real cy = GetUnitY(u)
   local real x
   local real y
   local real ang
   
   set BegoneCount = 0
   set BegoneThrownCount = 0
   //call GroupClear(BegoneGroup) technically don't need this because you know the group gets cleared every time the loop below this is run
   call GroupEnumUnitsInRange(BegoneGroup, cx, cy, Begone_RADIUS, Filter(function Begone_KnockGroupConditions)

    //> This is where units from the Group (BegoneGroup) are placed into Unit Array (BegoneUnit):
   loop
       set u = FirstOfGroup(BegoneGroup)
       exitwhen u == null
       call GroupRemoveUnit(BegoneGroup, u)
       call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, "a " + I2S(BegoneCount)) //this line will appear to lag on the screen because of how text display works; if you comment it out this part of the code will execute faster
       
       set x = GetUnitX(u)
       set y = GetUnitY(u)
       set ang = Atan2(y-cy, x-cx) //this is an arctan function to get the angle from point (cx,cy) to (x,y)
       set BegoneUnits[BegoneCount] = u
       set BegoneDX[BegoneCount] = Begone_SPEED*Cos(ang)*Begone_INTERVAL //these could be simplified to some algebra with x,y,cx,cy instead of Cos and Sin, but this makes it more clear how exactly they are computed
       set BegoneDY[BegoneCount] = Begone_SPEED*Sin(ang)*Begone_INTERVAL //the *Begone_INTERVAL factor is because you are moving them multiple times a second
       set BegoneDZ[BegoneCount] = Begone_HEIGHT/(0.5*Begone_DISTANCE/Begone_SPEED)*Begone_INTERVAL //The height divided by half the total time the kb takes
       
       call UnitAddAbility(u, 'Amrf')
       call UnitRemoveAbility(u, 'Amrf')
       call PauseUnit(u, true)
       //call SetUnitPathing(u, false) unnecessary here because SetUnitX/Y don't check pathing
       
       set BegoneCount = BegoneCount+1
   endloop
   
    if BegoneCount > 0 then
       set BegoneCountdown = R2I(Begone_DISTANCE/Begone_SPEED/Begone_INTERVAL) //how many loops of the timer should we do?
       set BegoneFlipDZ = BegoneCountdown/2                                   //half of BegoneCountdown rounded down
       call TimerStart(BegoneClock, Begone_INTERVAL, true, function Begone_Timer_Function)
   endif
   
   //set u = null also unnecessary because u is always null when the above loop exits
endfunction

Now for the periodic function, the meat of it all. Instead of checking the total distance moved vs the max it's better to just figure out how many times it should run and then count down to 0 by subtracting 1 each time; when you reach 0 the knockback is over.
JASS:
// This is the Timer function, that is used every 0.04 secs. The Ifs and Loops are all inside.
function Begone_Timer_Function takes nothing returns nothing
   local integer j = 0
   local integer nx
   local integer ny
   
   loop
       set nx = GetUnitX(BegoneUnit[j])+BegoneDX[j]
       set ny = GetUnitY(BegoneUnit[j])+BegoneDY[j]
       if GetTerrainCliffLevel(nx, ny) < BegoneCliff then
           call KillUnit(BegoneUnit[j])
           set BegoneThrownCount = BegoneThrownCount + 1
       endif
       
       call SetUnitX(BegoneUnit[j], nx)
       call SetUnitY(BegoneUnit[j], ny)
       if BegoneCount > BegoneFlipDZ
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])+BegoneDZ[j])
       else
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])-BegoneDZ[j])
       endif
       
       set j = j+1
       exitwhen j >= BegoneCount
   endloop
   
   set BegoneCountdown = BegoneCountdown-1
   if BegoneCountdown <= 0 then
       call PauseTimer(BegoneClock)
       call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, I2S(BegoneThrownCount) + "|c00FF7F00 units were thrown off the Cliff!|r")
       
       set j = 0
       loop
           call PauseUnit(BegoneUnit[j], false)
           call SetUnitFlyHeight(BegoneUnit[j], 0.00, 0.00) //0 as the third argument makes it instantaneous
           //You may have to do something here to move units out of unpathable terrain if they ended up in it
           set j = j+1
           exitwhen j >= BegoneCount
       endloop
   endif
endfunction

Now all of that together looks like this:
JASS:
globals
   timer   BegoneClock = null
   group   BegoneGroup = null
   integer BegoneCliff = 0
   integer BegoneCountdown = 0
   integer BegoneFlipDZ = 0 //used to tell when to switch from going up to going down, it's half the initial BegoneCountdown rounded down
   integer BegoneThrownCount = 0

   integer    BegoneCount = 0
   unit array BegoneUnits   //the units being knocked back
   real array BegoneDX       //change in x per step for each unit
   real array BegoneDY       //change in y per step for each unit
   real array BegoneDZ       //change in z per step for each unit

   constant real Begone_RADIUS = 500.00   //radius of kb check
   constant real Begone_SPEED = 500.00       //how many units per sec the kb moves
   constant real Begone_DISTANCE = 500.00   //how far the kb moves units
   constant real Begone_HEIGHT = 250.00   //how high up the kb moves units
   constant real Begone_INTERVAL = 0.04   //how frequently the timer runs   
endglobals


function Trig_Begone_Conditions takes nothing returns boolean
   return GetSpellAbilityId() == 'A001'
endfunction

// Condition used to find matching units for the b1KnockUnits group:
function Begone_KnockGroupConditions takes nothing returns boolean
   local unit f = GetFilterUnit()
   local boolean b1 = GetUnitState(f, UNIT_STATE_LIFE) > 0.405)
   local boolean b2 = IsUnitAlly(f, GetOwningPlayer(udg_FW)) //you could also save GetOwningPlayer(udg_FW) in a variable before this filter is run to further increaese efficiency
   set f = null
   return b1 and not b2
endfunction

// This is the Timer function, that is used every 0.04 secs. The Ifs and Loops are all inside.
function Begone_Timer_Function takes nothing returns nothing
   local integer j = 0
   local integer nx
   local integer ny
   
   loop
       set nx = GetUnitX(BegoneUnit[j])+BegoneDX[j]
       set ny = GetUnitY(BegoneUnit[j])+BegoneDY[j]
       if GetTerrainCliffLevel(nx, ny) < BegoneCliff then
           call KillUnit(BegoneUnit[j])
           set BegoneThrownCount = BegoneThrownCount + 1
       endif
       
       call SetUnitX(BegoneUnit[j], nx)
       call SetUnitY(BegoneUnit[j], ny)
       if BegoneCount > BegoneFlipDZ
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])+BegoneDZ[j])
       else
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])-BegoneDZ[j])
       endif
       
       set j = j+1
       exitwhen j >= BegoneCount
   endloop
   
   set BegoneCountdown = BegoneCountdown-1
   if BegoneCountdown <= 0 then
       call PauseTimer(BegoneClock)
       call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, I2S(BegoneThrownCount) + "|c00FF7F00 units were thrown off the Cliff!|r")
       
       set j = 0
       loop
           call PauseUnit(BegoneUnit[j], false)
           call SetUnitFlyHeight(BegoneUnit[j], 0.00, 0.00) //0 as the third argument makes it instantaneous
           //You may have to do something here to move units out of unpathable terrain if they ended up in it
           set j = j+1
           exitwhen j >= BegoneCount
       endloop
   endif
endfunction


// The MainFunction.
function Trig_Begone_Actions takes nothing returns nothing
   local unit u = GetTriggerUnit()
   local real cx = GetUnitX(u)
   local real cy = GetUnitY(u)
   local real x
   local real y
   local real ang
   
   set BegoneCount = 0
   set BegoneThrownCount = 0
   //call GroupClear(BegoneGroup) technically don't need this because you know the group gets cleared every time the loop below this is run
   call GroupEnumUnitsInRange(BegoneGroup, cx, cy, Begone_RADIUS, Filter(function Begone_KnockGroupConditions)

    //> This is where units from the Group (BegoneGroup) are placed into Unit Array (BegoneUnit):
   loop
       set u = FirstOfGroup(BegoneGroup)
       exitwhen u == null
       call GroupRemoveUnit(BegoneGroup, u)
       call DisplayTimedTextToPlayer(Player(2), 0, 0, 3, "a " + I2S(BegoneCount)) //this line will appear to lag on the screen because of how text display works; if you comment it out this part of the code will execute faster
       
       set x = GetUnitX(u)
       set y = GetUnitY(u)
       set ang = Atan2(y-cy, x-cx) //this is an arctan function to get the angle from point (cx,cy) to (x,y)
       set BegoneUnits[BegoneCount] = u
       set BegoneDX[BegoneCount] = Begone_SPEED*Cos(ang)*Begone_INTERVAL //these could be simplified to some algebra with x,y,cx,cy instead of Cos and Sin, but this makes it more clear how exactly they are computed
       set BegoneDY[BegoneCount] = Begone_SPEED*Sin(ang)*Begone_INTERVAL //the *Begone_INTERVAL factor is because you are moving them multiple times a second
       set BegoneDZ[BegoneCount] = Begone_HEIGHT/(0.5*Begone_DISTANCE/Begone_SPEED)*Begone_INTERVAL //The height divided by half the total time the kb takes
       
       call UnitAddAbility(u, 'Amrf')
       call UnitRemoveAbility(u, 'Amrf')
       call PauseUnit(u, true)
       //call SetUnitPathing(u, false) unnecessary here because SetUnitX/Y don't check pathing
       
       set BegoneCount = BegoneCount+1
   endloop
   
   if BegoneCount > 0 then
       set BegoneCountdown = R2I(Begone_DISTANCE/Begone_SPEED/Begone_INTERVAL) //how many loops of the timer should we do?
       set BegoneFlipDZ = BegoneCountdown/2                                   //half of BegoneCountdown rounded down
       call TimerStart(BegoneClock, Begone_INTERVAL, true, function Begone_Timer_Function)
   endif
   
   //set u = null also unnecessary because u is always null when the above loop exits
endfunction

//===========================================================================
function InitTrig_Begone takes nothing returns nothing
   set BegoneClock = CreateTimer()
   set BegoneGroup = CreateGroup()
   set BegoneCliff = GetTerrainCliffLevel(GetRectCenterX(gg_rct_FWBossArea), GetRectCenterY(gg_rct_FWBossArea))
   
   set gg_trg_Begone = CreateTrigger(  )
   call TriggerRegisterAnyUnitEventBJ( gg_trg_Begone, EVENT_PLAYER_UNIT_SPELL_CAST )
   call TriggerAddCondition( gg_trg_Begone, Condition( function Trig_Begone_Conditions ) )
   call TriggerAddAction( gg_trg_Begone, function Trig_Begone_Actions )
endfunction
 
Level 11
Joined
Dec 21, 2012
Messages
373
Thank you for the detailed explanation, Pyro, this really helps, and the ability now looks much smoother (though it ends rather abruptly). The only thing I don't quite get from the whole thing are the math formulas - that's something I have much to learn as well. But, I am sure I'll get it in time. Again, thanks for the help.

Although, talking about the part where I check how many were thrown off the cliff:

JASS:
if GetTerrainCliffLevel(nx, ny) < BegoneCliff then
           call KillUnit(BegoneUnit[j])
           set BegoneThrownCount = BegoneThrownCount + 1
endif


This part is inside a loop, and thus, even if just one unit is thrown off, the BegoneThrownCount actually gets +number of loops, e.g. if there are 6 units to knockback (6 loops), the variable gets +6 added, even if only one unit was thrown off.
This is why I employed my previous method of counting - adding thrown units into a separate group and later, counting the size of that group.
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,012
You could add a boolean array BegoneCliffKilled. Set it = false when a unit is added to the queue and set = true when the unit is killed, then check that it isn't already true:
JASS:
if not BegoneCliffKilled[j] and GetTerrainCliffLevel(nx, ny) < BegoneCliff then
    set BegoneCliffKilled[j] = true
    call KillUnit(BegoneUnit[j])
    set BegoneThrownCount = BegoneThrownCount + 1
endif

(though it ends rather abruptly)
How do you mean? I might have messed up the Z movement a little bit but it should only be off at the end by +/-DZ. Could also be weird going over cliffs. Did you want it to slow down at the end of the knockback? What exactly is abrupt?

The only thing I don't quite get from the whole thing are the math formulas
Effectively the math is just converting from (r, theta) to (x, y) coordinates: Polar to Cartesian. In that diagram r = the kb speed and theta is the ang we calculated (and Atan2(y,x) is analogus to ArcTan(y/x) but it keeps the quadrant information intact by taking y and x as separate arguments). We want to compute x (what I called DX) and y (my DY) so we know how far to move the unit along each coordinate axis every time the timer expires (you stored r and calculated the angle each time, which is considerably more math under-the-hood than adding 2 reals together!).

Cos(theta) = r/x (opposite-over-adjacent), so solving for x gives x = r*Cos(theta); you can do the same to find y = r*Sin(theta). Finally I multiplied each factor by the timeout period. This makes sense if you imagine the timer ran 1 time per second. How far should it move the unit in 1 second? The speed of the knockback (presuming speed is given in units per second), aka r = 500. What if the timeout period was 0.50 seconds? Well, then it happens twice before it's done so the speed would have to be r = 250. What if it happened 5 times per second? Then it would go r = 100 per expiration. Thus r = speed*1/(executions per second) = speed*period.

Logic on the vertical movement is actually easier. h = DZ*t (height = velocity x time) solved for DZ gives us DZ = h/t. The distance we have to travel up is 250, but to be able to go up and back down in the duration of the kb t is actually only half the total time. Or you can think of it as we have to be moving fast enough vertically to cover twice the maximum height in the duration of the kb. Either way gives dz = h/(0.5*t) = (2*h)/t. Now what is the total time of the knockback? It's the speed/distance (horizontal) of the kb, so dz = 2*h/(dist/duration). Finally this gets the same *period factor as the DX and DY.

A more complex knockback would use a parabolic arc for the DZ and require a bit more math.[/code]
 
Level 11
Joined
Dec 21, 2012
Messages
373
Hmm, I seemingly figured out the problem with the "abruptness". It was just a typo in the If statement, in "Begone_Timer_Function":

JASS:
if BegoneCount > BegoneFlipDZ
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])+BegoneDZ[j])
       else
           call SetUnitFlyHeight(BegoneUnit[j], GetUnitFlyHeight(BegoneUnit[j])-BegoneDZ[j])
endif
This line:
JASS:
 if BegoneCount > BegoneFlipDZ
Should have been written as:
JASS:
 if BegoneCountdown > BegoneFlipDZ
Because "BegoneCount" is the index of the Unit Array.

As for the maths - it's more clear now what you were doing, although, I imagine, that it'll take practice before I fully grasp this. I also looked through various other posts around here, as well as the link you provided, and I am beginning to recall it.

Thanks again for all the help.

EDIT #2: I changed the "throwing off cliff" part, so thrown off units are counted properly, and to solve the case of units landing on unpathable terrain.

JASS:
if not BegoneCliffKilled[j] and GetTerrainCliffLevel(nx, ny) < BegoneCliff or IsTerrainPathable(nx, ny, PATHING_TYPE_WALKABILITY) then
           call KillUnit(BegoneUnits[j])
           set BegoneCliffKilled[j] = true
           set BegoneThrownCount = BegoneThrownCount + 1
endif
 
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,012
Oh, oops! Glad to see you figured it out though. If that pathability check is working correctly for you then ignore me, but because you haven't grouped any of those booleans together I feel like it's instantly going to kill every unit when Begone happens.
JASS:
if A and B or C then
//would be evaluated as
if (A and B) or C
However I could be wrong about that, and if so ignore me. You might also want a "not" in front of the pathability check?
 
Status
Not open for further replies.
Top