Add placed actor persistence foundation

This commit is contained in:
2026-05-11 01:00:41 -07:00
parent e81138425b
commit 9a0a3608fb
7 changed files with 234 additions and 1 deletions
+4 -1
View File
@@ -24,6 +24,9 @@
- [x] Resource nodes can use item definition assets for harvest yields.
- [x] Building placement component added.
- [x] Character now owns a building placement component.
- [x] Persistent actor component added.
- [x] Persistence subsystem can capture and restore saveable world actors.
- [x] Primitive shelter actor is marked as a persistent world actor.
## Next Unreal Editor Tasks
@@ -45,6 +48,6 @@
- [x] Add building placement component.
- [x] Add simple crafting recipe defaults or data asset pipeline.
- [x] Add item definition data asset class.
- [ ] Add save/load capture for placed actors.
- [x] Add save/load capture for placed actors.
- [ ] Add admin/dev console commands.
- [ ] Add wildlife base actor.
@@ -1,7 +1,10 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianPersistenceSubsystem.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianSaveGame.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
UAgrarianSaveGame* UAgrarianPersistenceSubsystem::CreateEmptySave() const
@@ -31,3 +34,121 @@ bool UAgrarianPersistenceSubsystem::DoesSaveExist() const
{
return UGameplayStatics::DoesSaveGameExist(DefaultSlotName, UserIndex);
}
void UAgrarianPersistenceSubsystem::RegisterWorldActorClass(FName ActorTypeId, TSubclassOf<AActor> ActorClass)
{
if (ActorTypeId != NAME_None && ActorClass)
{
WorldActorClassRegistry.Add(ActorTypeId, ActorClass);
}
}
int32 UAgrarianPersistenceSubsystem::CaptureWorldActors(UAgrarianSaveGame* SaveGame) const
{
if (!SaveGame)
{
return 0;
}
TArray<UAgrarianPersistentActorComponent*> PersistentComponents;
FindPersistentComponents(PersistentComponents);
SaveGame->WorldActors.Reset();
for (const UAgrarianPersistentActorComponent* Component : PersistentComponents)
{
if (Component && Component->IsSaveable())
{
SaveGame->WorldActors.Add(Component->CaptureSaveState());
}
}
return SaveGame->WorldActors.Num();
}
int32 UAgrarianPersistenceSubsystem::RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors) const
{
UWorld* World = GetWorld();
if (!World || !SaveGame)
{
return 0;
}
if (bClearExistingActors)
{
TArray<UAgrarianPersistentActorComponent*> ExistingComponents;
FindPersistentComponents(ExistingComponents);
for (UAgrarianPersistentActorComponent* Component : ExistingComponents)
{
if (AActor* Owner = Component ? Component->GetOwner() : nullptr)
{
Owner->Destroy();
}
}
}
int32 RestoredCount = 0;
for (const FAgrarianSavedWorldActor& SavedActor : SaveGame->WorldActors)
{
const TSubclassOf<AActor>* ActorClass = WorldActorClassRegistry.Find(SavedActor.ActorTypeId);
if (!ActorClass || !(*ActorClass))
{
continue;
}
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* SpawnedActor = World->SpawnActor<AActor>(*ActorClass, SavedActor.Transform, SpawnParams);
if (!SpawnedActor)
{
continue;
}
if (UAgrarianPersistentActorComponent* Component = SpawnedActor->FindComponentByClass<UAgrarianPersistentActorComponent>())
{
Component->ApplySaveState(SavedActor);
}
RestoredCount++;
}
return RestoredCount;
}
bool UAgrarianPersistenceSubsystem::SaveCurrentWorld() const
{
UAgrarianSaveGame* SaveGame = LoadOrCreateSave();
if (!SaveGame)
{
return false;
}
CaptureWorldActors(SaveGame);
return WriteSave(SaveGame);
}
void UAgrarianPersistenceSubsystem::FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const
{
OutComponents.Reset();
UWorld* World = GetWorld();
if (!World)
{
return;
}
for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
{
AActor* Actor = *ActorIt;
if (!Actor || Actor->IsPendingKillPending())
{
continue;
}
if (UAgrarianPersistentActorComponent* Component = Actor->FindComponentByClass<UAgrarianPersistentActorComponent>())
{
OutComponents.Add(Component);
}
}
}
@@ -7,6 +7,7 @@
#include "AgrarianPersistenceSubsystem.generated.h"
class UAgrarianSaveGame;
class UAgrarianPersistentActorComponent;
UCLASS()
class UAgrarianPersistenceSubsystem : public UGameInstanceSubsystem
@@ -20,6 +21,9 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
int32 UserIndex = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
TMap<FName, TSubclassOf<AActor>> WorldActorClassRegistry;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
UAgrarianSaveGame* CreateEmptySave() const;
@@ -31,4 +35,19 @@ public:
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool DoesSaveExist() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
void RegisterWorldActorClass(FName ActorTypeId, TSubclassOf<AActor> ActorClass);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 CaptureWorldActors(UAgrarianSaveGame* SaveGame) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
int32 RestoreWorldActors(const UAgrarianSaveGame* SaveGame, bool bClearExistingActors = true) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool SaveCurrentWorld() const;
protected:
void FindPersistentComponents(TArray<UAgrarianPersistentActorComponent*>& OutComponents) const;
};
@@ -0,0 +1,44 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianPersistentActorComponent.h"
UAgrarianPersistentActorComponent::UAgrarianPersistentActorComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
bool UAgrarianPersistentActorComponent::IsSaveable() const
{
return ActorTypeId != NAME_None && GetOwner() != nullptr;
}
FAgrarianSavedWorldActor UAgrarianPersistentActorComponent::CaptureSaveState() const
{
FAgrarianSavedWorldActor SavedActor;
SavedActor.ActorTypeId = ActorTypeId;
SavedActor.StringState = StringState;
SavedActor.NumberState = NumberState;
if (bSaveTransform && GetOwner())
{
SavedActor.Transform = GetOwner()->GetActorTransform();
}
return SavedActor;
}
void UAgrarianPersistentActorComponent::ApplySaveState(const FAgrarianSavedWorldActor& SavedActor)
{
StringState = SavedActor.StringState;
NumberState = SavedActor.NumberState;
if (ActorTypeId == NAME_None)
{
ActorTypeId = SavedActor.ActorTypeId;
}
if (bSaveTransform && GetOwner())
{
GetOwner()->SetActorTransform(SavedActor.Transform);
}
}
@@ -0,0 +1,38 @@
// Copyright Pacificao. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AgrarianSaveGame.h"
#include "AgrarianPersistentActorComponent.generated.h"
UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class UAgrarianPersistentActorComponent : public UActorComponent
{
GENERATED_BODY()
public:
UAgrarianPersistentActorComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
FName ActorTypeId = NAME_None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
bool bSaveTransform = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
TMap<FName, FString> StringState;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Persistence")
TMap<FName, float> NumberState;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
bool IsSaveable() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
FAgrarianSavedWorldActor CaptureSaveState() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Persistence")
void ApplySaveState(const FAgrarianSavedWorldActor& SavedActor);
};
@@ -1,6 +1,7 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianShelterActor.h"
#include "AgrarianPersistentActorComponent.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
@@ -15,4 +16,7 @@ AAgrarianShelterActor::AAgrarianShelterActor()
ProtectionVolume->SetupAttachment(RootComponent);
ProtectionVolume->SetBoxExtent(FVector(250.0f, 250.0f, 180.0f));
ProtectionVolume->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
PersistentActorComponent = CreateDefaultSubobject<UAgrarianPersistentActorComponent>(TEXT("PersistentActorComponent"));
PersistentActorComponent->ActorTypeId = TEXT("primitive_shelter");
}
@@ -7,6 +7,7 @@
#include "AgrarianShelterActor.generated.h"
class UBoxComponent;
class UAgrarianPersistentActorComponent;
class UStaticMeshComponent;
UCLASS(Blueprintable)
@@ -23,6 +24,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Shelter")
TObjectPtr<UBoxComponent> ProtectionVolume;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Shelter")
TObjectPtr<UAgrarianPersistentActorComponent> PersistentActorComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Shelter", meta = (ClampMin = "0", ClampMax = "1"))
float WeatherProtection = 0.65f;
};