SurfaceManager
The SurfaceManager is a specialized library component that handles surface detection, grid visualization, and surface-specific functionality for the Grid Placement System. It automatically discovers tagged surfaces in the level and provides dynamic grid overlays that adapt to different placement types.
Overview
SurfaceManager acts as the bridge between the placement system and the level geometry, automatically discovering surfaces tagged for different placement types (Floor, Wall, Roof) and managing their visual grid overlays. It provides efficient surface grouping, dynamic material management, and real-time grid visualization that appears only when needed.
Key Features
- ✅ Automatic Surface Discovery - Finds all tagged surfaces in the level
- ✅ Dynamic Grid Visualization - Shows grids only for current placement type
- ✅ Material Management - Creates and manages dynamic grid materials
- ✅ Performance Optimized - Efficient surface grouping and rendering
- ✅ Multi-Surface Support - Handles complex levels with many surface types
- ✅ Runtime Refresh - Can detect new surfaces added at runtime
- ✅ Flexible Grid Patterns - Supports custom grid textures and styles
Data Structures
FSurfaceGroup
USTRUCT(BlueprintType)
struct FSurfaceGroup
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TArray<AActor*> Surfaces;
};
Groups all surfaces of the same placement type for efficient management.
Core Methods
Initialization
Initialize
void Initialize(UWorld* World, const TMap<EPlacementType, FName>& PlacementTypeTags);
Initializes the SurfaceManager with world context and tag mappings.
Parameters:
World
- The world context for surface discoveryPlacementTypeTags
- Mapping between placement types and surface tags
Surface Management
CollectSurfaces
void CollectSurfaces();
Discovers and catalogs all surfaces in the level based on tags.
Process:
- Clears existing surface groups
- Searches level for actors with placement type tags
- Groups surfaces by placement type
- Hides all grids initially
Usage:
// Called during initialization or when level changes
SurfaceManager->CollectSurfaces();
RefreshGrids
void RefreshGrids();
Re-scans the level for surfaces and updates the surface database.
Usage:
// Called when new objects are placed or level geometry changes
SurfaceManager->RefreshGrids();
Grid Visualization
ShowGridForPlacementType
void ShowGridForPlacementType(EPlacementType PlacementType);
Displays grid overlays for all surfaces of the specified placement type.
Parameters:
PlacementType
- The placement type to show grids for
Process:
- Hides all existing grids
- Finds surface group for placement type
- Creates dynamic grid materials for each surface
- Applies grid materials as overlay materials
Material Configuration:
MID->SetScalarParameterValue(TEXT("GridSize"), GridSize);
MID->SetVectorParameterValue(TEXT("GridColor"), GridColor);
MID->SetVectorParameterValue(TEXT("SubGridColor"), SubGridColor);
if (GridPattern)
{
MID->SetTextureParameterValue(TEXT("GridPattern"), GridPattern);
}
HideAllGrids
void HideAllGrids();
Hides all grid overlays across all surface types.
Process:
- Iterates through all surface groups
- Removes overlay materials from all surface meshes
- Validates actors before material operations
Configuration
Grid Settings
SetGridMaterial
void SetGridMaterial(UMaterialInterface* Material);
Sets the base material used for grid overlays.
SetGridSize
void SetGridSize(float Size);
Sets the grid cell size for all grid overlays.
SetGridColor
void SetGridColor(const FLinearColor& Color);
Sets the primary grid line color.
SetSubGridColor
void SetSubGridColor(const FLinearColor& Color);
Sets the sub-division grid line color.
SetGridPattern
void SetGridPattern(UTexture2D* Pattern);
Sets an optional texture pattern for the grid overlay.
Data Access
GetPlacementSurfaces
const TMap<EPlacementType, FSurfaceGroup>& GetPlacementSurfaces() const;
Returns the complete surface database for external access.
Surface Discovery System
Tag-Based Detection
SurfaceManager uses actor tags to identify surfaces:
for (const auto& PlacementTypeTag : PlacementTypeTags)
{
EPlacementType PlacementType = PlacementTypeTag.Key;
FName TagName = PlacementTypeTag.Value;
TArray<AActor*> TaggedActors;
UGameplayStatics::GetAllActorsWithTag(CachedWorld, TagName, TaggedActors);
if (TaggedActors.Num() > 0)
{
FSurfaceGroup Group;
Group.Surfaces = TaggedActors;
PlacementSurfaces.Add(PlacementType, Group);
}
}
Surface Validation
Each discovered surface is validated before use:
if (IsValid(Surface) && Surface->GetRootComponent())
{
if (UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Surface->GetRootComponent()))
{
// Surface is valid for grid overlay
ApplyGridMaterial(Mesh);
}
}
Grid Material System
Dynamic Material Creation
void ApplyGridToSurface(AActor* Surface, EPlacementType PlacementType)
{
if (UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Surface->GetRootComponent()))
{
UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(GridMaterial, Surface);
if (MID)
{
// Configure grid parameters
MID->SetScalarParameterValue(TEXT("GridSize"), GridSize);
MID->SetVectorParameterValue(TEXT("GridColor"), GridColor);
MID->SetVectorParameterValue(TEXT("SubGridColor"), SubGridColor);
// Apply optional pattern
if (GridPattern)
{
MID->SetTextureParameterValue(TEXT("GridPattern"), GridPattern);
}
// Apply as overlay material
Mesh->SetOverlayMaterial(MID);
}
}
}
Material Safety
// Safe material application with validation
if (Mesh && IsValid(Mesh) && !Mesh->IsBeingDestroyed())
{
Mesh->SetOverlayMaterial(MID);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Mesh became invalid before setting overlay material on %s"),
*Surface->GetName());
}
Integration with PlacementSystemComponent
Automatic Configuration
// In PlacementSystemComponent::ConfigureComponents()
if (SurfaceManager)
{
SurfaceManager->SetGridMaterial(GridMaterial);
SurfaceManager->SetGridSize(GridSize);
SurfaceManager->SetGridColor(GridColor);
SurfaceManager->SetSubGridColor(SubGridColor);
SurfaceManager->SetGridPattern(GridPattern);
}
Initialization Process
// In PlacementSystemComponent::InitializeComponents()
if (SurfaceManager)
{
SurfaceManager->Initialize(GetWorld(), PlacementTypeTags);
}
// During BeginPlay
if (bAutoCreateGrids && GetWorld() && SurfaceManager)
{
SurfaceManager->CollectSurfaces();
}
Grid Display Logic
// When placement type changes
if (SurfaceManager)
{
SurfaceManager->ShowGridForPlacementType(CurrentPlacementType);
}
// When objects are placed
if (SurfaceManager)
{
SurfaceManager->RefreshGrids();
}
Usage Examples
Basic Setup
// Initialize surface manager
TMap<EPlacementType, FName> PlacementTags = {
{EPlacementType::Floor, TEXT("Floor")},
{EPlacementType::Wall, TEXT("Wall")},
{EPlacementType::Roof, TEXT("Roof")}
};
SurfaceManager->Initialize(GetWorld(), PlacementTags);
SurfaceManager->SetGridMaterial(GridMaterialAsset);
SurfaceManager->SetGridSize(25.0f);
// Discover surfaces in level
SurfaceManager->CollectSurfaces();
Dynamic Grid Control
// Show grids for current placement mode
void UpdateGridDisplay(EPlacementType NewPlacementType)
{
if (SurfaceManager)
{
SurfaceManager->ShowGridForPlacementType(NewPlacementType);
UE_LOG(LogTemp, Log, TEXT("Showing grids for %s placement"),
*UEnum::GetValueAsString(NewPlacementType));
}
}
// Hide all grids (useful for screenshot mode)
void HideAllGrids()
{
if (SurfaceManager)
{
SurfaceManager->HideAllGrids();
UE_LOG(LogTemp, Log, TEXT("All grids hidden"));
}
}
Runtime Surface Discovery
// Add new surfaces at runtime
void OnLevelGeometryChanged()
{
if (SurfaceManager)
{
SurfaceManager->RefreshGrids();
// Check how many surfaces were found
const auto& Surfaces = SurfaceManager->GetPlacementSurfaces();
for (const auto& SurfaceGroup : Surfaces)
{
EPlacementType Type = SurfaceGroup.Key;
int32 Count = SurfaceGroup.Value.Surfaces.Num();
UE_LOG(LogTemp, Log, TEXT("Found %d %s surfaces"),
Count, *UEnum::GetValueAsString(Type));
}
}
}
Custom Grid Appearance
// Configure custom grid appearance
void SetupCustomGrids()
{
SurfaceManager->SetGridSize(50.0f); // Larger grid cells
SurfaceManager->SetGridColor(FLinearColor::Blue); // Blue main lines
SurfaceManager->SetSubGridColor(FLinearColor::Cyan); // Cyan sub-lines
// Optional: Set grid pattern texture
if (CustomGridTexture)
{
SurfaceManager->SetGridPattern(CustomGridTexture);
}
// Refresh to apply changes
SurfaceManager->ShowGridForPlacementType(CurrentPlacementType);
}
Surface Analysis
// Analyze discovered surfaces
void AnalyzeLevelSurfaces()
{
if (!SurfaceManager) return;
const auto& AllSurfaces = SurfaceManager->GetPlacementSurfaces();
UE_LOG(LogTemp, Log, TEXT("=== Level Surface Analysis ==="));
for (const auto& SurfaceGroup : AllSurfaces)
{
EPlacementType Type = SurfaceGroup.Key;
const FSurfaceGroup& Group = SurfaceGroup.Value;
UE_LOG(LogTemp, Log, TEXT("%s Surfaces: %d"),
*UEnum::GetValueAsString(Type), Group.Surfaces.Num());
// Analyze individual surfaces
for (AActor* Surface : Group.Surfaces)
{
if (Surface)
{
FVector Location = Surface->GetActorLocation();
FVector Bounds = Surface->GetActorBounds(false).BoxExtent;
UE_LOG(LogTemp, Log, TEXT(" - %s at %s (Size: %.1fx%.1fx%.1f)"),
*Surface->GetName(), *Location.ToString(),
Bounds.X * 2, Bounds.Y * 2, Bounds.Z * 2);
}
}
}
}
Performance Considerations
Efficient Surface Management
- Batched Operations: Groups surfaces by type for efficient processing
- Lazy Material Creation: Creates materials only when grids are shown
- Validation Checks: Prevents crashes with invalid or destroyed actors
- Memory Management: Properly manages dynamic material instances
Best Practices
// Efficient surface discovery
void OptimizedSurfaceCollection()
{
// Cache world reference to avoid repeated lookups
if (!CachedWorld) return;
// Use batch operations when possible
TArray<AActor*> AllActors;
UGameplayStatics::GetAllActorsOfClass(CachedWorld, AActor::StaticClass(), AllActors);
// Pre-filter by tags in single pass
TMap<EPlacementType, TArray<AActor*>> FilteredSurfaces;
for (AActor* Actor : AllActors)
{
if (!Actor) continue;
for (const auto& TagPair : PlacementTypeTags)
{
if (Actor->Tags.Contains(TagPair.Value))
{
FilteredSurfaces.FindOrAdd(TagPair.Key).Add(Actor);
break; // Actor can only belong to one surface type
}
}
}
// Update surface groups
PlacementSurfaces.Empty();
for (const auto& FilteredGroup : FilteredSurfaces)
{
FSurfaceGroup Group;
Group.Surfaces = FilteredGroup.Value;
PlacementSurfaces.Add(FilteredGroup.Key, Group);
}
}
Memory Optimization
// Prevent memory leaks with dynamic materials
void CleanupGridMaterials()
{
for (const auto& SurfaceGroup : PlacementSurfaces)
{
for (AActor* Surface : SurfaceGroup.Value.Surfaces)
{
if (IsValid(Surface))
{
if (UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Surface->GetRootComponent()))
{
// Clear overlay material to free dynamic material instance
Mesh->SetOverlayMaterial(nullptr);
}
}
}
}
}
Error Handling
Surface Validation
bool ValidateSurface(AActor* Surface)
{
if (!Surface)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Null surface actor"));
return false;
}
if (!IsValid(Surface))
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Invalid surface actor"));
return false;
}
if (Surface->IsBeingDestroyed())
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Surface is being destroyed"));
return false;
}
if (!Surface->GetRootComponent())
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Surface has no root component"));
return false;
}
UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Surface->GetRootComponent());
if (!Mesh)
{
UE_LOG(LogTemp, Warning, TEXT("SurfaceManager: Surface root is not StaticMeshComponent"));
return false;
}
return true;
}
Safe Material Application
void SafeApplyGridMaterial(AActor* Surface, UMaterialInstanceDynamic* Material)
{
if (!ValidateSurface(Surface)) return;
UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Surface->GetRootComponent());
try
{
Mesh->SetOverlayMaterial(Material);
}
catch (...)
{
UE_LOG(LogTemp, Error, TEXT("SurfaceManager: Failed to apply grid material to %s"),
*Surface->GetName());
}
}
Troubleshooting
Common Issues
"No grids showing"
- Verify surfaces are tagged correctly in the level
- Check that GridMaterial is assigned and valid
- Ensure CollectSurfaces() has been called
- Verify ShowGridForPlacementType() is being called
"Grids appear on wrong surfaces"
- Check PlacementTypeTags mapping
- Verify actor tags match exactly (case-sensitive)
- Ensure surfaces don't have multiple placement tags
"Performance issues with many surfaces"
- Consider using LODs for complex surface meshes
- Implement distance-based grid culling
- Batch surface operations where possible
"Grids not updating when level changes"
- Call RefreshGrids() after placing objects
- Ensure new surfaces have proper tags
- Check that surface actors are properly registered in world
Debug Commands
// Debug surface discovery
void DebugSurfaceManager()
{
if (!SurfaceManager) return;
UE_LOG(LogTemp, Log, TEXT("=== SurfaceManager Debug ==="));
const auto& AllSurfaces = SurfaceManager->GetPlacementSurfaces();
UE_LOG(LogTemp, Log, TEXT("Surface Groups: %d"), AllSurfaces.Num());
for (const auto& SurfaceGroup : AllSurfaces)
{
EPlacementType Type = SurfaceGroup.Key;
const FSurfaceGroup& Group = SurfaceGroup.Value;
UE_LOG(LogTemp, Log, TEXT("%s: %d surfaces"),
*UEnum::GetValueAsString(Type), Group.Surfaces.Num());
// Validate each surface
int32 ValidSurfaces = 0;
for (AActor* Surface : Group.Surfaces)
{
if (ValidateSurface(Surface))
{
ValidSurfaces++;
}
}
UE_LOG(LogTemp, Log, TEXT(" Valid: %d, Invalid: %d"),
ValidSurfaces, Group.Surfaces.Num() - ValidSurfaces);
}
}
// Test grid display
void TestGridDisplay()
{
UE_LOG(LogTemp, Log, TEXT("Testing grid display..."));
// Test each placement type
TArray<EPlacementType> PlacementTypes = {
EPlacementType::Floor,
EPlacementType::Wall,
EPlacementType::Roof
};
for (EPlacementType Type : PlacementTypes)
{
UE_LOG(LogTemp, Log, TEXT("Showing %s grids"), *UEnum::GetValueAsString(Type));
SurfaceManager->ShowGridForPlacementType(Type);
// Wait briefly between tests
FPlatformProcess::Sleep(1.0f);
}
// Hide all at the end
SurfaceManager->HideAllGrids();
UE_LOG(LogTemp, Log, TEXT("Grid display test complete"));
}
SurfaceManager provides robust surface discovery and grid visualization that automatically adapts to level geometry while maintaining excellent performance and providing comprehensive debugging capabilities.