As we discussed, we will describe the whole game in scripts, and the core functionality
we will define in the core. In this chapter we will be adding Lua to our application.
You do not need to download Lua itself - you’d better install it with your system’s
package manager (yum
or apt
or whatever your Linux uses, brew
for OSX…).
Dependencies
The only thing you need to download from Internet this time is Lua wrapper called luacppinterface. So go and get it from Github.
And unpack it… right to the source
directory of our project! That’s right! That’s
really small library so it will not pollute your project with tons of files.
Now, I mentioned dependency managers earlier. This is how we will handle them in our C++ application - we will simply put the sources of all the libraries we depend on, with the versions we depend on, right in our project. Given that, you may put Irrlicht there as well - you are free to do anything with our project!
Build instructions
To build our project we will need to change our CMakeLists.txt
file to fetch
our new dependency:
cmake_minimum_required(VERSION 3.1)
project(irrlicht_newton_game1)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(SOURCE_FILES source/main.cpp)
set(EXECUTABLE_NAME irrlicht_newton_game1)
set(LUACPPINTERFACE_PATH source/luacppinterface-master)
add_subdirectory(${LUACPPINTERFACE_PATH})
find_package(X11)
find_package(OpenGL)
find_package(ZLIB)
find_package(Lua)
if (NOT IRRLICHT_LIBRARY_PATH)
find_library(IRRLICHT_LIBRARY_PATH
NAMES Irrlicht
PATHS ${IRRLICHT_PATH}/lib/
PATH_SUFFIXES Linux MacOSX Win32-gcc Win32-visualstudio Win64-visualstudio)
message(STATUS "Found Irrlicht: ${IRRLICHT_LIBRARY_PATH}")
endif()
include_directories(${IRRLICHT_PATH}/include ${LUA_INCLUDE_DIR} ${LUACPPINTERFACE_PATH}/include)
add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES})
target_link_libraries(${EXECUTABLE_NAME}
luacppinterface
${IRRLICHT_LIBRARY_PATH}
${X11_LIBRARIES}
${OPENGL_LIBRARIES}
${ZLIB_LIBRARIES}
${X11_Xxf86vm_LIB}
${LUA_LIBRARIES})
And here’s the thing: if you try to compile our project on another machine, you will not need to install any other libraries than Lua on that machine! That supposed to sound like “sweet, huh?”, except that one little “but…“… Bittersweet…
Back to our busines… luacppinterface
needs to be tweaked a bit to fit our project -
we will hack its CMakeLists.txt
file to make it depend on system Lua libraries.
Just make it look like this:
cmake_minimum_required (VERSION 2.6)
project(luacppinterface)
include_directories("lua/src")
find_package(Lua)
include_directories(${LUA_INCLUDE_DIR})
add_library(luacppinterface STATIC
include/luacoroutine.cpp
include/luareference.cpp
include/luacppinterface.cpp
include/luatable.cpp
include/luafunction.cpp
)
target_link_libraries(luacppinterface ${LUA_LIBRARIES})
It barely differs from the original file, but it makes a compilation pleasant - you do not need to specify paths to Lua libs anymore!
Injecting some Lua
Our application now uses C++ code to place some 3D objects in a scene. Let’s move, say, sphere creation, to the script.
First of all, add luacppinterface
headers to our main.cpp
file:
#include "luacppinterface-master/include/luacppinterface.h"
Now let’s look at some of Irrlicht’ conventions:
- it uses
irr::video::IVideoDriver
for rendering operations - it uses
irr::scene::ISceneManager
for scene management
So why not to define a ScriptManager
to handle scripts? Our requirements
for this class (for now) are:
- it should load and evaluate scripts
- it should provide simple API to our scripts
Let’s get coding!
class ScriptManager {
private:
Lua luaState;
std::map<std::string, scene::ISceneNode*> nodes;
video::IVideoDriver *driver;
scene::ISceneManager *smgr;
void bindFunctions();
public:
ScriptManager(scene::ISceneManager *_smgr, video::IVideoDriver *_driver) {
driver = _driver;
smgr = _smgr;
}
void createSphereNode(const std::string name, const std::string textureFile);
void setNodePosition(const std::string name, LuaTable pos);
LuaTable getNodePosition(const std::string name);
void loadScript(const std::string filename) {
std::ifstream inf(filename);
std::string code((std::istreambuf_iterator<char>(inf)), std::istreambuf_iterator<char>());
bindFunctions();
luaState.RunScript(code);
}
};
This is just a skeleton - we will fill it out in a minute. Just catching up:
- this class depends on
IVideoDriver
andISceneManager
to handle 3D objects and the scene - it contains
Lua luaState
field to store the current state of our script running - it stores all the nodes as a
<string, ISceneNode*>
map to allow access to our nodes from scripts - it exposes three methods as an API to Lua scripts:
createSphereNode
,setNodePosition
andgetNodePosition
so we will be able to make some manipulations in our scripts - it provides really short and simple interface to our C++ core:
ScriptManager(...)
andloadScript
The main principle, each and every programmer breaks every day is KISS (Keep It Stupidly Simple). And that principle should guide us through this whole tutorial to not overthink and override ourselves as well as the project we are making. That is why our APIs are that simple.
But let’s get back to our ScriptManager
. It shows how things will look like, but never
defines how they will actually work. So here are the key points to Lua API:
-
LuaTable
is an array-like structure in Lua, representing both indexed as well as key-value arrays in Lua. This type is a way to pass variables between Lua script and C++ program. You may use bothtable.Get<value_type>(index)
andtable.Get<value_type>("key")
methods to access its values. -
To bind our
ScriptManager
methods to Lua functions, we need to use pointers to those functions. And as it is not that simple in usual C++, we will use C++11x lambdas:auto createSphere = luaState.CreateFunction<void(std::string, std::string)>([&](std::string name, std::string tex) -> void { createSphereNode(name, tex); });
-
All the functions and variables you want to pass to Lua scripts should be global. And since we have our pretty
luaState
member, we may set global members through its methods:LuaTable global = luaState.GetGlobalEnvironment(); // ... global.Set("createSphere", createSphere);
-
We will be using just a map of a Irrlicht’ nodes and its name to bypass those nodes between scripts and core:
void createSphereNode(const std::string name, const std::string textureFile) { scene::ISceneNode *node = smgr->addSphereSceneNode(); if (node) { node->setPosition(core::vector3df(0, 0, 30)); node->setMaterialTexture(0, driver->getTexture(textureFile.c_str())); node->setMaterialFlag(video::EMF_LIGHTING, false); } nodes[name] = node; } void setNodePosition(const std::string name, LuaTable pos) { float x, y, z; x = pos.Get<float>("x"); y = pos.Get<float>("y"); z = pos.Get<float>("z"); nodes[name]->setPosition(core::vector3df(x, y, z)); } LuaTable getNodePosition(const std::string name) { LuaTable pos = luaState.CreateTable(); core::vector3df v = nodes[name]->getPosition(); pos.Set("x", v.X); pos.Set("y", v.Y); pos.Set("z", v.Z); return pos; }
Given those, we have our API and are able to create and run our first Lua script.
Add one in the media/scripts/
directory:
createSphere("sphere1", "media/textures/wall.bmp")
Note: paths in the script will be used by C++ core, relatively to the binary file, which is… generated by our C++ code! So all the paths in the scripts are just the same as they are in C++ core.
And add the ScriptManager
initialization code:
ScriptManager *scriptMgr = new ScriptManager(smgr, driver);
scriptMgr->loadScript("media/scripts/test1.lua");
Now you may remove the code, creating sphere in the main()
function. And run the code.
You should see exactly the same picture as before:
Homework
Your task is: try to move all the other “factory” functions (creating cube, ninja,
circle animator for cube and fly animator for Ninja) to Lua script, adding API for them
to ScriptManager
.
More separation
We will now advance our script and add some convention to it. These will be our tasks for the rest of this chapter:
- move keyboard events handling to script
- create two function in script so we may call them by convention, not by configuration
The last phrase I took from Ember.js introduction. It says “prefer convention over configuration”, meaning we’d better call the functions of same name on different scripts, instead of setting somehow which function to call.
That is, we will define handleFrame()
function in our script, which will be called
on each onFrame
event in our C++ core and the main()
function, which will be called right
after script has been loaded.
auto handler = luaState.GetGlobalEnvironment().Get<LuaFunction<void(void)>>("handleFrame");
// ...
handler.Invoke();
Moreover, we will define a global keyboard state table for each of scripts we load and will be updating it as user presses keys on his keyboard. And this variable will be shared with script, but as read-only one. So changes in that table will have no effect on the application itself.
class ScriptManager {
private:
std::map<int, bool> keyStates;
public:
void setGlobalVariables() {
setKeyStates();
}
void setKeyStates() {
LuaTable keysTable = luaState.CreateTable();
for (auto &kv : keyStates) {
keysTable.Set(kv.first, kv.second);
}
luaState.GetGlobalEnvironment().Set("KEY_STATE", keysTable);
}
void setKeyState(int key, bool state) {
keyStates[key] = state;
}
void handleFrame() {
auto handler = luaState.GetGlobalEnvironment().Get<LuaFunction<void(void)>>("handleFrame");
setKeyStates();
handler.Invoke();
}
void loadScript(const std::string filename) {
std::ifstream inf(filename);
std::string code((std::istreambuf_iterator<char>(inf)), std::istreambuf_iterator<char>());
bindFunctions();
setGlobalVariables();
luaState.RunScript(code);
auto scriptMainFn = luaState.GetGlobalEnvironment().Get<LuaFunction<void(void)>>("main");
scriptMainFn.Invoke();
}
};
class MyEventReceiver : public IEventReceiver {
public:
MyEventReceiver(ScriptManager *scriptManager) {
scriptMgr = scriptManager;
for (u32 i = 0; i < KEY_KEY_CODES_COUNT; ++i)
scriptMgr->setKeyState(i, false);
}
// This is the one method that we have to implement
virtual bool OnEvent(const SEvent &event) {
// Remember whether each key is down or up
if (event.EventType == irr::EET_KEY_INPUT_EVENT)
scriptMgr->setKeyState(event.KeyInput.Key, event.KeyInput.PressedDown);
return false;
}
private:
ScriptManager *scriptMgr;
};
Variables are added to a GlobalEnvironment
just as function do:
luaState.GetGlobalEnvironment().Set("KEY_STATE", keysTable);
Lua-defined functions are found by their names and called with Invoke(args)
method:
auto handler = luaState.GetGlobalEnvironment().Get<LuaFunction<void(void)>>("handleFrame");
handler.Invoke();
Let’s add some simple interaction to our script now. I’ll help you a bit:
void moveNode(const std::string name, LuaTable pos) {
scene::ISceneNode *node = findNode(name);
core::vector3df vec = tableToVector3df(pos);
core::matrix4 m;
core::vector3df rot = node->getRotation();
m.setRotationDegrees(rot);
m.transformVect(vec);
node->setPosition(node->getPosition() + vec);
node->updateAbsolutePosition();
}
This is how nodes could be moved relatively to their current position in Irrlicht.
And here’s how our Lua script may look like now:
function handleFrame()
-- w
if KEY_STATE[0x57] == true then
move("sphere1", { x = 0, y = 1, z = 0 })
end
-- s
if KEY_STATE[0x53] == true then
move("sphere1", { x = 0, y = -1, z = 0 })
end
end
function main()
createSphere("sphere1", "media/textures/wall.bmp")
setPosition("sphere1", { x = 0, y = 0, z = 30 })
createCube("cube1", "media/textures/t351sml.jpg")
addCircleAnimator("cube1", { x = 0, y = 0, z = 30 }, 20.0)
createAnimatedMesh("ninja", "media/models/ninja.b3d", "media/textures/nskinbl.jpg", 0, 13, 15)
setRotation("ninja", { x = 0, y = -90, z = 0 })
setScale("ninja", { x = 2, y = 2, z = 2 })
addForwardAnimator("ninja", { x = 100, y = 0, z = 60 }, { x = -100, y = 0, z = 60 }, 3500, true)
end
If you run our application now, you should be able to control sphere with w and s keys: