Jump to content

How to Expose C++ Classes to Lua in Ultra Engine


Josh

7,905 views

 Share

I'm using the excellent sol2 library to interface C++ and Lua in the upcoming Turbo Game Engine. I've decided not to create an automatic header parser like I did for tolua++ in Leadwerks 4, for the following reasons:

  • There are a lot of different options and special cases that would probably make a header parser a very involved task with me continually discovering new cases I have to account for.
  • sol2 is really easy to use.

Each class I want available to Lua will have a static function the Lua virtual machine code can call when a new Lua state is created. The new_usertype method is able to expose a C++ class to Lua in a single command. At a minimum, the name of the class and the base class should be defined. This method can accept a lot of arguments, so I am going to break it up over several lines.

void Vec3::InitializeClass(sol::state* luastate)
{
	//Class
	luastate->new_usertype<Vec3>
	(			
		//Name
		"Vec3",

		//Hierarchy
		sol::base_classes, sol::bases<Object>()
	);
}

We can export members to Lua very easily just by adding more arguments in the call to new_usertype:

//Members
"x", &Vec3::x,
"y", &Vec3::y,
"z", &Vec3::z,

Metamethods are special operations like math operands. For example, adding these arguments into the method call will set all the metamethods we want to use.

//Metamethods
sol::meta_function::to_string, &Vec3::ToString,
sol::meta_function::index, [](Vec3& v, const int index) { if (index < 0 or index > 2) return 0.0f; return v[index]; },
sol::meta_function::new_index, [](Vec3& v, const int index, double x) { if (index < 0 or index > 2) return; v[index] = x; },
sol::meta_function::equal_to, &Vec3::operator==,
sol::meta_function::less_than, &Vec3::operator<,
sol::meta_function::subtraction, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator-),
sol::meta_function::addition, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator+),
sol::meta_function::division, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator/),
sol::meta_function::multiplication, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator*),
sol::meta_function::unary_minus, sol::resolve<Vec3()>(&Vec3::operator-),
sol::meta_function::modulus, &Vec3::operator%,	

And then finally the class methods we want to use can be exposed as follows:

//Methods
"Length", &Vec3::Length,
"Cross", &Vec3::Cross,
"Normalize", &Vec3::Normalize,
"Inverse", &Vec3::Inverse,
"Distance", &Vec3::DistanceToPoint

In C++ you can not retrieve a pointer to a function, so we are going to create a quick Lambda expression and expose it as follows:

//Constructor
luastate->set_function("Vec3", [](float x, float y, float z) {return Vec3(x, y, z); } );

This allows us to create a Vec3 object in Lua the same way we would with a constructor in C++.

Here is the complete Vec3 class initialization code, which makes Lua recognize the class, exposes the members, adds math operations, and exposes class methods:

void Vec3::InitializeClass(sol::state* luastate)
{
	//Class
	luastate->new_usertype<Vec3>
	(			
		//Name
		"Vec3",

		//Hierarchy
		sol::base_classes, sol::bases<Object>(),

		//Members
		"x", &Vec3::x,
		"y", &Vec3::y,
		"z", &Vec3::z,
		"r", &Vec3::x,
		"g", &Vec3::y,
		"b", &Vec3::z,

		//Metamethods
		sol::meta_function::to_string, &Vec3::ToString,
		sol::meta_function::index, [](Vec3& v, const int index) { if (index < 0 or index > 2) return 0.0f; return v[index]; },
		sol::meta_function::new_index, [](Vec3& v, const int index, double x) { if (index < 0 or index > 2) return; v[index] = x; },
		sol::meta_function::equal_to, &Vec3::operator==,
		sol::meta_function::less_than, &Vec3::operator<,
		sol::meta_function::subtraction, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator-),
		sol::meta_function::addition, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator+),
		sol::meta_function::division, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator/),
		sol::meta_function::multiplication, sol::resolve<Vec3(const Vec3&)>(&Vec3::operator*),
		sol::meta_function::unary_minus, sol::resolve<Vec3()>(&Vec3::operator-),
		sol::meta_function::modulus, &Vec3::operator%,			
			
		//Methods
		"Length", &Vec3::Length,
		"Cross", &Vec3::Cross,
		"Normalize", &Vec3::Normalize,
		"Inverse", &Vec3::Inverse,
		"Distance", &Vec3::DistanceToPoint
	);

	//Constructor
	luastate->set_function("Vec3", [](float x, float y, float z) {return Vec3(x, y, z); } );
}

To add your own C++ classes to Lua in Turbo, you will create a similar function as above and call it at the start of the program.

  • Like 1
 Share

8 Comments


Recommended Comments

If you want to expose multiple versions of the same function you can do it like this:

sol::meta_function::subtraction, sol::overload( sol::resolve<Vec3(const Vec3&)>(&Vec3::operator-), sol::resolve<Vec3(const float)>(&Vec3::operator-) ),

 

Link to comment

sol doesn't do default function arguments, so you have to create an overload with a Lambda expression. The World::Update function has the following syntax:

void World::Update(const int ups = 30)

This exposes the function but requires a single argument be supplied:

L->new_usertype<World>
(
	"World",
	sol::base_classes, sol::bases<SharedObject, Object>(),
	"Update", &World::Update
);

We will use sol::overload and a Lambda expression to add a version of the function that does not require any arguments.

L->new_usertype<World>
(
	"World",
	sol::base_classes, sol::bases<SharedObject, Object>(),
	"Update", sol::overload( &World::Update, [](shared_ptr<World> world) { world->Update(); } )
);

It seems kind of complicated, but that is literally ALL you have to do to add new classes. There are no funny headers or other files to deal with. That's it.

Link to comment

This script code now works:

--Create a window
local window = CreateWindow("Game",0,0,1024,768,WINDOW_TITLEBAR)

--Create a rendering context
local context = CreateContext(window)

--Create the world
local world = CreateWorld()

--Create a camera fpr rendering
local camera = CreateCamera(world)
camera:SetPosition(0,0,-3)

--Create a box
local box = CreateBox(world)

for n=1,#box.lods do
    local lod = box.lods[n]
    for k=1,#lod.surfaces do
        local material = lod.surfaces[k].material
        if material ~= nil then
            local texture = material.textures[1]
        end
    end
end

while window:KeyHit(KEY_ESCAPE)==false and window:Closed()==false do

    --Make the box spin
    box:Turn(0,1,0)    

    --Update physics and other stuff
    world:Update()

    --Send a frame to the rendering thread
    world:Render(context)

end

 

  • Upvote 1
Link to comment

As much as I can.

We also have getters/setters:

L->new_usertype<Entity>
(
	"Entity",
	"position", sol::property(&Entity::GetPosition, &Entity::SetPosition)
);

This will allow Lua code like this:

local box = CreateBox(world)
box.position = Vec3(10,0,0)

And it will actually call the SetPosition() functions, which updates the entity matrix, triggers changes in surrounding shadows, etc., instead of just simply setting a variable, which would not work.

https://sol2.readthedocs.io/en/latest/api/property.html

Link to comment

Yes. They have to be because they have additional parameters for global / local and you need that option available.

  • Thanks 1
Link to comment

I just realized that with this system, any object could have a script attached to it now and it would work just fine.

Link to comment
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...