Action sequences ( Sort of "coroutines" )

Hi everyone, I’m from the gamedev sphere and have my eye on the Odin language. Decided to see if it’s possible to do what I’ve done before in other langs. While I more or less understand how manual memory management works, I don’t have enough practice with it: some high-level stuff like closures that grab fields etc I’m missing, but fully understand why it’s not here.

For example I like to create stacks of actions that are sequentially executed: handy when working on scripts like dialogs or cinematics.

In an ideal example I would try to allocate enough memory in advance, maybe not use pointers and pass everything through handles, but I want to know how correct the code below is:

ActionWait :: struct{
   timer: f32
}
ActionEcho :: struct{
   text: string
}
Action :: struct{
   p: uintptr,
   execute: proc(p: uintptr) -> bool
}

actions : [3]Action
actionsLen : int

wait :: proc(time: f32){
   a:= new(ActionWait)
   a.timer = time
   actions[actionsLen].p = cast(uintptr)a
   actions[actionsLen].execute = proc(p: uintptr) -> bool {
      timer := cast(^ActionWait)p
      timer.timer -= 1
      fmt.println(timer.timer)
      if timer.timer == 0{
         free(timer)
         actionsLen -=1
         return true
      }
      else {
         return false
      }
   }
   actionsLen += 1
}
print :: proc(text: string){
   a:= new(ActionEcho)
   a.text = text
   actions[actionsLen].p = cast(uintptr)a
   actions[actionsLen].execute = proc(p: uintptr) -> bool {
      echo := cast(^ActionEcho)p
      fmt.println(echo.text)
      free(echo)
      actionsLen -=1
      return true
   }
   actionsLen += 1
}

execute :: proc(){
   actions[0].execute(actions[0].p)
}

main :: proc(){
   wait(4)
   print("hello!")
   index := 0
   for {
      a := actions[index]
      if a.execute(a.p){
         index += 1
      }
      if index == 2 do break
   }
}

I’m an Odin beginner too, so take the following with that in mind.

I think a more idiomatic way to write this in Odin would be:

ActionWait :: struct {
    timer: f32,
}
ActionEcho :: struct {
    text: string,
}
Action :: union {
    ActionWait,
    ActionEcho,
}

actions: [dynamic]Action

main :: proc() {
    append(&actions, ActionWait{ 4 })
    append(&actions, ActionEcho{ "hello!" })

    for idx := 0; idx < len(actions); {
        switch &a in actions[idx] {
        case ActionWait:
            a.timer -= 1
            if a.timer <= 0 {
                idx += 1
            }
        case ActionEcho:
            fmt.println(a.text)
            idx += 1
        }
    }
}

Depending on the situation, you could also use custom iterators by adding in these bits & replacing the for loop:

Action_Iterator :: struct {
    index: int,
    data: []Action,
}
make_action_iterator :: proc(data: []Action) -> Action_Iterator {
    return { data = data }
}
action_iterator :: proc(it: ^Action_Iterator) -> (val: Action, idx: int, cond: bool) {
    if it.index < len(it.data) {
        switch &a in it.data[it.index] {
        case ActionWait:
            a.timer -= 1
            if a.timer <= 0 {
                it.index += 1
            }
            val = a
        case ActionEcho:
            fmt.println(a.text)
            val = a
            it.index += 1
        }
    }

    idx = it.index
    cond = it.index < len(it.data)
    return
}

main :: proc() {
    append(&actions, ActionWait{ 4 })
    append(&actions, ActionEcho{ "hello!" })

    it := make_action_iterator(actions[:])
    for a in action_iterator(&it) {
    }
}

Also, in Odin we tend to allocate / free many things together, instead of one by one. If, in your use-case, you work through the whole queue at once, like for example once per frame, you could clear the dynamic actions array at the end of the frame and re-use the memory. Alternatively, since you seem to iterate in order, you could use something like the Queue data-structure in “core:container/queue”, which works like a ring-buffer.

That is what comes to my mind from the top of my head, but maybe more experienced Odin users could chime in with better suggestions.

1 Like

Thanks! Regarding unions… it is good if you know actions in advance, what if I need to add some other stuff for example from my backend?

like:

EngineActions :: union {
   ActionWait,
   ActionEcho,
}

GameActions :: union{
   EngineActions,
   ActionJump
}

Btw, I don’t get any errors by adding “nested” union to a union, but it doesn’t work either. Is it a bug or it’s prohibided to add nested unions?

Not sure what you mean by “not working”. If you encounter an error don’t forget to post the error message and/or the piece of code in question as well otherwise we can only guess what you are trying to do…

So this is only my guess, but you probably tried to directly assign a type of the inner union to the outer one, something like action: GameAction = ActionWait{}.

Nesting of unions does not mean that inner unions will be “unwrapped”

GameAction :: union {
	EngineActions,
	ActionJump,
}
// !=
GameAction :: union {
	ActionWait,
	ActionEcho,
	ActionJump,
}

So if you were to access the inner union, you’d need to do something like this:

// assignment through temporary var of type inner union
eng_action: EngineActions = ActionWait{}
action: GameAction = eng_action

// switching requires 2 steps as well
switch a in action {
case EngineActions: switch ea in a {
	case ActionWait: // ...
	case ActionEcho: // ...
}
case ActionJump: // ...
}

If you have access to both the game and engine actions (you are writing both, so you do…) then just write 1 Actions :: union instead.

3 Likes