Skip to content

Instantly share code, notes, and snippets.

@mdragon159
Last active January 3, 2022 01:20
Show Gist options
  • Save mdragon159/b82c3d46ea9f3c310cb74feb1987b025 to your computer and use it in GitHub Desktop.
Save mdragon159/b82c3d46ea9f3c310cb74feb1987b025 to your computer and use it in GitHub Desktop.
Sample EnTT Snapshot Creation & Restoration
// Macro workaround to having to define code for every single component
// Just need to remember to define all Component classes here as well as in Components.h
// References:
// https://stackoverflow.com/questions/25428144/iterate-a-preprocessor-macro-on-a-list-of-class-names
// https://stackoverflow.com/questions/6635851/real-world-use-of-x-macros
// https://en.wikibooks.org/wiki/C_Programming/Preprocessor_directives_and_macros#X-Macros
// Also yes, file name + type is arbitrary
// Note that yes, macros should be a last resort. If there's a better way to arbitrarily define list of classes in one place
// and freely use elsewhere to generate arbitrary repetitive code, then would be nice to replace this approach here
// Sample usage:
// #define COMPONENT_CLASS(classname) mSimContext.logWarnMessage("Example", "Class name: ##classname");
// #include "GameCore/Components/Components.def"
//////////////////////////////////////////////////////////////////////////////////////////////////
#ifndef COMPONENT_CLASS
#error COMPONENT_CLASS should be defined before including Components.def
#endif
// Define list of classes. Using "COMPONENT_CLASS" as arbitrary macro that is expected to not be used outside of this context
COMPONENT_CLASS(TransformComponent)
COMPONENT_CLASS(PhysicsComponent)
COMPONENT_CLASS(DynamicColliderComponent)
COMPONENT_CLASS(StaticColliderComponent)
COMPONENT_CLASS(SpawnVolumeComponent)
COMPONENT_CLASS(LocalPlayerInputsComponent)
COMPONENT_CLASS(CameraTransformComponent)
COMPONENT_CLASS(HumanoidStateComponent)
COMPONENT_CLASS(MovementInputsComponent)
COMPONENT_CLASS(MovementStateComponent)
COMPONENT_CLASS(AttackChargeActionComponent)
COMPONENT_CLASS(AttackActionComponent)
COMPONENT_CLASS(QuickStepActionComponent)
COMPONENT_CLASS(GrappleAimComponent)
COMPONENT_CLASS(GrappleActionComponent)
COMPONENT_CLASS(GrappleQuickPullComponent)
COMPONENT_CLASS(HitfreezeComponent)
COMPONENT_CLASS(HitstunComponent)
COMPONENT_CLASS(DeathComponent)
COMPONENT_CLASS(EnemyComponent)
COMPONENT_CLASS(HealthComponent)
COMPONENT_CLASS(ActiveGrappleProjectileComponent)
COMPONENT_CLASS(UnrealActorFeedbackComponent)
#undef COMPONENT_CLASS
class GameplayManager {
entt::registry registry;
bool isSnapshotStored = false;
// ECS Snapshot: Create vector of entities and components per component class, as EnTT stores data on a per-component-class basis
#define COMPONENT_CLASS(classname) std::vector<entt::entity> entitySnapshot_##classname; \
std::vector<classname> componentSnapshot_##classname;
#include "GameCore/Components/Components.def"
// Additional snapshot data for proper entity restoration
entt::entity snapshotReleased;
std::size_t snapshotRegistrySize;
std::vector<entt::entity> snapshotRegistryEntities;
public:
//........
void testSnapshotCreationAndRestoration() {
// Begin measuring time for performance stat
uint64_t actionStartTimeInUs = SharedUtilities::getTimeInMicroseconds();
std::string actionName;
// If snapshot not created yet, create one from current frame's data
if (!isSnapshotStored) {
actionName = "SnapshotCreation6";
isSnapshotStored = true;
createSnapshot();
}
// Otherwise, restore from the existing snapshot
else {
actionName = "SnapshotRestoration6";
isSnapshotStored = false; // For easy toggle testing, treat current state as if no snapshot stored
restoreSnapshot();
}
// Output runtime performance stat
uint64_t actionRunTimeInUs = SharedUtilities::getTimeInMicroseconds() - actionStartTimeInUs;
// .....Placeholder to log or otherwise use the above result
}
private:
void createSnapshot() {
snapshotRegistrySize = registry.size();
snapshotReleased = registry.released();
snapshotRegistryEntities.insert(snapshotRegistryEntities.end(), registry.data(), registry.data() + snapshotRegistrySize);
// Store the actual snapshot data per each component class
// This copy-per-class approach is necessary due to how EnTT is built
#define COMPONENT_CLASS(classname) createSnapshotForComponent<classname>(entitySnapshot_##classname, componentSnapshot_##classname);
#include "GameCore/Components/Components.def"
}
template<typename ComponentType>
void createSnapshotForComponent(std::vector<entt::entity>& entitySnapshot, std::vector<ComponentType>& componentSnapshot) {
// Get reference to unique storage for this component type
auto&& storage = registry.storage<ComponentType>();
// Create entity snapshot
// For why not using memcpy (speed) and examples of different methods for copying data into a vector: https://stackoverflow.com/a/261607/3735890
entitySnapshot.insert(entitySnapshot.end(), storage.data(), storage.data() + storage.size());
// Create component snapshot
// Note that data may cross multiple pages in memory. Thus, need to copy the data into the output vector one page at a time
const std::size_t pageSize = entt::component_traits<ComponentType>::page_size;
const std::size_t totalPages = (storage.size() + pageSize - 1u) / pageSize;
for(std::size_t pageIndex{}; pageIndex < totalPages; pageIndex++) {
// Calculate number of elements to copy so only copyyig over the necessary number of elements
// Truly necessary as using ComponentType vector here instead of, say, vector of arbitrary bytes
const std::size_t offset = pageIndex * pageSize;
const std::size_t numberOfElementsToCopy = std::min(pageSize, storage.size() - offset);
// Do the actual copying
ComponentType* pageStartPtr = storage.raw()[pageIndex];
componentSnapshot.insert(componentSnapshot.end(), pageStartPtr, pageStartPtr + numberOfElementsToCopy);
}
}
void restoreSnapshot() {
registry = {};
registry.assign(snapshotRegistryEntities.data(), snapshotRegistryEntities.data() + snapshotRegistrySize, snapshotReleased);
// Restore the actual snapshot data per each component class
// This copy-per-class approach is necessary due to how EnTT is built
#define COMPONENT_CLASS(classname) restoreSnapshotForComponent<classname>(entitySnapshot_##classname, componentSnapshot_##classname);
#include "GameCore/Components/Components.def"
cleanUpSnapshotData();
}
template<typename ComponentType>
void restoreSnapshotForComponent(std::vector<entt::entity>& entitySnapshot, std::vector<ComponentType>& componentSnapshot) {
// Get reference to unique storage for this component type
auto&& storage = registry.storage<ComponentType>();
// Restore entities
storage.insert(entitySnapshot.begin(), entitySnapshot.end());
// Restore components
// Note that data may cross multiple pages in memory. Thus, need to copy the data into the storage one page at a time
const std::size_t pageSize = entt::component_traits<ComponentType>::page_size;
const std::size_t totalPages = (storage.size() + pageSize - 1u) / pageSize;
for(std::size_t pageIndex{}; pageIndex < totalPages; pageIndex++) {
const std::size_t offset = pageIndex * pageSize;
const std::size_t numberOfElementsToCopy = std::min(pageSize, componentSnapshot.size() - offset);
ComponentType* pageStartPtr = storage.raw()[pageIndex];
memcpy(pageStartPtr, componentSnapshot.data() + offset, sizeof(ComponentType) * numberOfElementsToCopy);
}
}
void cleanUpSnapshotData() {
snapshotRegistryEntities.clear();
// Clear all existing snapshot data
#define COMPONENT_CLASS(classname) cleanUpSnapshotData<classname>(entitySnapshot_##classname, componentSnapshot_##classname);
#include "GameCore/Components/Components.def"
}
template<typename ComponentType>
void cleanUpSnapshotData(std::vector<entt::entity>& entitySnapshot, std::vector<ComponentType>& componentSnapshot) {
entitySnapshot.clear();
componentSnapshot.clear();
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment