About fifteen days ago I've posted about smart pointers and ownership. After laying out the rules for a piece of the system I wanted to code – namely, a way to iterate over instances of a type, following some restrictions and demands – I ended up concluding that maybe I should try to use handles. With the little time I have to code side projects, this ended up being a heavier task than expected. Because of c++ stuff I didn't know, didn't remember, didn't understand, or any permutation of these, I had to throw way many attempts.
Soooooo… the truth is, things got too complex and I decided to fall back into KISS (Keep it Simple, Stupid). Planning a bigger scope set of rules and demands would take too much of the little time available, so I've used a set of rules for the containers themselves, with handles.
[r1] – Containers are the unique owners of the data stored there.
[r2] – Contained data must be created within the container itself.
[r3] – Handles to contained data are created only (and only when) with the storing of data.
[r4] – Handles must be encapsulated by smart pointers. They must know how to resolve their destruction, and of the data they point to.
[r5] – Iterators that "directly" point to stored data must be available. They are unsafe and should be used with caution.
[r6] – Data can be destroyed even when their handles are still alive. This will make the handles point to a specific "No Data" element.
[r7] – Containers must assume "non pointers" template data types.
[r8] – Containers must be inheritable from an abstract container.
I know some rules can be rather controversial, but I'll leave it as is for now. Actually, they'll force some restrictions on the stored data as well. The biggest problem probably comes from rule 8; there are reasons that I want a uniform representation of any possible container for the same data type, such as a list of containers to be used by factories.
A good name for the container might be StoringFactory. After researching for a while, it seems to be rather unusual to mix up a container together with a factory, as the separation in "container only" and "factory only" classes is desirable. However, for my needs, it seems to make more sense to simply merge them together, with a name to clearly show this mix of responsibilities. Am I deciding wrong? Don't know, but I'm really afraid of over-engineering this, right now...
The abstract StoringFactory class above is rather simple. Someone that is somehow reading this post will probably notice that there are no methods to actually insert or create data into this container. The reason for that is that I want to try using emplacement methods with variadic templates. There's a catch, though: they can't be virtual, forcing them to be in the child classes. How, then, can I insert data into the container when I have only a pointer to the base class? It turns out that it is indeed possible, by means of abstract factories or dependency injections, and some magic.
Next comes the dubious methods of DeleteData and DeleteHandle. DeleteData is rather dangerous and nonsensical, but I want the ability to remove data from memory without first needing to properly clean up anything related to its handle (such as every object that has a shared ownership of the handle). This can be useful in some very rare cases, as unloading low priority resources when in need of memory. The DeleteHandle probably shouldn't be public. Handles will call this method to destroy themselves, but I don't see any reason to not make this method protected, and the handles a friend class of the StoringContainer.
And what does the handle looks like?
They're just a small container with enough information to refer back to the original data. I've restricted the handles to store handle_type (that is, unsigned int) as their "handling key". I could have used pointers, but ended up deciding on "indexes" or "keys" instead. This is a decision that'll probably be very hard to change later on... so let me pretend to be proud and sure of what I'm doing.
Finally, all that remain are the iterators types for the StoringFactory class. It is possible to make an abstract iterator type and then inherit as needed, for each storage desired, such as lists, maps, etc. This should work, by relying on Covariance, but soon many flaws of this idea would sprout everywhere. One of the problems, for example, is the need of casting between parent and child types of the iterator. Even if that is solved, templates and slicing would come right after with many other problems. The path I chose was to use the pimpl idiom, though I feel like I'm missing a bit of its point.
The idea was to use a single iterator "interface" class to redirect operations to a real implementation, that is storage specific. Some problems with slicing still remains, such as making clones of the iterator implementation when the "interface" is copied around. Since the implementation is handled only by the "interface" class, methods such as clone are used. Casting is still a problem (i.e when the user try to copy or compare the iterator of one type of storage with one of another type of storage). I didn't decide on any special semantics for that, but later there'll be probably some assertions for debug builds.
Some methods are still missing, such as container sizes, but this is the basic for my storing with handles system for game components. I'm also implementing an ArrayStoringFactory class, that inherits from StoringFactory, but I don't think it's code has anything interesting right now.
Let's see what happens in the next 15 days. After finishing up theses classes and adding any new needed method to the base classes, I'll make some unit tests and then the Transform component. Maybe I'll have some thoughts to write in this blog again.
All code in this post has been pretty-printed by hilite.me
terça-feira, 22 de março de 2016
domingo, 6 de março de 2016
C++ was my favorite language back then, at college, and even before. Fun were the days where Segmentation Fault wouldn't immediately translate to time or money wasted around, nor unhappy clients contacting the team. Right after graduating I've started using C# with Unity 3d and got into the Garbage Collected world – and I've stayed with C# from the very then.
I've decided to start a simple C++ game on my free time. It can't be that hard, right? And the game will run a lot faster, right? Actually, both are wrong. C++ can be very hard if you aren't used to think in terms memory management, resources ownership and mapping, and even when and how their memory will be released. Even with smart pointers and RAII, everything can eventually go south. In fact, even in Garbage Collected environments with RAII and everything else, things WILL eventually go south... And about being faster, even if the compiled code do have raw speed in terms of execution time, a lot more often than not, slowness is caused by design, code architecture, algorithmic decisions and so on. The end result is that in hands of good programmers, it's possible that games in C# or Java run faster simply because they're easier to profile, debug and refactor – and refactoring is as time demanding as it is need for any project survival as it grows.
Enough about that! I've decided to use c++ and so I've started searching around about what good practices there are when using the "new" standard smart pointers. "New" because they've already existed in Boost since like ever. Then I found this amazing stack overflow answer, by David (quoting here):
Personally, this is how I (more or less) do it:
These guidelines are indeed very accurate and simple, as, in my opinion, smart pointers greatest virtue is showing intention over “simply” managing memory. There are, of course, exceptions where those guidelines can't be properly followed, but exceptions exists for everything in the coding world. Looking at smart pointers types, we can see that there's an immediate synergy between unique_ptr and raw pointers, and shared_ptr and weak_ptr. It's simple, right? Can't be that hard. Again, this turns out to be wrong. Not because of the guidelines nor the pointers, but because of the problem that came way before them: resource ownership.
I've structure a simple Entity-Components-System for a game. I want Entities to have a list of components they hold, but I want actual components data to be stored somewhere else, in a ComponentsContainer class. The reason is that I want my systems to be able to iterate over all components of a single type easily.
I bet that didn't make any sense, right? Well, I've imposed a few restrictions on each part of this structure:
- [c1] Components can have methods, but "high level" methods such as "Update" or "Render" are desired to be in a System, which will iterate over all components executing their logic.
- [c2] Components must be able to store references to other components, of the same or any other entity. They must have means of easily checking if that component still exists.
- [c3] Components can be destroyed anytime, by any component or System.
- [c4] Components should be automatically deleted if no one references them, if possible.
- [c5] Components are owned by a single entity.
- [e1] They have a unique identifier
- [e2] Must not have logic other than helping methods
- [e3] They uniquely own their components
- [s1] Systems have the responsibility of iterating over components of a single type (the one that the system works with), executing logic, rendering, or whatever else it is supposed to do (i.e spatial partitioning for another system, such as collision testing)
There are problems with [s1], [e3], [c2] and [c5].
Because of [c2], it is desired that the entities own each component by means of a shared_ptr, even if they are the unique owners. With this, references to any component can be weak_ptr, and we can check whether they (the referenced components) have been deleted or not. Alternatively, using a unique_ptr would mean our references are now raw pointers. By the guidelines above, a raw pointer indicates that the resource pointed to will outlive the component, which is not guaranteed because of [c3]. With shared_ptr, actually, the solution for this is kind of straight away: entities must not ever allow another shared_ptr share the component, pushing us back to the original "unique" ownership.
Now, [s1] says that we need means to iterate over all components of a single type. It is possible to achieve this by simply iterating over all entities, searching for components of a specific type. There are some ways of achieving this, such as storing a matrix of entities X components, etc, but I personally don't like the concept of checking if an entity has a component in order for us to update it, in a System. If I want to iterate over all components of a type, it does not mean I am also interested in checking if they exists. Checking for this in Systems might be made later, as a need for deep refactoring.
Trying to not over-engineer, the components storing can be moved out of the entities, into a data container. A data container will also make some stuff a little bit simpler, such as factories and dependency injection, if I decide on them later. I want these characteristics:
- [d1] They must work with ranged for loops. Come on, that's cool! Every iteration must be over a valid component (in the context of allocated and alive)
- [d2] They do not need to worry about specific alternative accessing methods. For example, methods to allow accessing specific elements of an octree is too specific. The system will have to know the container type anyway, to do this effectively.
- [d3] They must not force any specific type of structuring. This means that they might be used as vectors, lists, maps, trees, octrees, etc
- [d4] They must be able to store data contiguously
- [d5] Reordering elements should not break any references
- [d6] Data not used by anyone should be deleted, to avoid leaks.
The rule [d4] is a critical one. It is listed here to avoid the need of some refactoring later. [d4] together with [d1] permits intuitive (for the System) sequential access over contiguous memory data. Even if I don't want to implement this right now (and I dont), this will inevitably become an important optimization later, as it allows for cache-friendly access.
With the data container, the components won't be stored in entities anymore, although entities still are their unique owners. Simple? No. There is a problem here. If the data is contiguous, then it's very likely that the components are stored by value in an array. We just can't delete it. However, were it in another data structure, such as tree of elements in the heap, then deleting would be a must. Fortunately, smart_ptr allows us to pass a function – better yet, a lambda - to use as deleting logic. We can pass this complexity back to the container, making it create the new component for us and returning a shared_ptr with the deleter set.
However, it turns out that smart pointers aren't enough anymore, because of [d5]. If the container reorders the elements, how am I supposed to update the references in the entities? They need to point to something else, instead of the data itself: maybe a handle – and then now we have a smart pointer to a handle. It is kind of like de-referencing twice, to access the data we want.
Since we have arrived at handles, now it is hard to let go of them. They can still exists even if the resourced they "handle" is not around anymore. If we can update a handle when the resource is deleted, we can make it either return nothing, or return something to expose the error. For example, a handle to a sound effect could return a default error-like sound when accessed after the original sound has been unloaded. This means allowing the data container being able to unload the components even if they are still being used by other entities. The use for this sounds dubious for components, but it is interesting if we can make the container generic.
So, how to structure everything with handles? I have no idea. I'll be thinking about it and then make a new post in the future.