Jump to content
Rick

Wait to do something

Recommended Posts

After making a lot of "demos" over the years I find that in game creation there are a lot of points where you want to do something on a given interval.

 

I'm playing around with converting monster.lua to a state machine and it does this in there as well. When in the idle state every 250ms it looks for a target. The part that's sort of a pain is that we make variables (I do the same thing) to hold the last check time. This is fine when it's 1 or 2 but when you have a lot of these kinds of checks or you do this in many games over and over again, it makes sense to think about different ways to do it that won't clutter up the script with "worker" type variables (the cleaner the script the way easier it is to manage later).

 

I don't have the exact idea yet but I'm curious if others have ideas on a better way to handle something like this. It feels like it would be easier to somehow inline the command that says "every 250ms do this code". Something like:

 

function Script:Idle_Update()
  DoEvery(250, function(self)
     self.target = self:ChooseTarget()
     if self.target then
        self.stateMgr:ChangeState("chase")
     end
  end)
end

 

Maybe something built into the entity itself that is specifically for running functions at intervals and it manages tracking that? It would just be nice to have to worry about game type stuff vs "system" type stuff when making games. Now I like making systems, but when I'm making games I prefer using systems and in this case, making a variable to track it and then checking the timing and updating the timing is just busy work distracting me from doing what I really want, which is run some code every X ms.

 

Any thoughts/comments/concerns welcome.

Share this post


Link to post

My version would probably be something like:

 

// Returns true when time interval has passed
bool DoEvery(int* timerindex, int interval, bool recurring);

 

Don't know what this would look like in Lua.

Share this post


Link to post

what is timerindex in your example? Since you're passing a pointer in I assume it's a variable you have to make? The point of the system would be that you don't have to make such a variable yourself to track since it clutters up your code with worker stuff instead of having your code be more intent oriented. The intent of the code is to run some code every interval. Anything else you have to make or check for that to happen isn't part of the intent of your code from a person reading it.

 

 

In C++ imagine having a lambda expression for the code you want to run vs doing an if statement with your function above. Just need to think of a way to automatically track the last time for that specific call.

Share this post


Link to post

It would be nice if there was a way to write code more like this:

PlayAnimation()--plays animation through one time
MoveToPosition(5,4,0)--moves slowly over a period of time
Turn(90)--moves slowly over a period of time
MoveToPosition(6,4,0)--moves slowly over a period of time

 

Know what I mean? Coroutines might be a way to achieve this kind of thing.

Share this post


Link to post

It would be nice if there was a way to write code more like this:

PlayAnimation()--plays animation through one time
MoveToPosition(5,4,0)--moves slowly over a period of time
Turn(90)--moves slowly over a period of time
MoveToPosition(6,4,0)--moves slowly over a period of time

 

Know what I mean? Coroutines might be a way to achieve this kind of thing.

 

 

Yes coroutines can give us that style. I've done that before in le but this thread is about different functionality. It's solving a different problem than that. What you have there is cinema stuff.

 

@game actually and index number would work. In C++ that could be a key to a map. In Lua a key to a table. It still means I'd have to be aware if I had multiple calls like this in a script to use a unique number per call which kind of sucks but I think there isn't any other way around it and at least it doesn't clutter up my Start function with a bunch of worker variables. None that I see in the code anyway as this function would automatically add them to the object and check for their existence at runtime.

Share this post


Link to post

I wrote a small helper for scheduling a callback after x milliseconds. It's been really useful for my AI scripts. It's not locked onto a specific time function so that you can create a timer that will not tick while the game is paused. If you are interested I'll post it but from the user end it looks something like this:

 

function Script:UpdateWorld()
 if self.some_condition_met then
   TimeKeeper:Add(250, self.Dosomething, self, arg_val)
 end
end

function Script:Dosomething(arg)
 --do some work or whatever!
end

 

I've used it for periodic stuff also like this:

function Script:Start()
 TimeKeeper:Add(250, self.Dosomething, self, arg_val)
end

function Script:Dosomething(arg)
 --do some work or whatever!
 TimeKeeper:Add(250, self.Dosomething, self, arg)
end

Share this post


Link to post

Here's an example where this would be useful. I added "Sleep" and "Wake" script functions and modified the sliding door script so it's more event-driven. Now the only reason I need the UpdatePhysics() function is to make the door automatically close after a given period of time:

function Script:UpdatePhysics()
   --Automatically close the door after a delay
   if self.closedelay>0 then
       if self.openstate then
           local time = Time:GetCurrent()
           if time-self.opentime>self.closedelay then
               self:Close()
           end
       end
   end
end

 

A delayed function call would be ideal:

entity:CallFunction("Close",self.closedelay)

 

I like this because it is more event-driven, and it also doesn't require the UpdatePhysics() function to be constantly called each loop. But a big problem is that if the door gets opened again by anything, that delayed Close function is no longer desired. So how do we resolve that?

SlidingDoor.lua

Share this post


Link to post

I suppose one way would be to have another parameter that is a function that needs to return a bool. Before the engine is ready to call the Close function it runs this parameter function and if that returns false it won't run Close? Sort functions sort of work the same way. table.sort takes a function and the return result helps determine the sort. In this case the return result helps determine if it should be ran. We could make it an anonymous inline function since those have access to any Scipt variables which a flag could be set if the door gets opened and that's what you'd return.

 

entity:CallFunction("Close", self.closedelay, function return self.doornotclose end)

Share this post


Link to post

Here's a working version. Notice you can pass in any arguments to the function call (yay Lua!):

function CreateDelayedFunctionCall(object,func,delay,arg0,arg1,arg2)

   --Create table for function call info
   local call = {}
   call.object = object
   call.func = func
   call.args = {arg0,arg1,arg2}
   call.time = Time:GetCurrent() + delay

   --Store the function call
   if DelayedFunctionCalls==nil then
       DelayedFunctionCalls={}
   end
   DelayedFunctionCalls[call]=call

   --Remove the function call
   function call:Cancel()
       DelayedFunctionCalls[self]=nil
   end

   --Execute the calle
   function call:Execute()
       call.func(call.object,self.args[1],self.args[2],self.args[3])
       DelayedFunctionCalls[self]=nil
   end

   return call
end

function UpdateDelayedFunctionCalls()
   if DelayedFunctionCalls then
       local i,k
       local tm = Time:GetCurrent()
       for i,k in pairs(DelayedFunctionCalls) do
           if tm>i.time then
               i:Execute()                
           end
       end
   end
end

 

The door open function now looks like this:

function Script:Open()--in
   if self.enabled then
       self.opentime = Time:GetCurrent()

       --Create delayed function call
       if self.closedelay>0 then
           if self.delayedclosecall~=nil then
               self.delayedclosecall:Cancel()
           end
           self.delayedclosecall = CreateDelayedFunctionCall(self,self.Close,self.closedelay)
       end

       if self.openstate==false then

           self.joint:SetAngle(self.openangle)
           self.openstate=true
           self.component:CallOutputs("Open")

           --Play a sound
           if self.sound.open then
               self.entity:EmitSound(self.sound.open)
           end

           --Play a looping sound
           if self.loopsource~=nil then
               self.loopsource:Play()
           end

       end
   end
end

 

You need to call UpdateDelayedFunctionCalls() once per loop, but it only executes needed functions instead of updating a lot of dormant entities each frame.

Share this post


Link to post

Below is how it would maybe look like with the state manager class I have. The benefit is dropping the need for those flag variables in the script and less nested statements which makes it easier to read and follow.

 

Script.enabled = true --bool "Enabled"
Script.startingState = "Close" --text "Starting State"
Script.openAngle = 90 --float "Open Angle"
Script.closedAngle = 0 --float "Closed Angle"
Script.openSound = "" --path "Open Sound"
Script.closeSound = "" --path "Close Sound"

function Script:Start()
  self.sounds = {}
  self.sounds.openSound = Sound:Load(self.openSound)
  self.sounds.closeSound = Sound:Load(self.closeSound)

  self.stateMgr = StateManager:Create(self)

  -- this automatically looks for functions like Open_Enter, Open_Exit, Open_Update and if there, calls them based on the state
  self.stateMgr:AddState("Open")
  self.stateMgr:AddState("Close")

  -- this just sets the active state but doesn't call the *_Enter() function for it (since in this case we wouldn't want the sound we are doing to play)
  -- and it ignores the enabled flag passed into self.stateMgr:Update()
  self.stateMgr:SetInitialState(self.startingState)
end

-- since in a door there are only 2 states this can be called from flowgraph to get the other state and pass it into ChangeState() below
function Script:GetNegateState()--arg
  if self.stateMgr:GetActiveState() == "Open" then
     return "Close"
  end

  return "Open"
end

-- input from the flowgraph
function Script:ChangeState(state)--in
  self.stateMgr:ChangeState(state)
end

function Script:Open_Enter()
  self.joint:SetAngle(self.openAngle)
  self.entity:EmitSound(self.sound.open)
  self.component:CallOutputs("Open")

  -- change to the close state in 5 seconds (if Open is called again before the state changes to Close the delayed function call will simply reset its timer)
  self.stateMgr:ChangeState("Close", 5 * 1000)
end

function Script:Close_Enter()
  self.join:SetAngle(self.closedAngle)
  self.entity:EmitSound(self.sound.close)
  self.component:CallOutputs("Close")
end

function Script:UpdateWorld()
  -- if self.enabled == false then no states will be changed even if self.stateMgr:ChangeState() is called
  self.stateMgr:Update(self.enabled)
end

 

 

If states were built into entities then the code gets a little cleaner. If that was the case then ideally ChangeState() would be an input to ALL entities on the flowgraph automatically.

 

Script.enabled = true --bool "Enabled"
Script.startingState = "Close" --text "Starting State"
Script.openAngle = 90 --float "Open Angle"
Script.closedAngle = 0 --float "Closed Angle"
Script.openSound = "" --path "Open Sound"
Script.closeSound = "" --path "Close Sound"

function Script:Start()
  self.sounds = {}
  self.sounds.openSound = Sound:Load(self.openSound)
  self.sounds.closeSound = Sound:Load(self.closeSound)

  -- this automatically looks for functions like Open_Enter, Open_Exit, Open_Update and if there, calls them based on the state
  self.entity:AddState("Open")
  self.entity:AddState("Close")

  -- this just sets the active state but doesn't call the *_Enter() function for it (since in this case we wouldn't want the sound we are doing to play)
  -- and it ignores the enabled flag passed into self.stateMgr:Update()
  self.entity:SetInitialState(self.startingState)
end

-- since in a door there are only 2 states this can be called from flowgraph to get the other state and pass it into ChangeState()
function Script:GetNegateState()--arg
  if self.entity:GetActiveState() == "Open" then
     return "Close"
  end

  return "Open"
end

function Script:Open_Enter()
  self.joint:SetAngle(self.openAngle)
  self.entity:EmitSound(self.sound.open)
  self.component:CallOutputs("Open")

  -- change to the close state in 5 seconds (if Open is called again before the state changes to Close the delayed function call will simply reset its timer)
  self.entity:ChangeState("Close", 5 * 1000)
end

function Script:Close_Enter()
  self.join:SetAngle(self.closedAngle)
  self.entity:EmitSound(self.sound.close)
  self.component:CallOutputs("Close")
end

function Script:UpdateWorld()
  -- wouldn't need anything state related here as that could be updated by the engine
end

 

I actually just thought about having ChangeState() accept a delayed time. I think that's a useful idea with the state manager class.

Share this post


Link to post

I don't want to edit my post above because it screws up the formatting I worked so hard to get :).

 

One of the bigger things to notice is that you end up with nice and small functions that are very concise. It also helps remove nested if's. In your example you go 3 layers deep. That's what happens when you're managing states in a more brute force manner. It's unavoidable almost when doing things that way. With states those are masked in the state manager class making the gameplay code easier to read and maintain. This was a simple door but the AI scripts have even more nesting levels and a lot more bigger functions doing more than they should. State managers help keep functionality separated into their own concise methods.

 

Combined with adding a delayed state change handled automatically by the state manager I think it offers a good 2 for 1 deal :)

 

Note the idea of automatically giving every entity node on the flowgraph a ChangeState(state) input. It could also benefit from automatically giving outputs for Enter()/Exit() for every state defined as well. This is where I think having the ability to add states via the editor would help at design time. It makes the code even less and cleaner.

Share this post


Link to post

Why would you want to replace the self-apparent "Open" and "Close" functions in the flowgraph with an ambiguous "ChangeState" function?

Share this post


Link to post

With the engine as is you probably wouldn't (I did just to setup the idea of the 2nd code snippet where it was built in but the users wouldn't have to code it as each node would get it automatically). If LE had states built into entities then it would be a more generic way of changing states for any entity and people would be used to the idea of entity states to direct the flow to the functionality they want.

 

For a simple door you're right why bother with that. But for an object that has more states having one input vs all possible it starts to make more sense from a visual node perspective. Also it's a common interface for all entities. However instead of the current arg method it would be best to have some kind of drop down that lists all possible states that one could change too and that hard coded value from the flowgraph is passed in for that connection.

Share this post


Link to post

Ah cool.

 

Play around with my code above and see how well this works in real usage.

 

I feel like there's some kind of higher-level script feature we could add that would make life simpler and easier, but I am not seeing it yet.

Share this post


Link to post

If I had to critique it I'd say:

 

- Replace your arg values at the end with 3 dots (...) as that's how you get unlimited arguments. Inside a function is a special variable then called arg which is a table of all the arguments that could be in the ... When you pass them to the function to call you can pass the arg var to the unpack() function and it'll place them into each individual parameters of the users function by order they are in the arg table. That's makes your function truly generic so we don't have to make our own if we want more arguments.

 

- I think you'd want to remove the call after it was called once. So giving that object to the user could invalidate the object if you did that which might lead to them using it. If you don't remove the object it's function will just keep getting called which isn't desirable.

 

 

[Philosophical rant]

Simpler and easier comes with structure and less choices. More choices (freedom) is harder. You're providing little functions here and there but hesitant to provide structure around how people make games with Leadwerks. We see game engine succeed both with freedom and structure. Structured game engines are often more newbie friendly.

 

What I see is that you dip your toe into structure but don't go all the way in. One could really take the Leadwerks API and make a very structured game engine from it because it's so free and open. This doesn't mean "here is a fps template and you mod it". It really means providing the common game design structures for your users.

 

The pathfinding is an example of toe dipping by you and it was a HUGE success! There are more structured systems out there that you can adapt to Leadwerks though. You went big with the pathfinding but you're hesitant to go big with other things and your mind goes to little functions here and there vs implementing a system. You implemting Raycast DEFINED how people do pathfinding in Leadwerks. You removed the freedom (not really but when you officially do something the majority use it) and people loved it! Think bigger than single functions.

 

[edit]

 

Just for the record I think behavior trees for ai is that next thing that would be as big as pathfinding was. Then I think state machines built in for structure is another. Finally a cinema maker using coroutines would be on that list too.

Share this post


Link to post

Here's the code modified to use unlimited arguments:

function CreateDelayedFunctionCall(object,func,delay,...)
--Create table for function call info
local call = {}
call.object = object
call.func = func
call.args = {...}
call.delay = delay
call.time = Time:GetCurrent() + delay

--Store the function call
if DelayedFunctionCalls==nil then
	DelayedFunctionCalls={}
end
DelayedFunctionCalls[call]=call

--Remove the function call
function call:Cancel()
	DelayedFunctionCalls[self]=nil
end

--Execute the calle
function call:Execute()
	if self.args~=nil then
		self.func(self.object,unpack(self.args))
	else
		self.func(self.object)
	end
	DelayedFunctionCalls[self]=nil
end

return call
end

function UpdateDelayedFunctionCalls()
if DelayedFunctionCalls then
	local i,k
	local tm = Time:GetCurrent()
	for i,k in pairs(DelayedFunctionCalls) do
		if tm>i.time then
			i:Execute()				
		end
	end
end
end

Share this post


Link to post
The pathfinding is an example of toe dipping by you and it was a HUGE success! There are more structured systems out there that you can adapt to Leadwerks though. You went big with the pathfinding but you're hesitant to go big with other things and your mind goes to little functions here and there vs implementing a system. You implemting Raycast DEFINED how people do pathfinding in Leadwerks. You removed the freedom (not really but when you officially do something the majority use it) and people loved it! Think bigger than single functions.

Whenever I read discussions about this kind of thing, in any forum, anywhere, there are a few things that turn me off from it:

  • There's always a lot of indecipherable diagrams that have no obvious relation to game behavior.
  • I never understand what they are talking about.
  • They can never give any examples except extremely simplistic ones, like a page of code to make a light turn on.
  • It seems more about conforming to some abstract ideal than actually getting work done.

 

If you want to impress me, show me how something like this would be able to achieve the exact same result as one of our existing scripts, using less code.

Share this post


Link to post

I'll replicate the LE monster with my behavior tree lib I made work with LE. I used this in our Dino game for the dinosaurs but they were pretty basic and people probably didn't notice the fact that they found food and would remember where food was as they came across it and go there if they got hungry later.

 

Share this post


Link to post

CrawlerBehaviorTree.png

 

I think this covers the main idea of the crawler. Now I just fill in code for each node. The oval ones are conditions so they are simple. Just checking flags in the crawler script. The rounded rectangles are the actions. I'll have to translate what you have in the crawler code for those. I have family over this weekend but hoping to get to those Sunday evening sometime.

 

After I do that I had an idea to expand it to have the crawler look for a health pack if its health is less than 50, and if it's health is less than 25 to explode smile.png. That would look like the below. The cool thing is with this editor (http://behavior3js.guineashots.com/editor/#) I can map out how I want my AI to look before doing any code. Once I get how I want the AI to work I go and create the code for each node which is its own LUA file in the way I have the system setup. My vision would be we have a big library of nodes that anyone can create and people can piece them together with the visual editor to make different behaviors. Some behaviors would require certain things to exist in the world editor. For example hiding behind a barricade would require something to be classified as a barricade so the node could possibly do a ForEachEntityInAABBDo() looking for the entity defined as a barricade and figure out which side it needs to be on (where is it being shot from). Then a person simply drops that node in here and their AI now has that functionality.

 

CrawlerBehaviorTree2.png

Share this post


Link to post

Join the conversation

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

Guest
Reply to this topic...

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