Closed genaray closed 3 years ago
I have implemented "double buffering" in my game for multithreading reasons with some modifications to artemis-odb to speed it up and the performance cost is not too heavy as long as i use many small components instead of large ones.
I have one world that is always the live one. During system processing i mark component adds/changes/removals in BitVectors. To get good performance i do multiple processings one after another if game speed is high. At the end i go through the adds/changes/removals BitVectors and delete entities, copy changed components, add entities to the world copy.
To avoid locks i have 2st world copies. One that is being updated and another that is read only for this time step. It complicates the copy update a bit but not too much. I gained more performance by not needing locks for each world than i lose by applying changes twice.
During my parallel world updates for each StarSystem i can safely read the world copies of other StarSystems. Same for UI.
See classes Galaxy StarSystem and ShadowStarSystem in https://github.com/exuvo/Aurora-J .
Artemis-odb modifications include a CustomComponentMapper that automatically marks component adds/removals in my BitVectors. The other change is to the EntityMangager to allow creating an entity with a specific ID for the world copies. This is a mostly unsafe operation but is required to maintain same entityIDs in the main world as well as the copies.
The tedious parts are that any time i change a component i have to mark it as changed, and each component i want to be propagated to the world copies needs to have a copy method. Most of my component copy methods store an hash of something so it can quickly decide if this a new component that needs a full copy or only a partial faster copy.
I have implemented "double buffering" in my game for multithreading reasons with some modifications to artemis-odb to speed it up and the performance cost is not too heavy as long as i use many small components instead of large ones.
I have one world that is always the live one. During system processing i mark component adds/changes/removals in BitVectors. To get good performance i do multiple processings one after another if game speed is high. At the end i go through the adds/changes/removals BitVectors and delete entities, copy changed components, add entities to the world copy.
To avoid locks i have 2st world copies. One that is being updated and another that is read only for this time step. It complicates the copy update a bit but not too much. I gained more performance by not needing locks for each world than i lose by applying changes twice.
During my parallel world updates for each StarSystem i can safely read the world copies of other StarSystems. Same for UI.
See classes Galaxy StarSystem and ShadowStarSystem in https://github.com/exuvo/Aurora-J .
Artemis-odb modifications include a CustomComponentMapper that automatically marks component adds/removals in my BitVectors. The other change is to the EntityMangager to allow creating an entity with a specific ID for the world copies. This is a mostly unsafe operation but is required to maintain same entityIDs in the main world as well as the copies.
The tedious parts are that any time i change a component i have to mark it as changed, and each component i want to be propagated to the world copies needs to have a copy method. Most of my component copy methods store an hash of something so it can quickly decide if this a new component that needs a full copy or only a partial faster copy.
Thanks a lot for your answer ! Im gonna look at it once im home :) Great that some people are still active in here... i just hope that artemis ODB adds double buffering on their own in the future ^^
Implementing double buffering would be a major effort and such a fundamental change that it is unlikely to be implemented at this time.
Multithreading is something we'd like to solve, but @junkdog has said in the past that artemis-odb (for multiple reasons) will most likely never be multithreaded.
From the changes i did for double buffering it is not a major code effort to implement but it is very bug prone if you somewhere forget to mark a component as changed. I could not alleviate it by always using setters as they never had references to the world being used so it was always up to me to remember to mark it as changed whenever i did anything with a component. I would put it on par with remembering to free in C.
At best you could have it in its own branch with hefty warnings.
So lets talk about double buffering... its an other little tool used by some ECS to make multithreading a lot easier.
Its "pretty" simple... you have two game states
So right now say, your velocity integration system might look like...
position[ID] += velocity[ID] * deltaTime
Using double buffering it may look like this instead.
newGameState.position[ID] = oldGameState.position[ID] + oldGameState.velocity[ID] * deltaTime
newGameState.position could have been completely empty before this moment. The act of updating the state populates the state.
You just need to make sure every piece of meaningful state data gets a chance to be pushed through like that. Or is in a separate read-only collection that can be shared, if it's not something that can change frame to frame.
This can also be good for other multithreading in our games. Now even jobs that read data A can run at the same time as jobs that write data A, because the readable and writable versions are separate. So, you have much less work relating to scheduling dependencies, locks, etc.
The cost is that it can take longer for a change to propagate through multiple passes of updates. But you still have the power to read from the writable data if you know the last set of writes have finished and need to propagate the latest changes faster :)