Advertisement

How to go about C++ bindings for lua?

Started by August 22, 2024 12:37 AM
2 comments, last by jully333gill 3 weeks, 2 days ago

I'm working on a C++ game engine as a hobby project and one of my goals is to be able to make games 100% script-driven with Lua I've decided to go with sol2 for bindings and the part I'm confused with now is what should I bind? For example input I want to do something similar to Roblox Studio since I've used it in the past I'm quite a fan of their api which looks like this

UserInputService.InputBegan:Connect(function(inputObject)
if inputObject.UserInputType == Enum.UserInputType.Keyboard then
print(inputObject.KeyCode,"was pressed")
end)

if UserInputService:IsKeyDown(Enum.KeyCode.A) then
end

Currently, my input module is made up of 3 classes Keyboard, Mouse, and Input the first two provide methods for checking state of buttons while the input class just provides easy access and forwarding SDL2 events to them. If I wanted to replicate the lua code above would it be possible with my existing code or would I want to create some monolithic class or should I have have some wrapper class(es) to bind instead?

Anyway, sorry I made this a bit lengthy and hopefully what I'm questioning makes sense.

Years ago (mid-2000s) we did something like this for a custom engine (in what would be considered the serious games space), binding Lua to engine subsystems and objects with the same goals to write Lua, but with the added goal to be able to script operations for a visual editor that turned out to be very similar in function as UE's Blueprints.

Our approach was to map Lua objects at interfaces we wanted to control in script. For instance, each of what we might consider the major engine subsystems had a Lua binding (e.g. input, audio, graphics, physics, etc). The core Entity had a binding as well (ECS design), and the EntityComponent interface had a binding, allowing us to create new Lua components to define entity behavior that might use the subsystems in various ways (or derive from base components).

We didn't dive into performance much more than necessary to meet objectives, but in retrospect that may have been the weakest aspect of the design. Otherwise, it was a very powerful system (like every other game engine these days). In your case with a hobby project, I doubt you'll have anything to be concerned about there besides just enjoying your project.

Also, since you mentioned Roblox, in case you weren't aware Roblox created their own Lua derivative for their own objectives and open sourced it (luau).

Admin for GameDev.net.

Advertisement

Hello,

No problem, your question makes perfect sense! It sounds like you're working towards a flexible and script-driven approach similar to Roblox Studio's Lua API. First, you need to decide on the high-level API that Lua scripts will interact with. Based on your example from Roblox Studio, you want a way to handle events and query input states. For handling events in Lua, you can provide a mechanism to connect Lua functions to input events. This means you'll need a way to register Lua functions that should be called when certain events occur, such as a key being pressed.

Given your existing structure with Keyboard, Mouse, and Input classes, you have a few options for exposing functionality to Lua:

Event Handling Wrapper:

Create a wrapper class or module in C++ that handles event registration and dispatching to Lua. This class will manage a list of Lua functions that are called when events occur.
For instance, you can have a class UserInputService that registers Lua callbacks for input events. You can then bind this class and its methods using sol2.
Direct Class Bindings:

Bind your Keyboard and Mouse classes directly to Lua. This will allow Lua scripts to query input states directly, but you'll need to handle event-driven behavior (like InputBegan) separately.
Example:

sol::state lua;lua.open_libraries(sol::lib::base);lua.new_usertype<Keyboard>("Keyboard", "isKeyDown", &Keyboard::isKeyDown);lua.new_usertype<Mouse>("Mouse", "isButtonDown", &Mouse::isButtonDown);

Event-Driven Interface:

Combine both approaches: Bind the Keyboard and Mouse classes for querying state, and create a separate UserInputService class to handle event registration and dispatch.

Example:

class UserInputService {
public:
void connect(std::function<void(const InputObject&)> callback) {
callbacks.push_back(callback);
}
void triggerEvent(const InputObject& inputObject) {
for (auto& callback : callbacks) {
callback(inputObject);
}
}
private:
std::vector<std::function<void(const InputObject&)>> callbacks;
};

// Bind the UserInputService to Lua
lua.new_usertype<UserInputService>("UserInputService",
"connect", &UserInputService::connect
);
Integrating with SDL2
You need to ensure that the UserInputService class can receive events from SDL2 and dispatch them to Lua. You'll need to connect SDL2 event handling to the triggerEvent method of UserInputService.

Example Lua Usage
In Lua, your script might look like this:

local UserInputService = require("UserInputService")

UserInputService:connect(function(inputObject)
if inputObject.UserInputType == "Keyboard" then
print(inputObject.KeyCode .. " was pressed")
end
end)

if UserInputService:IsKeyDown("A") then
-- Handle key press
end
Hope that helps!

Advertisement