Production-Ready Actor Component | Auto-Save/Load | State Management | Data Validation
The UEntityPersistence component provides robust, production-ready entity persistence capabilities for Unreal Engine 5 actors. This component automatically handles saving and loading actor data to/from Supabase with comprehensive error handling, validation, and state management.
// Add component to your actor
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Persistence")
UEntityPersistence* EntityPersistence;
// In constructor
EntityPersistence = CreateDefaultSubobject<UEntityPersistence>(TEXT("EntityPersistence"));
// Configure the component
FEntityPersistenceConfig Config;
Config.bAutoSave = true;
Config.AutoSaveInterval = 120.0f; // 2 minutes
Config.bAutoLoad = true;
Config.bValidateData = true;
EntityPersistence->SetConfiguration(Config);
| Property | Type | Default | Description |
|---|---|---|---|
bAutoSave |
bool | false | Enable automatic saving |
AutoSaveInterval |
float | 300.0f | Auto-save interval in seconds |
bAutoLoad |
bool | false | Enable automatic loading on BeginPlay |
bSaveOnDestroy |
bool | true | Save data when component is destroyed |
bValidateData |
bool | true | Enable data validation before operations |
CustomTableName |
FString | “” | Override default table name |
CustomMetadata |
TMap | {} | Additional metadata key-value pairs |
// Production configuration
FEntityPersistenceConfig ProductionConfig;
ProductionConfig.bAutoSave = true;
ProductionConfig.AutoSaveInterval = 180.0f; // 3 minutes
ProductionConfig.bAutoLoad = true;
ProductionConfig.bValidateData = true;
ProductionConfig.CustomTableName = TEXT("game_entities");
// Set custom metadata
ProductionConfig.CustomMetadata.Add(TEXT("version"), TEXT("1.0"));
ProductionConfig.CustomMetadata.Add(TEXT("type"), TEXT("player_data"));
EntityPersistence->SetConfiguration(ProductionConfig);
// Manual save
EntityPersistence->SaveData();
// Force immediate sync
EntityPersistence->ForceSync();
// Update data from current actor state
EntityPersistence->UpdateDataFromActor();
// Manual load
EntityPersistence->LoadData();
// Check if data exists before loading
if (EntityPersistence->GetCurrentState() == EEntityPersistenceState::Ready)
{
EntityPersistence->LoadData();
}
// Check current state
EEntityPersistenceState CurrentState = EntityPersistence->GetCurrentState();
// Available states:
// - Uninitialized: Component not yet initialized
// - Initializing: Component is initializing
// - Ready: Ready for operations
// - Saving: Save operation in progress
// - Loading: Load operation in progress
// - Error: Error state with retry logic
The component provides comprehensive event broadcasting for UI integration:
| Event | Parameters | Description |
|---|---|---|
OnSaveComplete |
Success (bool), Message (string) | Fired when save operation completes |
OnLoadComplete |
Success (bool), Message (string) | Fired when load operation completes |
OnError |
Success (bool), Message (string) | Fired when an error occurs |
OnDataChanged |
HasChanges (bool) | Fired when actor data changes |
// Bind to events
EntityPersistence->OnSaveComplete.AddDynamic(this, &AMyActor::OnSaveComplete);
EntityPersistence->OnLoadComplete.AddDynamic(this, &AMyActor::OnLoadComplete);
EntityPersistence->OnError.AddDynamic(this, &AMyActor::OnPersistenceError);
// Event handlers
UFUNCTION()
void OnSaveComplete(bool bSuccess, const FString& Message);
UFUNCTION()
void OnLoadComplete(bool bSuccess, const FString& Message);
UFUNCTION()
void OnPersistenceError(bool bSuccess, const FString& ErrorMessage);
The component automatically validates:
// Check validation status
bool bIsValid = EntityPersistence->ValidateCurrentData();
// Get detailed validation errors
FString ValidationErrors = EntityPersistence->GetValidationErrors();
// Example validation errors:
// "Entity ID is empty; Invalid transform; Invalid actor class"
// Disable validation for performance-critical scenarios
FEntityPersistenceConfig Config;
Config.bValidateData = false; // Skip validation
EntityPersistence->SetConfiguration(Config);
// Set custom metadata
EntityPersistence->SetCustomMetadata(TEXT("player_level"), TEXT("15"));
EntityPersistence->SetCustomMetadata(TEXT("last_checkpoint"), TEXT("level_3_boss"));
EntityPersistence->SetCustomMetadata(TEXT("completion_time"), TEXT("3600.5"));
// Get metadata
FString PlayerLevel = EntityPersistence->GetCustomMetadata(TEXT("player_level"));
The component implements intelligent retry logic:
// Automatic retry parameters
const int32 MaxOperationAttempts = 3;
const float OperationTimeout = 30.0f;
// Exponential backoff calculation
float RetryDelay = FMath::Pow(2.0f, CurrentAttempts - 1); // 1s, 2s, 4s
| Error Type | Recovery Action | User Action Required |
|---|---|---|
| Network Timeout | Auto-retry with backoff | None |
| Invalid Data | Validation error reported | Fix data validation issues |
| Connection Lost | Wait for connection restore | Check network/Supabase status |
| Table Missing | Error reported | Verify table exists in Supabase |
// Error event handler
void AMyActor::OnPersistenceError(bool bSuccess, const FString& ErrorMessage)
{
UE_LOG(LogTemp, Error, TEXT("Persistence Error: %s"), *ErrorMessage);
// Check if we can retry
if (EntityPersistence->GetCurrentState() == EEntityPersistenceState::Error)
{
// Component will auto-retry, but you can also manually retry
GetWorld()->GetTimerManager().SetTimer(
RetryTimer,
[this]() { EntityPersistence->SaveData(); },
5.0f, // Wait 5 seconds
false
);
}
}
The component uses efficient change detection:
// Lightweight tick system - only checks for changes when needed
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
// Manual change detection
EntityPersistence->UpdateDataFromActor(); // Only call when actor changes
// Automatic cleanup in EndPlay
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override
{
// Save pending changes
if (Config.bSaveOnDestroy && bHasPendingChanges && IsReady())
{
SaveData(); // Final save before cleanup
}
// Clean up timers
StopAutoSaveTimer();
// Clear event bindings
OnSaveComplete.Clear();
OnLoadComplete.Clear();
OnError.Clear();
}
For multiple entities, consider using APersistentMap instead:
// For single entities: UEntityPersistence
UEntityPersistence* EntityPersistence; // Per-actor component
// For multiple entities: APersistentMap
APersistentMap* PersistentMap; // Handles batch operations efficiently
// Use custom table for specific entity types
FEntityPersistenceConfig Config;
Config.CustomTableName = TEXT("player_characters");
EntityPersistence->SetConfiguration(Config);
// Table will be: "player_characters" instead of default "entity_persistence"
// Get current entity ID
FString CurrentID = EntityPersistence->GetEntityId();
// Regenerate entity ID (useful for duplicating actors)
EntityPersistence->RegenerateEntityId();
// Entity ID format: "ClassName_ActorName_GUID"
// Example: "BP_Player_PlayerCharacter_12345678-1234-1234-1234-123456789ABC"
// Set specific connection
EntityPersistence->SetConnection(MyCustomConnection);
// Get current connection
USupabaseConnection* CurrentConnection = EntityPersistence->GetConnection();
// Auto-fallback to subsystem connection if none set
// Component automatically uses USupabaseSubsystem connection when available
-- Entity persistence table schema
CREATE TABLE entity_persistence (
id TEXT PRIMARY KEY, -- Entity ID (generated)
actor_class TEXT NOT NULL, -- Actor class name
transform JSONB NOT NULL, -- Transform data (location, rotation, scale)
custom_data JSONB, -- Custom metadata
level_name TEXT, -- Level/map name
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX idx_entity_persistence_actor_class ON entity_persistence(actor_class);
CREATE INDEX idx_entity_persistence_level ON entity_persistence(level_name);
CREATE INDEX idx_entity_persistence_updated ON entity_persistence(updated_at);
// Transform data structure
{
"location": {"x": 100.0, "y": 200.0, "z": 50.0},
"rotation": {"x": 0.0, "y": 0.0, "z": 45.0},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
}
// Custom data structure
{
"player_level": "15",
"last_checkpoint": "level_3_boss",
"completion_time": "3600.5",
"inventory_items": "sword,shield,potion"
}
// Production settings
FEntityPersistenceConfig ProductionConfig;
ProductionConfig.bAutoSave = true;
ProductionConfig.AutoSaveInterval = 300.0f; // 5 minutes
ProductionConfig.bAutoLoad = true;
ProductionConfig.bValidateData = true;
ProductionConfig.bSaveOnDestroy = true;
// Development settings
FEntityPersistenceConfig DevConfig;
DevConfig.bAutoSave = false; // Manual saves during development
DevConfig.bValidateData = true; // Always validate during development
DevConfig.bSaveOnDestroy = false; // Prevent accidental saves
// Always bind to error events
EntityPersistence->OnError.AddDynamic(this, &AMyActor::OnPersistenceError);
// Implement graceful error handling
void AMyActor::OnPersistenceError(bool bSuccess, const FString& ErrorMessage)
{
// Log error for debugging
UE_LOG(LogGamePersistence, Error, TEXT("Entity Persistence Error: %s"), *ErrorMessage);
// Show user-friendly message
if (ErrorMessage.Contains(TEXT("connection")))
{
ShowNotification(TEXT("Connection lost. Will retry automatically."));
}
else if (ErrorMessage.Contains(TEXT("validation")))
{
ShowNotification(TEXT("Data validation failed. Please check your settings."));
}
}
| Issue | Cause | Solution |
|---|---|---|
| “Entity ID is empty” | Component not properly initialized | Ensure BeginPlay is called |
| “Invalid transform” | NaN or invalid scale values | Check actor transform for invalid values |
| “Connection timeout” | Network issues or slow Supabase response | Check network and Supabase status |
| “Table not found” | Missing table in Supabase | Create required table with proper schema |
| “Validation failed” | Data exceeds limits | Check metadata size and entry count |
// Enable debug logging
EntityPersistence->bDebugLogging = true;
// Logs will show:
// - State transitions
// - Operation attempts
// - Validation results
// - Timer events
// - Connection status
// Test save operation
EntityPersistence->SaveData();
// Test load operation
EntityPersistence->LoadData();
// Test validation
bool bIsValid = EntityPersistence->ValidateCurrentData();
FString Errors = EntityPersistence->GetValidationErrors();
// Test connection
bool bHasConnection = EntityPersistence->GetConnection() != nullptr;
// APlayerCharacter.h
UCLASS()
class MYGAME_API APlayerCharacter : public ACharacter
{
GENERATED_BODY()
public:
APlayerCharacter();
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Persistence")
UEntityPersistence* EntityPersistence;
UFUNCTION()
void OnSaveComplete(bool bSuccess, const FString& Message);
UFUNCTION()
void OnLoadComplete(bool bSuccess, const FString& Message);
// Custom data methods
UFUNCTION(BlueprintCallable, Category = "Persistence")
void SavePlayerStats();
UFUNCTION(BlueprintCallable, Category = "Persistence")
void LoadPlayerStats();
private:
UPROPERTY(EditAnywhere, Category = "Player Stats")
int32 PlayerLevel = 1;
UPROPERTY(EditAnywhere, Category = "Player Stats")
float ExperiencePoints = 0.0f;
};
// APlayerCharacter.cpp
APlayerCharacter::APlayerCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// Create persistence component
EntityPersistence = CreateDefaultSubobject<UEntityPersistence>(TEXT("EntityPersistence"));
}
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
// Configure persistence
FEntityPersistenceConfig Config;
Config.bAutoSave = true;
Config.AutoSaveInterval = 120.0f; // 2 minutes
Config.bAutoLoad = true;
Config.CustomTableName = TEXT("player_characters");
EntityPersistence->SetConfiguration(Config);
// Bind events
EntityPersistence->OnSaveComplete.AddDynamic(this, &APlayerCharacter::OnSaveComplete);
EntityPersistence->OnLoadComplete.AddDynamic(this, &APlayerCharacter::OnLoadComplete);
}
void APlayerCharacter::SavePlayerStats()
{
// Set custom metadata before saving
EntityPersistence->SetCustomMetadata(TEXT("player_level"), FString::FromInt(PlayerLevel));
EntityPersistence->SetCustomMetadata(TEXT("experience_points"), FString::SanitizeFloat(ExperiencePoints));
// Update actor data and save
EntityPersistence->UpdateDataFromActor();
EntityPersistence->SaveData();
}
void APlayerCharacter::LoadPlayerStats()
{
// Data will be loaded automatically, retrieve from metadata in OnLoadComplete
}
void APlayerCharacter::OnLoadComplete(bool bSuccess, const FString& Message)
{
if (bSuccess)
{
// Retrieve custom metadata
FString LevelStr = EntityPersistence->GetCustomMetadata(TEXT("player_level"));
FString ExpStr = EntityPersistence->GetCustomMetadata(TEXT("experience_points"));
if (!LevelStr.IsEmpty())
{
PlayerLevel = FCString::Atoi(*LevelStr);
}
if (!ExpStr.IsEmpty())
{
ExperiencePoints = FCString::Atof(*ExpStr);
}
UE_LOG(LogTemp, Log, TEXT("Player stats loaded: Level %d, XP %.2f"), PlayerLevel, ExperiencePoints);
}
}
This documentation is for Supabase UE5 Plugin v1.34.2. For the latest updates, visit our GitHub repository or join our Discord community.