So you've implemented lua into your game engine except now you're asking yourself, "When do I run my lua scripts and call the lua functions?" This is actually a pretty complicated question and requires a lot of time and thought to structure out what belongs where and how things interact with each other. If you've read an earlier blog post of mine then you've heard me say that the number one rule is: Minimize the number of times you have to go back and forth between lua and C+. Moving forward with the article we are going to continually refer back to this rule.
You need to call your lua scripts somewhere in your code so let's start off by figuring out where exactly we call the scripts from. Lets take a look at an example of the flow of logic across a single frame.
Pretty straight forward, we check for input, run game logic, progress our physics simulation a step, update our audio system, draw to the screen, and then repeat. So now let's take a look at each step and decide if that step involves lua and then later we'll go into how to structure it.
Input: All we are doing here is filling our keyboard state, gamepad state, and mouse state structs so that things like gameplay code or scripts can use it. Lua shouldn't be called here.
Game Logic: This is what makes our game a game, we run all the code or scripts that have to do with our gameplay logic here, so it makes sense that we should also call lua here.
Physics: Our physics engine is probably in C++ so we aren't going to call lua to do the simulation, when two objects collide however we might want to have some sort of gameplay logic however which may be in lua.
Audio: Most likely we're using something like FMOD or Wwise which you update every frame by calling their API's update function. I'm not sure why you would need lua in this step since any function calls to play a sound should be done inside of your game logic step.
Graphics: We need to draw things to the screen and this is where we do it. There are probably C++ sprite components which have a Draw function that gets called here and if we have lua scripts that draw things then we probably want to call those here.
So 3 out of our 5 steps involve lua scripts, Game Logic, Physics, and Graphics. Before we take a look at each step individually let's learn about something I call event hooking (or event handling.)
If I have 100 components to update wouldn't it be great to just call one function which updates all of them? Or better yet, if i'm in C++ and I want to run 100 lua functions involving gameplay logic, wouldn't it be great to just make one lua function call which then calls the 100 other functions so I don't have to jump back and forth between lua a hundred times? Of course it would be so let's make ourselves a way of "hooking" functions to an event, and then when we call that event all of the hooked functions get called!
Imagine this as having different "hooks" each corresponding with a specific event for instance Game Logic Update, Frame Update, Draw, User Interface Draw, ect. We "hook" functions and their context (in C++ this would be a pointer to an instance of the object we want to run the function on, in lua it would be the object's table) to the event and then when the event gets called we in turn call each hooked function. This is important since that means we can make a single lua function call from C++ which will then call the rest of the lua function calls we wanted to make.
A Hook System as described is very easy to implement and can be less than a hundred lines in lua. Here is an example interface:
--[[ Hooks a function and a context to an event. The context will be passed in as the self variable to the function ]]--
Hooks:Add(event, context, func)
--[[ Removes a hooked function and context from an event. ]]--
Hooks:Remove(event, context)
--[[ Calls an event, ... so we can support a variable number of arguments ]]--
Hooks:Call(event, ...)
Now that you know what Event Hooking is let's talk about examples of using it.
An inefficient way of running update logic on all of your lua scripts would be to call each update function individually from C++, there is any easier way however. With using our hook system updating game logic lua scripts we just have to make one function call. The only thing we need to do is hook any functions that need to be run every update to an event like Logic Update or Frame Update and then call the event on our lua hook system.
By doing this we've drastically cut down on the number of transitions between lua and C++ which will greatly help our performance. If you end up needing more than just one hook call to lua inside of your game logic step then you might think about creating a GameLogicUpdate lua function which then calls each hook you need in their proper order. This however will give you only minimal benefits and might make your code harder to understand.
The only lua involvement that will happen during the physics step of your game engine will involve game logic that needs to be run upon two objects colliding with each other. It's probable that this will only ever happen for a few objects but it's important to make a solution that will scale nicely since you never know where you game might take you. Personally I've never had to deal with it but these are the steps I would take.
First off we need to cache every single instance of collision that happens in our physics step. This will probably be easy since we may already have a list of manifolds we use to resolve collisions between objects. Now that we have that list we should filter out any collision instances that we don't actually want to trigger a callback for, or keep them all, it's up to you.
Now we have a list of collisions (which should contain handles or pointers to the two game objects involved in each collision!) and we are going to send that to a lua in a single function call which will process each collision instance and run any necessary callback functions. Except for oh wait, we don't have anything to handle that yet! We have our hook system except that works for global events, not object specific events. Taking care of this should be a simple task of encapsulating an instance of our hook system into each lua object we have so we can hook functions to events which are object-specific which will not only allow us handle this situation but many others involving per-object events!
This is a big once especially if you're doing lots of draw calls from inside of lua. Similar to what we discussed in the Game Logic step we need to hook every single lua draw call and then simply trigger the event in C++ which will in turn call all of the lua functions needed. I'm not going to make a diagram for this since it's essentially the same thing as Game Logic.
In my Sophomore game project we had multiple draw hooks which were called per frame: Draw, PostDraw, and GUIDraw each with their own purpose. We did however run into problems caused by lua making hundreds of C++ draw calls per frame which breaks our rule of "minimize the number of transitions between lua and C++." We can solve this issue however by batching all of our C++ draw calls into one single call by using an array of all draw data.
Depending on how you use lua there may be many more times that you want to call lua functions from C++ and by no means should you try and restrict your calls to the three steps we've discussed. For instance I've heard of people actually using lua scripts as a way of creating their levels, it's essentially a list of instructions on what objects to create, where they should be, and other attributes they have.
Another very important instance that I left out involves tracking the creation and deletion of game objects inside of your engine. This will be discussed in another article involving the structure of your lua interface.