Add Planar Capture System implementation checklist and developer reference

- Created a comprehensive implementation checklist for the Planar Capture System (Systems 136-147) detailing tasks across multiple phases including C++ core, material foundation, Blueprint actors, data assets, integration, and performance testing.
- Added a developer reference document outlining the architecture, data flow, state machine, budget enforcement, render target pooling, horror features, integration points, multiplayer networking, performance characteristics, debugging methods, and build order for the capture systems.
- Introduced examples of capture surface usage in the Project Void horror game, including specific implementations for mirrors, monitors, portals, and fake windows, along with a checklist for integration tasks.
This commit is contained in:
Lefteris Notas
2026-05-22 15:36:08 +03:00
parent 6b6c702dd7
commit 0a2d08b2ad
34 changed files with 5245 additions and 32 deletions

View File

@@ -0,0 +1,434 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — SS_PlanarCaptureManager implementation
#include "Capture/SS_PlanarCaptureManager.h"
#include "Capture/BP_PlanarCaptureActor.h"
#include "Capture/BPC_PlanarCapture.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/GameplayStatics.h"
ASS_PlanarCaptureManager::ASS_PlanarCaptureManager()
{
}
void ASS_PlanarCaptureManager::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Initialized for world."));
}
void ASS_PlanarCaptureManager::Deinitialize()
{
// Release all render targets
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
{
if (Entry.RenderTarget)
{
Entry.RenderTarget->ConditionalBeginDestroy();
}
}
RenderTargetPool.Empty();
RegisteredSurfaces.Empty();
Super::Deinitialize();
}
void ASS_PlanarCaptureManager::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
TimeSinceLastEvaluation += DeltaTime;
if (TimeSinceLastEvaluation >= FullEvaluationInterval)
{
TimeSinceLastEvaluation = 0.0f;
EvaluateAllSurfaces();
}
}
TStatId ASS_PlanarCaptureManager::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(ASS_PlanarCaptureManager, STATGROUP_Tickables);
}
// ========================================================================
// Surface Registry
// ========================================================================
void ASS_PlanarCaptureManager::RegisterSurface(ABP_PlanarCaptureActor* Surface)
{
if (!Surface)
{
return;
}
// Check for duplicates
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Existing : RegisteredSurfaces)
{
if (Existing.Get() == Surface)
{
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Surface '%s' already registered."),
*Surface->SurfaceDisplayName);
return;
}
}
RegisteredSurfaces.Add(Surface);
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Registered surface '%s'. Total: %d"),
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
OnSurfaceRegistered.Broadcast(Surface, RegisteredSurfaces.Num());
// Evaluate immediately to assign initial tier
EvaluateAllSurfaces();
}
void ASS_PlanarCaptureManager::UnregisterSurface(ABP_PlanarCaptureActor* Surface)
{
if (!Surface)
{
return;
}
RegisteredSurfaces.RemoveAll([Surface](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
{
return Entry.Get() == Surface;
});
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Unregistered surface '%s'. Total: %d"),
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
OnSurfaceUnregistered.Broadcast(Surface, RegisteredSurfaces.Num());
}
TArray<ABP_PlanarCaptureActor*> ASS_PlanarCaptureManager::GetRegisteredSurfaces() const
{
TArray<ABP_PlanarCaptureActor*> Result;
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
{
if (Entry.IsValid())
{
Result.Add(Entry.Get());
}
}
return Result;
}
// ========================================================================
// Quality Budget Management
// ========================================================================
void ASS_PlanarCaptureManager::ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier)
{
ForceTierOverride = Tier;
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
{
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
{
if (UBPC_PlanarCapture* Capture = Surface->CaptureComponent)
{
Capture->ApplyQualityTier(Tier);
}
}
}
}
void ASS_PlanarCaptureManager::ReleaseForceTier()
{
ForceTierOverride.Reset();
EvaluateAllSurfaces();
}
// ========================================================================
// Render Target Pool
// ========================================================================
UTextureRenderTarget2D* ASS_PlanarCaptureManager::RequestRenderTarget(int32 Size)
{
// Check pool for an available RT of the right size
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
{
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
{
Entry.bInUse = true;
return Entry.RenderTarget;
}
}
// Create a new one
return CreateRenderTarget(Size);
}
void ASS_PlanarCaptureManager::ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget)
{
if (!RenderTarget)
{
return;
}
// Find the entry and mark as free
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
{
if (Entry.RenderTarget == RenderTarget)
{
Entry.bInUse = false;
Entry.OwningSurface.Reset();
return;
}
}
// Not in pool — add it
FPlanarCaptureRenderTargetEntry NewEntry;
NewEntry.RenderTarget = RenderTarget;
NewEntry.CurrentSize = RenderTarget->SizeX;
NewEntry.bInUse = false;
RenderTargetPool.Add(NewEntry);
}
float ASS_PlanarCaptureManager::GetPoolMemoryUsageMB() const
{
float TotalBytes = 0.0f;
for (const FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
{
if (Entry.RenderTarget)
{
// RGBA8 = 4 bytes per pixel
TotalBytes += static_cast<float>(Entry.CurrentSize * Entry.CurrentSize * 4);
}
}
return TotalBytes / (1024.0f * 1024.0f);
}
// ========================================================================
// Query
// ========================================================================
ABP_PlanarCaptureActor* ASS_PlanarCaptureManager::GetNearestSurfaceOfMode(
EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance) const
{
ABP_PlanarCaptureActor* Nearest = nullptr;
float NearestDistSq = (MaxDistance > 0.0f) ? (MaxDistance * MaxDistance) : FLT_MAX;
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
{
if (ABP_PlanarCaptureActor* Surface = Entry.Get())
{
if (!Surface->CaptureComponent || Surface->CaptureComponent->CaptureMode != Mode)
{
continue;
}
const float DistSq = FVector::DistSquared(Surface->GetActorLocation(), WorldLocation);
if (DistSq < NearestDistSq)
{
NearestDistSq = DistSq;
Nearest = Surface;
}
}
}
return Nearest;
}
// ========================================================================
// Internal Methods
// ========================================================================
void ASS_PlanarCaptureManager::EvaluateAllSurfaces()
{
// Reset tier counts
TierAssignmentCounts.Empty();
// Ensure registered surfaces are still valid
RegisteredSurfaces.RemoveAll([](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
{
return !Entry.IsValid();
});
// If force tier override is active, apply it to all
if (ForceTierOverride.IsSet())
{
ForceAllSurfacesToTier(ForceTierOverride.GetValue());
return;
}
// Score and assign tiers
TArray<TPair<ABP_PlanarCaptureActor*, float>> ScoredSurfaces;
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
{
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
{
if (!Surface->bIsActive || !Surface->CaptureComponent)
{
continue;
}
UBPC_PlanarCapture* Capture = Surface->CaptureComponent;
FPlanarCaptureScore Score = Capture->GetCurrentScore();
ScoredSurfaces.Add(TPair<ABP_PlanarCaptureActor*, float>(Surface, Score.CompositeScore));
}
}
// Sort by score descending (highest score first)
ScoredSurfaces.Sort([](const TPair<ABP_PlanarCaptureActor*, float>& A,
const TPair<ABP_PlanarCaptureActor*, float>& B)
{
return A.Value > B.Value;
});
// Assign tiers within budget
for (const auto& Pair : ScoredSurfaces)
{
ABP_PlanarCaptureActor* Surface = Pair.Key;
float Score = Pair.Value;
if (!Surface->CaptureComponent)
{
continue;
}
EPlanarCaptureQualityTier AssignedTier = ScoreAndAssignTier(Surface->CaptureComponent);
// Apply tier
Surface->CaptureComponent->ApplyQualityTier(AssignedTier);
}
}
EPlanarCaptureQualityTier ASS_PlanarCaptureManager::ScoreAndAssignTier(UBPC_PlanarCapture* Capture)
{
if (!Capture)
{
return EPlanarCaptureQualityTier::Off;
}
const FPlanarCaptureScore Score = Capture->GetCurrentScore();
// If not visible, off
if (!Score.bInFrustum && Score.CompositeScore < 0.01f)
{
return EPlanarCaptureQualityTier::Off;
}
// If too far, off
if (Score.DistanceToViewer > MaxCaptureDistance)
{
return EPlanarCaptureQualityTier::Off;
}
// Determine natural tier based on composite score
EPlanarCaptureQualityTier NaturalTier;
if (Score.CompositeScore >= 0.8f)
{
NaturalTier = EPlanarCaptureQualityTier::Hero;
}
else if (Score.CompositeScore >= 0.5f)
{
NaturalTier = EPlanarCaptureQualityTier::High;
}
else if (Score.CompositeScore >= 0.2f)
{
NaturalTier = EPlanarCaptureQualityTier::Medium;
}
else
{
NaturalTier = EPlanarCaptureQualityTier::Low;
}
// Apply global quality cap
if (static_cast<int32>(NaturalTier) > static_cast<int32>(GlobalQualityCap))
{
NaturalTier = GlobalQualityCap;
}
// Check budget limits
const int32 CurrentCount = TierAssignmentCounts.FindRef(NaturalTier);
switch (NaturalTier)
{
case EPlanarCaptureQualityTier::Hero:
if (CurrentCount >= MaxHeroSurfaces)
{
NaturalTier = EPlanarCaptureQualityTier::High;
}
break;
case EPlanarCaptureQualityTier::High:
if (CurrentCount >= MaxHighSurfaces)
{
NaturalTier = EPlanarCaptureQualityTier::Medium;
}
break;
case EPlanarCaptureQualityTier::Medium:
if (CurrentCount >= MaxMediumSurfaces)
{
NaturalTier = EPlanarCaptureQualityTier::Low;
}
break;
default:
break;
}
// Increment the assigned tier count
TierAssignmentCounts.FindOrAdd(NaturalTier)++;
return NaturalTier;
}
void ASS_PlanarCaptureManager::EnforceBudgetLimits()
{
// Additional enforcement for total memory budget
float TotalMemoryMB = GetPoolMemoryUsageMB();
if (TotalMemoryMB > MaxTotalRenderTargetMemoryMB)
{
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Render target memory budget exceeded (%.2f MB / %.2f MB)"),
TotalMemoryMB, MaxTotalRenderTargetMemoryMB);
// If over budget, demote lowest-priority surfaces
// Future: implement more sophisticated memory budget enforcement
}
}
UTextureRenderTarget2D* ASS_PlanarCaptureManager::CreateRenderTarget(int32 Size)
{
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(this);
if (!RT)
{
return nullptr;
}
RT->InitCustomFormat(Size, Size, PF_B8G8R8A8, false);
RT->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8;
RT->bGPUSharedFlag = false;
RT->ClearColor = FLinearColor::Black;
RT->UpdateResourceImmediate(true);
FPlanarCaptureRenderTargetEntry Entry;
Entry.RenderTarget = RT;
Entry.CurrentSize = Size;
Entry.bInUse = true;
RenderTargetPool.Add(Entry);
return RT;
}
UTextureRenderTarget2D* ASS_PlanarCaptureManager::GetOrCreateRenderTarget(int32 Size)
{
// Check pool first
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
{
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
{
Entry.bInUse = true;
return Entry.RenderTarget;
}
}
return CreateRenderTarget(Size);
}