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

Some uses of coroutines

Status
Not open for further replies.
Level 13
Joined
Nov 7, 2014
Messages
571
Unfortunately no one can be told what coroutines are, they have to see them for themselves.

Coroutine references 1, 2, 3.

1. Sequence of actions interleaved with waits (more accurate TriggerSleepAction)

Lua:
local tmr = CreateTimer()
local sequenceOfActionsInterleavedWithWaitsCo -- coroutine

local function resumeWait()
    coroutine.resume(sequenceOfActionsInterleavedWithWaitsCo)
end

local function wait(seconds)
    TimerStart(tmr, seconds, false, resumeWait)
    coroutine.yield()
end

local function sequenceOfActionsInterleavedWithWaits()
    print('A')
    wait(1.0)
    print('B')
    wait(1.0)

    for a = 1, 50 do
        print(a)
        wait(1.0/50.0)
    end
end

...
-- the initial invocation of a coroutine is a bit different from that of regular functions
--
sequenceOfActionsInterleavedWithWaitsCo = coroutine.create(sequenceOfActionsInterleavedWithWaits)
coroutine.resume(sequenceOfActionsInterleavedWithWaitsCo) -- == resumeWait()
...

This is a very common thing to do (think cinematic triggers full of TriggerSleepActions), a lot of the uses of timers. (1, 2, 3).


2. State machines (Spells)

Lua:
local MySpell = {}
MySpell.__index =MySpell

local function wait()
    coroutine.yield(true)
end

function MySpell:think()
    return coroutine.resume(self.thinkCo)
end

function MySpell:stateSpawn()
    self.fx = AddSpecialEffect("...", self.x, self.y)
    ...
    a:stateMove()
end

function MySpell:stateMove()
    while true do
        if <condition> then
            break
        end

        -- move code

        wait() -- note the call to wait
    end

    self:stateDestroy()
end

function MySpell:stateDestroy()
    DestroyEffect(self.fx)
end

function MySpell.new(a, b, c)
    local s = setmetatable({}, MySpell)

    s.thinkCo = coroutine.create(function()
        s:stateSpawn() -- initial state
    end)
    s:think()

    return s
end

local tmr: timer = CreateTimer()
local xs = {}

local function tmrLoop()
    local a = 1
    while a <= #xs do
        local running = xs[a]:think()
        if not running then
            table.remove(xs, a)
        else
            a = a + 1
        end
    end
    if 0 == #xs then
        PauseTimer(tmr)
    end
end

local function mySpellCasted()
    local s = MySpell.new(...)
    xs[#xs+1] = s
    if 1 == #xs then
        TimerStart(tmr, 1.0/32.0, true, tmrLoop)
    end
end

This example has 3 states (stateSpawn, stateMove, stateDestroy), but its easy to add more states that do something interesting (animations, colors/fading etc.). If you have a spell and there's a field somewhere called 'state', you could probably use coroutines there.


If you know more uses of coroutines, please, do tell.
 

~El

Level 17
Joined
Jun 13, 2016
Messages
556
A generalization of the first use is to use coroutines to implement asynchronicity. In addition to using coroutines to implement asynchronous, precise timers that look like synchronous calls, you can also use them in the same way to do a plethora of other things, like:

* Wait for a specific game event to occur
* Wait for something to happen to a specific unit
* Wait for something to happen to *any* unit
* Wait for a unit to enter a certain area
* And so on

Anything that is an event can be adapted to be used as a wait condition inside a coroutine with some scaffolding. It's primarily useful for complex, possibly branching, strictly sequenced mechanics when you don't want to bog yourself down in callback hells. For example, spells, cinematics, unit AI, etc. With a little bit of grease you can make an adapter that can await multiple different conditions (like selecting a Future in some other languages) to implement complex control flow that looks like it runs on a thread of it's own, but is actually asynchronous. Conceptually it's all a state machine like you already outlined, but I thought I'd provide a few more real-world examples of how to apply coroutines to things in a game.

None of this is unique to WC3 either. You can carry that knowledge over to any other game that has Lua support or another language with coroutines (generators, green threads, asynchronous support, Futures, Promises are all different names for very similar concepts elsewhere).
 
Level 13
Joined
Nov 7, 2014
Messages
571
Anything that is an event can be adapted to be used as a wait condition inside a coroutine with some scaffolding.

If anyone is curious what that might look like here's a small demo (don't mind the type annotations):
Lua:
local WeakKeysTable_mt = { __mode = 'k' }
local function newWeakKeysTable(): table
    return setmetatable({}, WeakKeysTable_mt)
end

-- @Optimization: use caching for the 'wait'/'delay' function because it's probably
-- the one that's going to get called the most
--
local wait: function(seconds: number)
do
    local tmrOf: {thread:timer} = newWeakKeysTable() as {thread:timer}
    local cbOf: {thread:function} = newWeakKeysTable() as {thread:function}

    wait = function(seconds: number)
        local co: thread = coroutine.running()

        local tmr = tmrOf[co]
        if tmr == nil then
            tmr = CreateTimer()
            tmrOf[co] = tmr
        end

        local cb = cbOf[co]
        if cb == nil then
            cb = function() coroutine.resume(co) end
            cbOf[co] = cb
        end

        TimerStart(tmr, seconds, false, cb)
        coroutine.yield()
    end -- wait
end

local function waitUnitDie(u: unit)
    local co: thread = coroutine.running()
    local trg = CreateTrigger()
    TriggerRegisterUnitEvent(trg, u, EVENT_UNIT_DEATH)
    TriggerAddAction(trg, function()
        DestroyTrigger(trg)
        coroutine.resume(co)
    end)
    coroutine.yield()
end

local function waitUnitEnterRect(u: unit , r: rect)
    local reg = CreateRegion()
    RegionAddRect(reg, r)
    if IsUnitInRegion(reg, u) then
        RemoveRegion(reg)
        return
    end

    local co: thread = coroutine.running()
    local trg = CreateTrigger()
    TriggerRegisterEnterRegion(trg, reg, nil)
    TriggerAddAction(trg, function()
        DestroyTrigger(trg)
        RemoveRegion(reg)
        coroutine.resume(co)
    end)
    coroutine.yield()
end

local function waitUnitSelected(u: unit)
    if IsUnitSelected(u, GetOwningPlayer(u)) then
        return
    end

    local co: thread = coroutine.running()
    local trg = CreateTrigger()
    TriggerRegisterUnitEvent(trg, u, EVENT_UNIT_SELECTED)
    TriggerAddAction(trg, function()
        DestroyTrigger(trg)
        coroutine.resume(co)
    end)
    coroutine.yield()
end

local function waitForStuff(u: unit, r: rect)
    wait(1.0)
    print('A')

    waitUnitSelected(u)
    print('B')

    waitUnitEnterRect(u, r)
    print('C')

    TimerStart(CreateTimer(), 2.0, false, function()
        DestroyTimer(GetExpiredTimer())
        KillUnit(u)
    end)
    waitUnitDie(u)
    print('D')

    wait(1.0)
    print('E')
end

local function init()
    local u = CreateUnit(Player(0), FourCC('Hpal'), 0.0, 0.0, 270.0)
    local r = Rect(256, -256, 768, 256)

    local waitForStuffCo: thread = coroutine.create(waitForStuff)
    coroutine.resume(waitForStuffCo, u, r)
end

With a little bit of grease you can make an adapter that can await multiple different conditions
I don't know how that would work, other than doing a wait loop:

Lua:
...
while true do
    if IsUnitSelected(u, GetOwningPlayer(u)) or IsUnitInRegion(reg, u) then
        break
    end
    wait(0.5)
end
...

Edit: a very "greasey" 'waitFirstOf' implementation:
Lua:
local WaitEventKind = enum
    'WaitDelay'
    'WaitUnitSelected'
    'WaitUnitDie'
    'WaitUnitEnterRect'
end

local WaitEvent = record
    kind: WaitEventKind
end

local WaitDelayEvent = record
    kind: WaitEventKind
    delay: number
end

local function newWaitDelayEvent(seconds: number): WaitEvent
    local x: WaitDelayEvent = {
        kind = 'WaitDelay',
        delay = seconds,
    }
    return x as WaitEvent
end

local WaitUnitSelectedEvent = record
    kind: WaitEventKind
    u: unit
end

local function newWaitUnitSelectedEvent(u: unit): WaitEvent
    local x: WaitUnitSelectedEvent = {
        kind = 'WaitUnitSelected',
        u = u,
    }
    return x as WaitEvent
end

local WaitUnitDieEvent = record
    kind: WaitEventKind
    u: unit
end

local function newWaitUnitDieEvent(u: unit): WaitEvent
    local x: WaitUnitDieEvent = {
        kind = 'WaitUnitDie',
        u = u,
    }
    return x as WaitEvent
end

local WaitUnitEnterRectEvent = record
    kind: WaitEventKind
    u: unit
    r: rect
    reg: region
end

local function newWaitUnitEnterRectEvent(u: unit, r: rect): WaitEvent
    local x: WaitUnitEnterRectEvent = {
        kind = 'WaitUnitEnterRect',
        u = u,
        r = r,
        reg = nil,
    }
    return x as WaitEvent
end

local function registerWaitEvent(trg: trigger, we: WaitEvent, m: {number:WaitEvent})
    local kind: WaitEventKind = we.kind

    if 'WaitDelay' == kind then
        local x = we as WaitDelayEvent
        TriggerRegisterTimerEvent(trg, x.delay, false)
        m[h2i(EVENT_GAME_TIMER_EXPIRED as handle)] = we

    elseif 'WaitUnitSelected' == kind then
        local x = we as WaitUnitSelectedEvent
        TriggerRegisterUnitEvent(trg, x.u, EVENT_UNIT_SELECTED)
        m[h2i(EVENT_UNIT_SELECTED as handle)] = we

    elseif 'WaitUnitDie' == kind then
        local x = we as WaitUnitDieEvent
        TriggerRegisterUnitEvent(trg, x.u, EVENT_UNIT_DEATH)
        m[h2i(EVENT_UNIT_DEATH as handle)] = we

    elseif 'WaitUnitEnterRect' == kind then
        local x = we as WaitUnitEnterRectEvent
        x.reg = CreateRegion()
        RegionAddRect(x.reg, x.r)
        local cond: boolexpr = Condition(function(): boolean
            return x.u == GetFilterUnit()
        end) as boolexpr
        TriggerRegisterEnterRegion(trg, x.reg, cond)
        m[h2i(EVENT_GAME_ENTER_REGION as handle)] = we
    end
end

local orWaitDelay = newWaitDelayEvent
local orWaitUnitSelected = newWaitUnitSelectedEvent
local orWaitUnitDie = newWaitUnitDieEvent
local orWaitUnitEnterRect = newWaitUnitEnterRectEvent

local function waitFirstOf(...: WaitEvent): WaitEventKind
    local co: thread = coroutine.running()
    local trg = CreateTrigger()

    local m: {number:WaitEvent} = {}

    TriggerAddAction(trg, function()
        local we: WaitEvent = m[h2i(GetTriggerEventId() as handle)]
        DestroyTrigger(trg)

        local kind = we.kind
        if 'WaitUnitEnterRect' == kind then
            local x = we as WaitUnitEnterRectEvent
            RemoveRegion(x.reg)
        end

        coroutine.resume(co, kind)
    end)

    for _, we in ipairs({...}) do
        registerWaitEvent(trg, we, m)
    end

    return coroutine.yield() as WaitEventKind
end

local function watFirstOfTestFn(u: unit, r: rect)
    local ev: WaitEventKind

    ev = waitFirstOf(
        orWaitUnitSelected(u),
        orWaitDelay(2.0),
    )
    if 'WaitUnitSelected' == ev then
        print('A1')
    else
        print('A2')
    end

    ev = waitFirstOf(
        orWaitUnitEnterRect(u, r),
        orWaitDelay(5.0),
    )
    if 'WaitUnitEnterRect' == ev then
        print('B1')
    else
        print('B2')
    end

    -- TimerStart(CreateTimer(), 0.5, false, function() KillUnit(u) end)
    ev = waitFirstOf(
        orWaitUnitDie(u),
        orWaitDelay(1.0),
    )
    if 'WaitUnitDie' == ev then
        print('C1')
    else
        print('C2')
    end
end

local function waitFirstOfTest()
    local u = CreateUnit(Player(0), FourCC('Hpal'), 0.0, 0.0, 270.0)
    local r = Rect(256, -256, 768, 256)

    local watFirstOfTestFnCo: thread = coroutine.create(watFirstOfTestFn)
    coroutine.resume(watFirstOfTestFnCo, u, r)
end

It seems that a 'waitAllOf' function would be harder to write though.
 
Last edited:
Status
Not open for further replies.
Top