Trouble switching from inheritance to composition.
Games/Projects Currently In Development:
Discord RPG Bot | D++ - The Lightweight C++ Discord API Library | TriviaBot Discord Trivia Bot
EDIT: Sean, I think I FINALLY got the idea. You mean that I should have only one class for entity, and that is the GameObject, and I should have two functions there, addComponent and removeComponent (for starters).
And if I want a Monster, instead of creating a monster class, I just create a new GameObject object, and add the components for monster. And the only thing that is left is for me to figure out how to do that in practice. But is this the main idea, having only _one_ class for entity, and _multiple_ classes for components, and then whenever I want a Player, Monster or Bunker, I just create a new GameObject object and fill in the proper components? If you don't have time, just answer with yes or no.
You certainly _can_ use sub-classing to configure a GameObject, though I do think a function is a better approach.
EDIT 2: In order to add a component, I need to have a pointer to that component in the GameObject. So you say that if I have 58 possible components, I need to create 58 empty pointers in my GameObject class, just in case it needs it?
For a hobby game where you might only have 10 types of component, hard-coding the pointers into your GameObject is probably just fine. It doesn't cost you anything, it's easy to write, and _it just works_. Start there. Don't over-complicate things for no reason.
Now, if you have 58 components, then you perhaps need to start thinking about alternatives. A very simple approach here is to store your components on your GameObject with a map of some kind (say, a std::unordered_map). You can use the std::type_index (*not* type_info) of the component type. Something like this:
class GameObject {
unordered_map<type_index, unique_ptr<ComponentBase>> _components;
public:
template <typename T>
void addComponent(unique_ptr<T> component) {
_components.insert(typeid(*component), std::move(component));
}
template <typename T>
T* findComponent() {
auto const it = _components.find(typeid(T));
return it != _components.end() ? static_cast<T*>(it->second.get()) : nullptr;
}
};
// ...game code...
Transform* transform = object->findComponent<Transform>();
// ...object initialization...
object->addComponent(make_unique<Model>(transform));
You can do the same without templates, too, though the templates make the usability much nicer.
There are even more sophisticated or more efficient ways to implement this, but again - do those matter for your engine? Probably not. Don't over-complicate things needlessly.
The one thing I will say, though - avoid querying components while the game is running. If you need an object's transform, store the pointer; don't call findComponent over and over again needlessly.
EDIT 3: The other way people do it is to not create an object for the entity, just store an ID, based on bitmasking component IDs, but I'm going into Entity/Systems there... I really like the empty pointers design, the only thing is that it wastes memory. But one pointer is 4-6 bytes, my ram is 8 gigs, it's not a big deal, right?
Yeah, that's the traditional ECS approach. I don't recommend it. It has some nice advantages but it getting it right is very difficult and getting it wrong is very easy. Simpler components architectures are much easier to write and use.
And no, RAM is not a big deal with GameObjects, unless you have a truly stupendous number of them. The cost of accessing or iterating over them is your enemy, not the memory usage. (Even on consoles; the modern devices give you enough wiggle room to not worry _too_ much about this kind of stuff.)
EDIT 4: How do I add a component that needs stuff from the previous component as an argument? My Armature component needs my Model component. Is this a sign of a bad design that I need to do this? (yes/no)
You can pass components in as parameters to other components, or have the component initialization logic query for other components, possibly using two passes to avoid order-of-construction issues:
simpleCreateMissile() {
// components are handed points to their dependencies, so they must be
// created in the proper order so they can initialize properly
object.transform = createTransform();
object.model = createModel(transform);
object.armature = createArmature(model);
}
complexCreateMissile() {
// components are created/loaded, but do not have their dependencies;
// this means the order is unimportant, but components cannot be fully
// initialized at this time - note that we might use a data file here to load
// the component types instead of hard-coding them so that our objects
// can be customized without recompiling the game
object.add(createArmature());
object.add(createTransform());
object.add(createModel());
// all the components are present so they can now search for any
// dependencies and finish their initialization work
for (component : object.components)
component.initialize(this); // components use object->findComponent to find their dependencies
}
I again would keep it simple at first.
EDIT 5: And if stuffing all references into GameObject is what you meant, then what's the difference between that and the update pattern I posted earlier?
Storing components in a GameObject statically is different than querying those components in a loop multiple times every frame. :)
Say you spawn 1000 objects, and only 200 of them are visible/renderable (the others are invisible spawn points, or invisible trigger zones, or invisible sound sources, etc.). Do you think it's faster to iterate 1000 objects or 200? Do you think it's faster to iterate with an "if (has renderable)" in the loop or without that condition? Now, only 100 of those objects are dynamic physics bodies, so do you think looping over 1000 or 100 is better? Only 150 of them has AI, only 2 of them have player controllers, etc.
Here's the number one rule of optimization: not doing something will always run faster than doing it. :) The implication is that looping over objects that you don't need to loop over is inefficient.
Here's another important rule of optimization: don't put branches inside of loops if you can help it. That makes the CPU's branch predictor mispredict and results in frequent CPU instruction pipeline stalls.
Don't get me wrong: your original proposal _works_. Real shipping successful games have used very similar setups. I just don't recommend it; it seems easier/lazier but it can create more work in the long run. It works well only if your game has few types of components and relatively few objects.
Sean Middleditch – Game Systems Engineer – Join my team!
Do you think it's faster to iterate 1000 objects or 200? Do you think it's faster to iterate with an "if (has renderable)" in the loop or without that condition? Now, only 100 of those objects are dynamic physics bodies, so do you think looping over 1000 or 100 is better? Only 150 of them has AI, only 2 of them have player controllers, etc.
At these numbers you are no longer looking at times for the operations, you are looking at cache effects.
As an example, for a cache-friendly system it can be faster to simply walk across an 8,000 element sorted buffer and find the first match rather than to do a binary search where you would only hit 13 items.
Looping over 1000 objects in a cache-friendly way and doing cache-friendly operations will be far faster than hopping around to 100 items.
Also, a common mistake beginners make is to operate on single items. That isn't what happens in good code. Sure you could draw ONE thing, then draw ONE thing, then draw ONE thing, repeating doing ONE thing thousands of times.... Or you could bundle them all up together, make a single bundle with a huge batch of things, then call it ONE time. The same is true with so many other systems. As a quick study on rendering alone, here's a great writeup on GTA5 rendering. Note that they don't call a Render() or Draw() function on every item in the city.
The idea of putting a function on every single object that gets called every frame is tempting because it is easy, but it is terrible from a performance perspective. If you can process two or four or eight of them at once by doing them in parallel with SIMD instructions, you have just multiplied your performance by 2x or 4x or 8x. If you can bundle up a bunch of them into a single combined process, you've eliminated all the extra overhead of all those function calls.
There are many variations on this theme. Virtual functions that are rarely specialized are an enormous waste; you invested the cost of a major jump lookup and cache miss so that you could do zero work, sometimes doing this thousands of times per frame. Listeners that do nothing. Calling base classes that are empty. Repeating work that has already been done with idempotent calls. Sorting something that was already sorted, etc., etc.
While it is good to have a single instance where many things point to the real item, it can also be a good design to make specialized copies of data when that makes sense, too. There are innumerable tools available if you look for them. Inheritance and composition are two items among an enormous number of design options.
for a cache-friendly system it can be faster to simply walk across an 8,000 element sorted buffer and find the first match rather than to do a binary search where you would only hit 13 items.
The benchmarking we've been doing for the flat_map proposal for the C++ committee does not reflect that folk wisdom, unfortunately. We've found it difficult to make linear search out-perform binary search past a dozen or so elements on real hardware (albeit in micro-benchmark-y cases, so take it with a grain of salt).
Note this also depends a bit on the key and whether the whole array fits in cache, how often you're searching this array, etc. Even a fragmented linked listed can be consistenly entirely in L1 cache, depending on usage patterns. :)
Looping over 1000 objects in a cache-friendly way and doing cache-friendly operations will be far faster than hopping around to 100 items.
Looping over 100 items in a cache-friendly way is faster than looping over 1000 in a cache-friendly way. ;)
Do less work, run faster.
To mesh that with frob's excellent advice: getting data out of main memory is more work for the hardware and getting data out of the CPU's caches is less work for the hardware. :)
Sean Middleditch – Game Systems Engineer – Join my team!
Imagine my character has the magic ability to become invisible, where he can go through walls and doesn't take hp.
So when the character is invisible, I would just like to remove the CollisionBox and Health out of the equation. With inheritance it's messy and ugly. But with composition, I can just remove the Health and the CollisionBox components when I'm invisible, and add them back when I'm visible, for example.
Removing components is clumsy. Being incorporeal is part of the state of the character, and the proper representation is as one or more fields in the Character class.
With a composition-based hierarchy of components, being incorporeal might or might not be split into, or copied into, component-level state (the character's CollisionBox is disabled, the Character's Health doesn't take damage from certain sources, the Character's Model switches to transparent textures).
The main challenge is not handling special cases (there's going to be a small and finite number of conditions like being incorporeal) but handling the state duration coherently in all components, presumably with the main entity (here a Character) handling timers etc.
Omae Wa Mou Shindeiru
Also, a common mistake beginners make is to operate on single items. That isn't what happens in good code. Sure you could draw ONE thing, then draw ONE thing, then draw ONE thing, repeating doing ONE thing thousands of times.... Or you could bundle them all up together, make a single bundle with a huge batch of things, then call it ONE time.
Frob, of course you are right. Draw calls are expensive. But I'm far behind this. My only concern for now is to make my code easy to read so I can upload less code to my brain before making a change. Everything else is not a priority. Maybe I will start to get performance issues after a few years, and I will have your advice in mind. And even then I would use a profiler first before touching anything because everybody says different things. :)