Game Engine, C 1, P 12, RAII idiom
I want to begin development on the engine. It is great to get something up and running fast. Look at all these great engines like Unity and Unreal! They have so many features, such good graphics. I can do that quick, right? In reality, no. I can probably make something nice using the "Demoscene" approach, but the engine would be lacking so much, that it will be unusable at all.
To begin with, I need a good basis for the engine. I can start making engine layers and systems, tightly coupling them together and making a pointer-based context interface. But the problem lies in tight coupling. In case I want to change something, I'll need to rewrite a lot of code. This isn't great for supportability and future development.
VUL
I decided to create Vivid Utility Library to help me break my engine into separate systems. Then inside the actual engine, I'll be able to use my own and external libraries to their full extent to bring the best quality possible. Supporting and upgrading separate sub-systems is much easier too.
A few examples
I think that what I've said is not controversial and straight-forward. However, it's nice to have a few examples to make an emphasis, that this is a common practice.
WebGL Studio is an online game engine with a great set of broken apart subsystems. LiteGL library wraps around OpenGL functions, LiteGraph is a node graph solution for the editor, LiteGUI manages UI.
Ez Engine is another great example of a game engine with a nice broken apart list of subsystems. It makes heavy usage of other projects and libraries, which allows the creators to focus on the important parts first.
Contents of VUL
After I was set up to create VUL, it was time for me to consider, what do I want to put inside. The obvious answer to that is - anything that can be used separately without modifications, but is created specifically for the engine and other tools that will be developed and used alongside it.
RAII Idiom
Now that I have a place to put my utilities, I can move to actually creating them. I decided to start off with something simple - the RAII (Resource acquisition is initialization) idiom. The basic idea - you allocate resources and construct the object on initialization, then deallocate and destroy on scope end. It is a basic idiom that is used throughout the standard library and user projects, but it is not always safe to deallocate resources when scope ends, if the resources were passed to another place and will be used in the future.
RAII depiction |
Because of that, important parts of the engine, that need to have specific manual resource deallocation, aren't following this idiom. But what if I want to use it anyway?
Solution
One solution would be to use unique or shared pointers from the standard library. This is robust and tested, but these constructs carry a lot of additional logic besides them. This is why I decided to make a tiny RAII wrapper to help me with testing and other things. It has features similar to the
There is nothing complicated in the code, linked above, so I highly suggest you take a look at it and figure out how it works on your own, if you want it. It is many times simpler than the unique pointer from the standard library, but it may be useful to me in the future.
unique_ptr
constructor and make_unique
. The main difference is that it works better with my initialization and termination methods.There is nothing complicated in the code, linked above, so I highly suggest you take a look at it and figure out how it works on your own, if you want it. It is many times simpler than the unique pointer from the standard library, but it may be useful to me in the future.
The only piece of code worth showing here is the constructor:
template <class ...Args>
raii(_Ty&& value, Args... args) : _value(value)
{
if constexpr (
sizeof...(Args) == (std::size_t)1 &&
std::is_same<first_type<Args...>::type, raii_dummy>::value
)
{
_init = _value.init();
}
else
{
_init = _value.init(std::forward<Args>(args)...);
}
}
As you can see, during compilation I check for a dummy struct, if I find it, initialize the value with no parameters. Otherwise, forward input parameters to the initialization. It works great and allows me to safely wrap my engine systems. The corresponding commit is here.
In the next article I'll focus on the managed singleton pattern.
Comments
Post a Comment