Jump to content

Coroutine Sequences

Josh

431 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.

  • Blog Entries

    • By Josh in Josh's Dev Blog 4
      Here are some screenshots showing more complex interface items scaled at different resolutions. First, here is the interface at 100% scaling:

      And here is the same interface at the same screen resolution, with the DPI scaling turned up to 150%:

      The code to control this is sort of complex, and I don't care. GUI resolution independence is a complicated thing, so the goal should be to create a system that does what it is supposed to do reliably, not to make complicated things simpler at the expense of functionality.
      function widget:Draw(x,y,width,height) local scale = self.gui:GetScale() self.primitives[1].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) self.primitives[2].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) --Tabs local n local tabpos = 0 for n = 1, #self.items do local tw = self:TabWidth(n) * scale if n * 3 > #self.primitives - 2 then self:AddRect(iVec2(tabpos,0), iVec2(tw, self.tabsize.y * scale), self.bordercolor, false, self.itemcornerradius * scale) self:AddRect(iVec2(tabpos+1,1), iVec2(tw, self.tabsize.y * scale) - iVec2(2 * scale,-1 * scale), self.backgroundcolor, false, self.itemcornerradius * scale) self:AddTextRect(self.items[n].text, iVec2(tabpos,0), iVec2(tw, self.tabsize.y*scale), self.textcolor, TEXT_CENTER + TEXT_MIDDLE) end if self:SelectedItem() == n then self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos, 0) self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) + iVec2(0,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 2].color = self.selectedtabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,-1) self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,0) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,0) else self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) self.primitives[2 + (n - 1) * 3 + 2].color = self.tabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,2) if n == self.hovereditem then self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor else self.primitives[2 + (n - 1) * 3 + 3].color = self.textcolor end self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 3) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,2) end self.primitives[2 + (n - 1) * 3 + 3].text = self.items[n].text tabpos = tabpos + tw - 2 end end  
    • By 💎Yue💎 in Dev Log 5
      The prototype of a four-wheeled vehicle is completed, where the third person player can get on and off the vehicle by pressing the E key.  To move the vehicle either forward or backward, is done with the keys W, and the key S, to brake with the space key.  And the principle is the same as when driving the character, a third person camera goes behind the car orbiting 360 degrees.

      I don't think the vehicle is that bad, but I'm absolutely sure it can be improved.  The idea is that this explorer works with batteries, which eventually run out during the night when there is no sunlight.
      Translated with www.DeepL.com/Translator
       
      Mechanics of the game.
      I'm going to focus on the mechanics of the game, establish starting point (Landing area), after the orbiter accident on Mars where all your companions died, now, to survive, you will have to repair your suit, oxygen runs out, good luck.  This involves replacing the oxygen condenser that is failing and the suit is stuck.

      On the ground and performance.
      The rocks, the terrain and the vehicle kill the SPF, but there is a solution, and everything is related to the chassis of the vehicle. That is to say that if I put a simple collision bucket for the vehicle, the yield recovers, something that does not happen if I put a collider of precise calculation for the car. This has the advantage of better performance but is not very accurate, especially when the car crashes with an object in front, because the horn of the car has no collision. And the solution to this, is to put a sliding joint, as was done with the area in which the player climbs the car and descends from it.


       
      On the rocks, I am trying to make them with the slightest polygons and the most distant from each other. 
      Obviously on Mars I can not create canyons, high mountains, is because the terrain does not produce shadows on itself, that's why the terrain tries to be as flat as possible, simulating a desert with dunes. 

      That's all for now.
       
    • By 💎Yue💎 in Dev Log 9
      The prototype is finished, and the mechanics of the game can be given way.  It has established a desert terrain in the form of dunes, this implies that there are no cannons or anything similar, because Leadwerks does not allow a terrain to cast shadows on that same terrain and this looks visually rare.
      So the terrain is like low-slope dunes. On the other hand, I think the texture of the terrain is already the definitive one, with the possibility of changes and suggestions on the part of those involved in this project.
      On the other hand we have taken the model of a habitat of the nasa, which certainly looks very nice. 
      The next steps, are to establish the starting point of the player, this must start near the capsule return to Mars somewhere on the map of 2024 x 2.
      And think about the first thing you should do, repair your suit? Seek a shelter? things like that.  


×
×
  • Create New...