A few tips (not a full review)
Constructing components
For the client to add a component to an entity, they do this:
struct Position
{
double x;
double y;
};
a.createComponent<Position>(Position{ 0.2, 0.3 });
I think this syntax can be cleaned up a little. In the createComponent function, you're constructing components like this:
componentArray<T>.emplace_back(args...);
If you change that line to this:
componentArray<T>.push_back(T{args...});
Then creating a component becomes:
a.createComponent<Position>(0.2, 0.3);
I find this syntax a bit cleaner.
Static data structures
You're storing all your entities and components statically on the Entity class. This is OK because it is unlikely that anyone would need to have two separate systems. But static is global and global is bad. You should store all of the components and entities in a Registry object. Don't make this a singleton. Never uses singletons. This makes it obvious what depends on the game objects and allows the client to have multiple systems.
If you do this, then you can't have an Entity class because such a class would need to store a pointer to the Registry. With a thousand game objects, you have a thousand pointers which are just unnecessary. You should call functions on the Registry with the entity IDs to manipulate the game.
Systems
Your SystemManager is almost useless. I like the idea of event systems (I've never used them in any of my games though). Systems are functions and functions have parameters. Systems are heavily dependent on order. Take this example.
struct DataStructure {};
struct Registry {
// all of the entities and components
};
void writeData(Registry ®, DataStructure &data) {
// iterate some components and write to the data structure
}
void readAndWriteData(Registry ®, DataStructure &data, int someOtherThing) {
// iterate some components, reading and writing to the data structure.
}
void readData(Registry ®, const DataStructure &data) {
// modify some of the components based on the data structure
}
struct Game {
DataStructure data;
Registry reg;
void runSystems(int thing) {
writeData(reg, data);
readAndWriteData(reg, data, thing);
readData(reg, data, thing);
// handle timing
}
};
All of the systems take a different set of parameters and they must all be executed in the right order.