Okay, it's time to write another part about the refactoring I've done. It's also contains the hardest part of the refactoring!
Here's some small stuff I've done:
1. Lua enums.
I've written about those already. They work pretty good
2. LuaEntityHandle. Read more about this technique
here.
This thing allows me to write stuff like this:
entity:setAnimation("walk")
instead of
setAnimation(this, talk)
A minor change, but the one I like very much, because it makes scripts easier to read
3. Lua/C++ events. Written about it
here.
4. And now for the hardest part. I didn't really think too much about header dependencies because I try to not include stuff in headers, use forward declaration as possible, try to decouple classes, etc. But changes in some classes caused lots of recompile for no reason. You now, like file changing A.h caused B.cpp which didn't include A.h in any way to recompile. I've searched for hours for the root of the problem. I've created a big graph of includes in my code but found no reason for this thing. And here comes the worst part: when I created new functions and registered them in LuaBridge, this caused almost full recompilation of the whole project (which took 4-5 minutes!). This was not acceptable. This would limit me a lot, because I wouldn't want to add C++ functions which can be called for Lua or change some classes which caused a lot.
And then I found it. It was LuaBridge.h. I created a quick project to test things out. So, suppose you have two classes A and B which are not related in any way. You register A class in A.cpp and do some stuff with LuaBridge in B.cpp. So, A.cpp and B.cpp have LuaBridge.h included in them. And now suppose you make some changes in class A and compile. B.cpp compiles too! What? This is what LuaBridge does. I think that compiler generates some weird template code again which causes LuaBridge.h to recompile causing ALL files which include LuaBridge.h to recompile.
As you may imagine, lots of files in my project included LuaBridge.h. This is what caused the problem! And I've found the solution. It was to make
LuaScriptManager which included LuaBridge.h in LuaScriptManager.cpp but not in LuaScriptManager.h, so if you change some stuff and LuaBridge.h needs to recompile, only LuaScriptManager.cpp is recompiles, but all the files which use LuaScriptManager.h are not recompiled!
But making good interface for LuaScriptManager was a big challenge. There were two problems:
Problem A. I needed to make some templated functions which used LuaRef in them (like T getData<T>). This meant that I needed to include LuaBridge.h in LuaScriptManager.h which would cause the same problems again!
Here's how I solved it:
You can declare a template function in .h but define it in C++ if you do template specialization. But this proved to be bad, because a lot of the code had mostly common stuff in it and I didn't want to just copy-paste stuff. And then I've found out about one of the coolest template features ever. It's called explicit template instantiation. So, all you have to do is to define the template function in .cpp and then put explicit instantiations in .cpp for all the types you want this function to use with. This limits you to only those types, but I was okay with that, because getData was used with only a number of classes like int, float, bool, etc.
So, here's how the code looks:
// LuaScriptManager.h
class LuaScriptManager {
...
template <typename T>
T getData<T>(...);
...
};
// LuaScriptManager.cpp
...
template <typename T>
T LuaScriptManager::getData<T>(...) {
// template code
}
// explicit instantiations
template bool LuaScriptManager<bool> getData(...);
template int LuaScriptManager<int> getData(...);
... // etc.
So, this causes compiler to generate all this functions in .cpp file, so you don't have to have template function definition in your .h file which enables you to not put code into headers.
Problem B. I needed to find some wrapper around LuaRef which could act like LuaRef, but didn't require files where it's used to include LuaBridge.h. This one was easy, I just created class like this:
namespace luabridge {
class LuaRef;
} // forward declaration
class LuaRefWrapper {
template <class T>
T cast() const;
...
std::unique_ptr<luabridge::LuaRef> refPtr;
};
This also allowed me to get rid of some explicit template instantiations in LuaScriptManager which made code a lot better. I can just use LuaRefWrapper.h in LuaScriptManager.h without including LuaBridge.h! So, I got rid of the dependency, but I also got a few good things out of LuaScriptManager too, of course.
1) Hiding stuff/error checking inside the scripts
Suppose I have entity definition that looks like this:
someEntity = {
...,
GraphicsComponent = {
filename = "res/images/someEntity.png",
...
}
}
Suppose I want to get someEntity.CollisionComponent.collide. Here's what I had to do previously:
auto entityTable = luabridge::getGlobal(L, "someEntity");
auto componentTable = entityTable["GraphicsComponent"];
auto filenameRef = componentTable["filename"];
std::string filename;
if(filenameRef.isString()) {
filename = filenameRef.cast<std::string>();
} else {
filename = "res/images/error.png";
}
There are lots of things that can go wrong. What if one of the tables is not found? Should add checks for this too?
Here's what I can do now:
auto componentTable = scriptManager.getTable("someEntity.GraphicsComponent");
auto filename = scriptManager.getData<std::string>("filename", "res/images/error.png", componentTable);
Much better! If one of the tables is not found, the program doesn't crash and LuaScriptManager tells me about the error. If the value is of the wrong type, the script manager tells me about this too. If there are some errors, getData returns the default value which I provided as the second argument.
2) Overwrite
One functionality I've implemented a long ago was entity "inheritance". Suppose I have one entity with lots of components. And now I decide to create almost the same entity, but with another sprite. Here's how I can do that:
base_entity = {
...
}
derived_entity = {
template_init = function(this)
this:copyEntity("base_entity") // copies everything from base_entity
end,
GraphicsComponent = {
filename = "res/images/newSprite.png"
}
}
The difference between default loading and this type of loading is that I don't care if the value is not found in derived entity. If this is a case, I just don't change the value. If the value is found, I replace it with a new one. And here's what I had to do previously:
auto entityTable = luabridge::getGlobal(L, "someEntity");
auto componentTable = entityTable["GraphicsComponent"];
auto filenameRef = componentTable["filename"];
if(!filenameRef.isNil()) {
filename = filenameRef.cast<std::string>();
}
Notice how it's almost the same as the previous example. Can I implement it better with new scriptManager? Sure
auto componentTable = scriptManager.getTable("someEntity.GraphicsComponent");
auto filenameRef = scriptManager.getRef("filename", componentTable);
if(filenameRef.isNil()) {
filename = filenameRef.cast<std::string>();
}
This almost looks like LuaBridge code above! So I came up with a better solution, now I can do stuff like this:
scriptManager.getData(filename, "res/images/error.png", "filename", componentTable, isOverwrite);
Here's what's going on. Fist of all, notice that I don't have to specify template argument because of the type deduction which happens because I pass the first argument.
If
isOverwrite == false, this works as
getData<std::string> that I showed above. If
isOverwrite == true, then I rewrite the value only if it's found. But I don't write about errors that some tables or values are not wrong, because some values may be missing in derived entities and that's fine.
So, this one line works for two cases: loading value expecting to find it and trying to overwrite value only if the value is found.
So, all this stuff lead me to great simplification of script loading code, allowed me not to worry about the errors in scripts and reduced a lot of header dependencies. This also allows me to replace LuaBridge with some other library in the future only by changing LuaScriptManager and not bothering about other code which is very good.
I'm very glad I did this epic refactoring/engine improvement. Now I'm a lot more confident in the engine and I'm already making some progress about the game.