[vJASS] Delay code execution until next frame

Status
Not open for further replies.
Level 45
Joined
Feb 27, 2007
Messages
5,578
I want to select a unit for a player and immediately force a UI key once it's selected so the player just has to click a point and then it casts a spell. Problem is it seems like the key force doesn't work until 1 frame passes to update the UI (logical). So how can I slightly delay the execution of the code until the next frame is drawn? Just picking a number and delaying by that amount doesn't seem like a good idea because it's affected by latency and the actual GPU/CPU lag of the player. However, that's the method I'm using right now; the lowest number I've found to work is 0.025. Below 0.025 it only works if the unit to cast the spell is already selected by the player, which happens instantaneously.

JASS:
library ForceCast initializer Init requires GroupUtils
    private keyword KeyPress
    globals
        private constant real   ADD_DELAY = 0.05
        private constant real CLEAN_DELAY = 0.50
        private constant real LOOP_PERIOD = 0.025
    
        private integer  LP
        private boolexpr CountBE
        private integer  count
    
        private timer    clock
        private integer  instances
        private boolean  running
        private KeyPress array stack
    endglobals

    private struct KeyPress
        unit u
        integer pN
        player  p
        integer a
        integer lvl
        integer lprev
        string k
        group select
 
        static method create takes unit u, player p, integer a, integer l, integer lprev, string k returns thistype
            local thistype new = thistype.allocate()
            set new.u = u
            set new.p = p
            set new.pN = GetPlayerId(p)
            set new.a = a
            set new.lvl = l
            set new.lprev = lprev
            set new.k = k
            set new.select = NewGroup()
            return new
        endmethod
    
        method onDestroy takes nothing returns nothing
            if .lprev == 0 then
                call UnitRemoveAbility(.u, .a)
            elseif .lvl != .lprev then
                call SetUnitAbilityLevel(.u, .a, .lprev)
            endif
        endmethod
    endstruct

    private function CountGroup takes nothing returns boolean
        set count = count+1
        return true
    endfunction
 
    private function DelayedPress takes nothing returns nothing
        local integer i = 0
        local KeyPress kp
    
        loop
            set kp = stack[i]
            if IsUnitSelected(kp.u, kp.p) then
                if LP == kp.pN then
                    call ForceUIKey(kp.k)
                    call BJDebugMsg("success?")
                    //only if the delay is < 0.025
                endif
            
                set instances = instances-1
                if instances < 1 then
                    set running = false
                    call PauseTimer(clock)
                    set stack[0] = 0
                    exitwhen true
                else
                    set stack[i] = stack[instances]
                    set stack[instances] = 0
                    set i = i-1
                endif
            endif
    
            set i = i+1
            exitwhen i>=instances
        endloop
    endfunction
    
 
    function ForcePlayerUnitSpellcast takes unit caster, player castPlayer, integer abilityID, integer level, string hotkey returns nothing
        local integer lprev = GetUnitAbilityLevel(caster, abilityID)
        local boolean isSelect = IsUnitSelected(caster, castPlayer)
        local boolean didAdd    = false
        local boolean didSelect = false
        local KeyPress kp = KeyPress.create(caster, castPlayer, abilityID, level, lprev, hotkey)
    
        set count = 0
        call GroupEnumUnitsSelected(kp.select, castPlayer, CountBE)
        if not isSelect or (isSelect and count>1) then
            set didSelect = true
            if LP == kp.pN then
                call ClearSelection()
                call SelectUnit(caster, true)
            endif
        endif
            
        if lprev == 0 then
            set didAdd = UnitAddAbility(caster, abilityID)
        endif
        if level != 1 and lprev != level then
            call SetUnitAbilityLevel(caster, abilityID, level)
        endif
    
        if didSelect then//or didAdd then
            set stack[instances] = kp
            set instances = instances+1
            if not running then
                set running = true
                call TimerStart(clock, LOOP_PERIOD, true, function DelayedPress)
                call BJDebugMsg("wait")
            endif
        else
            if LP == kp.pN then
                call ForceUIKey(kp.k)
                call BJDebugMsg("forced")
            endif
        endif
    
        if not (didSelect or didAdd) then
//            call TimerStart(kp.t, CLEAN_DELAY, false, function Cleanup)
        endif
    endfunction
 
    private function Init takes nothing returns nothing
        set LP = GetPlayerId(GetLocalPlayer())
        set CountBE = Filter(function CountGroup)

        set clock = NewTimer()
        set instances = 0
        set running = false
    endfunction
endlibrary
 
Are you sure the issue is with ForceUIKey and not with IsUnitSelected? If you're testing online/LAN then IsUnitSelected won't return true instantly after you select a unit, it has to synchronize your selections first.

If ForceUIKey really doesn't work when selecting a new unit until a frame passes then running a periodic timer until it works should be fine. Although I don't think 0.025 would be one frame, it would be closer to 0.016 seconds.

You could also try out the selection event, maybe it fires at the proper time.
 
Level 45
Joined
Feb 27, 2007
Messages
5,578
I'll check the selection event, didn't think of that. I'm sure it's not with IsUnitSelected because the only time I use that after selecting is to determine if I need to wait until it is selected yet. I'm playing offline and my computer is great for much more intense shit and it 100% doesn't work with a 0.02 timeout.

Edited:

I ran it with the selection event and it seems like the time taken before firing that event is inconsistent. Waiting for it does, however, make the keypress work every time. Using the code below (0.005 period) I got between 16 and 40 "loop" messages before "select" showed up, so that's 0.08-0.20 range. I don't think that's gonna work since it needs so much less.

JASS:
library ForceCast initializer Init requires GroupUtils//, LinkedList, xefx, xepreload, Table
    private keyword KeyPress
    globals
        private constant real   ADD_DELAY = 0.05
        private constant real CLEAN_DELAY = 0.50
        private constant real LOOP_PERIOD = 0.005
      
        private integer  LP
        private boolexpr CountBE
        private integer  count
      
        private timer    clock
        private integer  instances
        private boolean  running
        private KeyPress array stack
      
        private trigger TrigSel
        private trigger TrigDes
        private boolean fl
    endglobals

    private struct KeyPress
        unit u
        integer pN
        player  p
        integer a
        integer lvl
        integer lprev
        string k
        group select
  
        static method create takes unit u, player p, integer a, integer l, integer lprev, string k returns thistype
            local thistype new = thistype.allocate()
            set new.u = u
            set new.p = p
            set new.pN = GetPlayerId(p)
            set new.a = a
            set new.lvl = l
            set new.lprev = lprev
            set new.k = k
            set new.select = NewGroup()
            return new
        endmethod
      
        method onDestroy takes nothing returns nothing
            if .lprev == 0 then
                call UnitRemoveAbility(.u, .a)
            elseif .lvl != .lprev then
                call SetUnitAbilityLevel(.u, .a, .lprev)
            endif
        endmethod
    endstruct
  
    private function Cleanup takes nothing returns nothing
        //????
//        call KeyPress(GetTimerData(GetExpiredTimer())).destroy()
    endfunction

    private function CountGroup takes nothing returns boolean
        set count = count+1
        return true
    endfunction

    private function onSelect takes nothing returns nothing
        set fl = true
        call BJDebugMsg("select")
    endfunction

    private function onDeselect takes nothing returns nothing
        call BJDebugMsg("deselect")
    endfunction
  
    private function DelayedPress takes nothing returns nothing
        local integer i = 0
        local KeyPress kp
      
        call BJDebugMsg("loop")
        loop
            set kp = stack[i]
            if IsUnitSelected(kp.u, kp.p) and fl then
                if LP == kp.pN then
                    call ForceUIKey(kp.k)
                    call BJDebugMsg("success?")
                    //only if the delay is < 0.025
                endif
              
                set instances = instances-1
                if instances < 1 then
                    set running = false
                    call PauseTimer(clock)
                    set stack[0] = 0
                    exitwhen true
                else
                    set stack[i] = stack[instances]
                    set stack[instances] = 0
                    set i = i-1
                endif
            endif
      
            set i = i+1
            exitwhen i>=instances
        endloop
    endfunction
  
    function ForcePlayerUnitSpellcast takes unit caster, player castPlayer, integer abilityID, integer level, string hotkey returns nothing
        local integer lprev = GetUnitAbilityLevel(caster, abilityID)
        local boolean isSelect = IsUnitSelected(caster, castPlayer)
        local boolean didAdd    = false
        local boolean didSelect = false
        local KeyPress kp = KeyPress.create(caster, castPlayer, abilityID, level, lprev, hotkey)
      
        set count = 0
        call GroupEnumUnitsSelected(kp.select, castPlayer, CountBE)
        if not isSelect or (isSelect and count>1) then
            set didSelect = true
            if LP == kp.pN then
                call ClearSelection()
                call SelectUnit(caster, true)
            endif
        endif
              
        if lprev == 0 then
            set didAdd = UnitAddAbility(caster, abilityID)
        endif
        if level != 1 and lprev != level then
            call SetUnitAbilityLevel(caster, abilityID, level)
        endif
      
        set fl = false
        if didSelect then//or didAdd then
            set stack[instances] = kp
            set instances = instances+1
            if not running then
                set running = true
                call TimerStart(clock, LOOP_PERIOD, true, function DelayedPress)
                call BJDebugMsg("wait")
            endif
        else
//          call TimerStart(kp.t, CLEAN_DELAY, false, function Cleanup)
            if LP == kp.pN then
                call ForceUIKey(kp.k)
                call BJDebugMsg("forced")
            endif
        endif
      
        if not (didSelect or didAdd) then
//            call TimerStart(kp.t, CLEAN_DELAY, false, function Cleanup)
        endif
    endfunction
  
    private function Init takes nothing returns nothing
        set LP = GetPlayerId(GetLocalPlayer())
        set CountBE = Filter(function CountGroup)

        set clock = NewTimer()
        set instances = 0
        set running = false
      
        set TrigSel = CreateTrigger()
        set TrigDes = CreateTrigger()
        call TriggerRegisterPlayerUnitEvent(TrigSel, Player(5), EVENT_PLAYER_UNIT_SELECTED, null)
        call TriggerRegisterPlayerUnitEvent(TrigDes, Player(5), EVENT_PLAYER_UNIT_DESELECTED, null)
        call TriggerAddAction(TrigSel, function onSelect)
        call TriggerAddAction(TrigDes, function onDeselect)
    endfunction
endlibrary

Edit again: IsUnitSelected() returns true in between the time the timer first starts and the selection event fires. In fact even before the deselect event fires from ClearSelection()! Probably fast because I'm not on bnet but still that seems to confirm it's a visual update thing.
 
Last edited:
Whenever I need delayed executions with units and the UI, I add them to a queue like this:

JASS:
function timeout takes nothing returns nothing
    local integer i = 1
    loop
        // do the delayed stuff with someUnit[i]
        if (i == maxIndex) then
            set maxIndex = 0
            exitwhen true
        endif
        set i = i + 1
    endloop
endfunction

function start takes nothing returns nothing
    set maxIndex = maxIndex + 1
    set someUnit[maxIndex] = GetTriggerUnit()
    call TimerStart(t, 0.00, false, function timeout)
endfunction
 
Level 45
Joined
Feb 27, 2007
Messages
5,578
I see now that it is not. As I said above I've already tried the 0.00 timeout timer approach and it fires before the UI gets updated so the keypress doesn't work then.

Tested a little bit more and it seems to take a standardized amount of time to switch from selecting 1 unit to selecting the next is consistently just over 0.2, 0.175, or 0.1 seconds (it runs 201, 176, or 101 loops every time on a 0.001 timeout), which looks exactly like something timed to the 0.025 period that would be 40 fps. Or maybe selection events push updates every 0.025 seconds.

You can play with my test map and see what happens. Pressing escape or typing any text will force the far seer to select a target for a spell. Select different allied and enemy units before typing and it will show you different delays before the unit selected event fires. Aside from forcing the UI key repeatedly in a loop I have no idea how to time an event to go off as soon as its forceable, but there's definitely a bit of visual delay where yo ucan see the command card.
 

Attachments

  • Multi Target.w3x
    136.2 KB · Views: 24
Last edited:
Status
Not open for further replies.
Top