Jump to content

Coroutine Sequences

Josh

172 views

I am experimenting with a system for creating a sequence of actions using Lua coroutines. This allows you to define a bunch of behavior at startup and let the game just run without having to keep track of a lot of states.

You can add coroutines to entities and they will be executed in order. The first one will complete, and then the next one will start.

A channel parameter allows you to have separate stacks of commands so you can have multiple sequences running on the same object. For example, you might have one channel that controls entity colors while another channel is controlling position.

function Script:Start()
	local MotionChannel = 0
	local ColorChannel = 1

  	local turnspeed = 1
	local colorspeed = 3

  	--Rotate back and forth at 1 degree / second
	self:AddCoroutine(MotionChannel, ChangeRotation, 0, 45, 0, turnspeed)
	self:AddCoroutine(MotionChannel, ChangeRotation, 0, -45, 0, turnspeed)
	self:LoopCourtines(MotionChannel)--keeps the loop going instead of just running once

  	--Flash red and black every 3 seconds
	self:AddCoroutine(ColorChannel, ChangeColor, 1, 0 , 0, 1, colorspeed)
	self:AddCoroutine(ColorChannel, ChangeColor, 0, 0, 0, 1, colorspeed)
	self:LoopCourtines(ColorChannel)--keeps the loop going instead of just running once
end

There's no Update() function! Where do the coroutine functions come from? These can be in the script itself, or they can be general-use functions loaded from another script. For example, you can see an example of a MoveToPoint() coroutine function in this thread.

The same script could be created using an Update function but it would involve a lot of stored states. I started to write it out actually for this blog, but then I said "ah screw it, I don't want to write all that" so you will have to use your imagination.

Now if you can imagine a game like the original Warcraft, you might have a script function like this that is called when the player assigns a peasant to collect wood:

function Script:CollectWood()
	self:ClearCoroutines(0)
	self:AddCoroutine(0, self.GoToForestAndFindATree)
	self:AddCoroutine(0, self.ChopDownTree)
	self:AddCoroutine(0, self.GoToCastle)
	self:AddCoroutine(0, self.Wait, 6000)
	self.AddCoroutine(0, self.DepositWood, 100)
	self:LoopCoroutines(0)
end

I wonder if there is some way to create a sub-loop so if the NPC gets distracted they carry out some actions then return to the sequence they were in before, at the same point in the sequence.

Of course this would work really well for cutscenes or any other type of one-time sequence of events.



6 Comments


Recommended Comments

Very similar to how I did my cutscenes.

It would be nice if Leadwerks supported this as well, but allowed the user-interface to add co-routines. 

For my cutscenes, I have things like

Start Animation,
Go to position,

Go to rotation.

You could even have a "Execute Lua Function".


I like it!

Share this comment


Link to comment

Yes with coroutines. These things are amazingly powerful.

So I'd say while this does work for some situations it's not as flexible. Sometimes you want to run multiple coroutine enabled functions at the same time and not continue until both are done. In a cut scene let's say you want to move from point A to B but also kick off some audio (the bad guy is talking as he's moving). He will reach his destination point before his speech is finished. The next step is to move again, but you want his speech and his first movement to be completed before you continue on.

In my cut scene library I had my coroutine enabled methods return ID's of the coroutine itself, then made a method that would check if that coroutine was completed or not. So I could start both coroutine enabled functions and then do a loop checking if both were in a 'dead' state before continuing.

local moveId = MoveToPoint(...)
local speechId = StartAudio(...)

-- don't recall lua syntax for looping atm
while(IsCompleted(moveId) == false && IsCompleted(speechId) == false)
loop

-- continue with something else

 

I would think at a minimum what you could do is make each script function a coroutine, or maybe let us somehow define that a script function should be coroutine enabled. This is what I did in my state manager library. In the scripts Update() I call my state manager Update() function. You change states by just calling stateMgr:ChanageState("StateName"). You added the script 'self' when creating the state manager because when you changed states it would look for functions of that state in that script by concatenating the state name to _Enter(), _Exit(), or _Update() and since you can search for functions by string name in Lua if the StateName_Enter() or StateName_Exit() function existed it would make a coroutine from them. The Update() was a normal function that looped while in that state, but I found myself using _Enter() more because coroutines rock.

It might be nice to just have state type stuff built into these LE scripts honestly. States usually have an enter, exit, update. You don't need an update as you can get the same behavior by looping in enter and yielding out though. Exit is nice in case there are any cleanup stuff for that state. Wherever you are in that script you can change the state by calling some kind of ChangeState() function. Since you're dealing with coroutines you sort of need that because it tells the underlying system to stop calling that coroutine.

If you're curious on how this works I have the library uploaded to the workshop. It's FSM (Finite State Machine). I think it's easy to use and gets the point across with states that have their enter/exit functions coroutine enabled.

Share this comment


Link to comment

@Rick The whole thing is very interesting because you are programming a "flow" of events instead of a frame-by-frame slice.

Maybe a sequence can be created that has two sub-sequences. Something like "bake the chicken and pour the wine" and then dinner is ready when both sequences are complete.

I am also thinking about a sub-sequence. If your AI is going to a destination point, but they see a powerup on their way there that they want, it makes sense to create a subtask "go get the powerup" and then resume what they were doing. If a peasant is collecting wood and is attacked by an orc, he should fight the enemy and then go back to collecting wood. He would not start the process over, he would go back to the point he left off.

I can think of how to draw that on paper with lines but I don't know yet what the commands would look like.

Share this comment


Link to comment

You are starting to get into the idea of behavior trees a little there (which you should totally implement as well into Turbo as they are amazing for AI). I did implement a general behavior tree with Lua. I found a lua implementation and converted it to work with LE. I implemented that into that dinosaur game me, Tim, and Shad did (which I can't find in the games section for some reason).

https://github.com/tanema/behaviourtree.lua

If you start reading up on behavior trees you see they have the idea of sequences. So the actual task or doing is the thing you originally were talking about with coroutines, but the structuring of all these tasks is what the behavior tree handles. A nice UI to manage the trees is handy as well. I think I used this https://github.com/behavior3/behavior3editor which exported to json and then the library I modified I created a function that loaded the json result of this editor into the behavior tree structure. It was pretty slick actually.

Behavior trees have the idea of interrupts that you can put on any branch that would look for things that should take the AI off the current task and rerun the entire behavior tree again. When behavior trees are being ran through they run through your sequences in order so you always put the highest priority sequence first like getting shot at, or starving. Some sort of self preservation. When you're in a lower priority sequence the interrupt would say check your hunger level and if < 10 it would bail out of the current sequence and rerun the tree, which then the highest priority sequence would see you're hunger and then run it's sub sequences like find food, or cook food, whatever.

You can play around with the editor online: https://www.behaviortrees.com/#/editor

So if you hover over the Nodes header section on the right a New link comes up and that's where you'd make your new node actions like WalkToPoint or condition like IsHungry.

It's actually really cool, very powerful, and fun to play with!

You can even dynamically expand an actors "knowledge" by adding entire behavior trees as sub sequences!  Imagine you creates a behavior tree that just has the sequence of "How to start a fire when cold". Then let's say you have 5 AI around your map. You start them with a basic behavior tree that has sequences like Find Food, Hunt, etc but you give 1 of them the "How to start a fire when cold" sequence. Then you code the game that when 2 actors bump into each other they share sequences in their behavior tree they have so you then give the other actor the "How to start a fire when cold" sequence and now you have AI teaching each other sequences they know. One might be able to expand this to make a more "real" AI that tries to randomly mix and match actions (you'd just have to code a bunch of actions that are available) to create their own sequences and if those sequences help their stats (hunger, thirst, etc) in any way they keep them and if they don't help they remove them! How cool would that be!

 

 

 

 

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...