Hello again! In this video, we are going to look at the entity manager and object creation.
In fact, there is quite a lot to unpack with the entity manager, so it is going to be split over two
videos.
This one will deal with creating objects, and the second one will deal with operating on objects.
So, just to remind ourselves, the entity manager has a member function called create(), which will create
a new entity object in the game.
It will add it to all_entities, which is a vector of unique_ptrs to each entity in the game.
And it will also add an alias to the object to this grouped_entity,
which groups
all the entities by their type.
The arguments to this create() call will be used for constructing the object, so they are going to be passed
to the entity constructor.
And it may be that different entities will require different numbers of arguments, or different types
of arguments.
So we may have to write overloads. And also, some of the arguments may be expensive to copy, or they could
be move-only objects.
So we may have to write overloads for rvalue references as well.
So instead of writing all that code ourselves, why not get the compiler to do it for us?
And if you remember the section on compile time programming, there are things called variadic templates
in C++ 11: templates which can take variable numbers and types of arguments.
So the compiler will instantiate these with the correct arguments, depending on how it is called.
And it is also possible to use forwarding references with variadic templates.
So, if necessary, the compiler will instantiate with rvalue references.
So this means we can use perfect forwarding, which is the most efficient way of moving data around
between function calls.
We end up with this rather frightening-looking template function definition!
So let's look instead at how we would call it.
So we have some object of the entity manager.
We call its create() member function.
We give the type of the entity as the type parameter.
So here, we want to create a ball.
So this type parameter T will be replaced by ball when the template is instantiated.
And then the arguments will be the arguments to the ball constructor.
So this will create a ball at position "x" and "y" on the screen.
The compiler will deduce the arguments type from this: x and y are floats.
So this is going through a forwarding reference.
So reference collapsing will apply, and these will emerge as l value references to float.
So this will be instantiated as a function which takes two lvalue references to float and returns
lvalue reference to ball.
The grouped_entities map has all the entities, grouped by their type.
The key of the map depends on the type of the entity. In the runtime type information, we saw that
we can get a hash code for each type, which is guaranteed to be unique.
So this means that each child class of entity will have a different hash code, and we use that as the key
for the map.
And then the value of the map will be a vector of pointers to entity.
So these will be alias pointers to every entity which has the same type as the key.
These are not smart pointers.
They are just traditional C++ pointers, memory addresses. So they do not have any ownership and they are
not connected to the lifetime of the objects.
So if we delete the entities, these pointers will still exist, but they will be invalid.
So we must make sure we do not use those by accident.
So the pseudo-code for the create() member function.
We start off by creating the entity object. we call make_unique(), which will create the object on the
heap, and return a unique_ptr to it.
To get the arguments into the constructor, we use the std::forward() function.
So if we have any arguments which are rvalue references, they will remain rvalues, and not get
converted into lvalues.
So these arguments will be forwarded from the call to create(), into make_unique() and then into the constructor
for the object.
So that minimizes the amount of copying.
Then make_unique() will return a unique_ptr to the object.
We insert that into the all_entities vector.
We get our alias pointer to the entity object, its memory address.
Then we find the hash code of the entity's type.
We get the element in grouped_entities which corresponds to the element's type.
And then we insert our alias pointer into the vector for that element.
And finally, we return the new entity object.
So here is our template function definition, with the variable number of arguments and types, and the forwarding
reference.
So we call this by giving the type of the entity as the parameter and the arguments to the constructor
as the arguments.
So this will create a background object and the constructor arguments will be 0. And similar for
ball and paddle.
So when these functions are instantiated, T will be replaced by background, ball and paddle.
The Args parameter will be replaced by lvalue reference to float and lvalue reference to float.
The actual arguments will be these ones.
We can provide an extra bit of safety at no cost, or no run-time cost any way. We can do a static_assert.
And this will check whether the object we are being asked to create is actually derived from entity.
If it is, then this will be replaced by "true", and the compiler will ignore the assert and continue compiling
the code.
If not, this will be replaced by false and the compiler will emit an error message which includes this
string.
And then we have embedded double quotes in the string so we can use a C++11 raw string, to make that
slightly easier to write.
We then create the actual object. We call make_unique() with the type as the parameter, so make_unique()
with ball as the parameter,
for example. We pass the arguments to create() as the arguments to make_unique().
We put them inside a call to std::forward() to make sure we do not lose any rvalues. forward() requires
a type parameter, so we can put the parameter pack in there.
And then, these three dots will cause the compiler to expand the packs.
So this is going to be replaced by forwarding lvalue reference to float, which takes, say, 0 as argument.
And then another lvalue reference to float, which is forwarded, and takes zero as argument.
And this will return a unique_ptr to the object.
And we can use this as "auto", because the actual type will depend on how this is instantiated.
Then we get our alias pointer.
I do not think I mentioned this before, but unique_ptr
has a member function, get(), which will return a pointer to the object, a C++ pointer with the address.
And generally, this is dangerous, and not a good thing to use.
However, it is safe here, because we are not going to use this to subvert the management of the lifetime,
by unique_ptr.
We are just using this as a kind of reference.
We get the hash code for the entity, so the hash code for ball, for example.
Then we find the element of grouped entities which corresponds to that type.
So this is going to be the element for ball, and then the value will be a vector of pointer aliases
to ball objects.
So we just insert our alias pointer into this vector. And then we insert the unique_ptr into
all_entities. And we have to call std::move() here, because unique_ptrs cannot be copied.
They can only be moved.
And finally, we return the new object, by dereferencing the alias pointer.
OK.
That is it for this video.
I will see you next time.
But until then, keep coding!