How to clamp an orthographic camera to a grid (world-space bounds) in C++?

Hi everyone,

I’m building a Fallout Shelter / SimTower–style side-view base management game in UE5 (2D gameplay on an XZ grid, rooms placed in cells, orthographic camera).

Right now I’m trying to hard-limit the camera to the exact playable area of my grid (so the camera can never show anything outside the grid bounds). The grid itself is data-driven (a struct array / generated cells), not individual actors per cell.

What I tried

I attempted to clamp the camera using calculated world bounds derived from my grid settings:

  • Grid origin (world)

  • Cell size

  • Grid width/height in cells

  • Optional “forbidden columns” on the left/right (spawn lanes that must never be visible)

Then every tick I:

  • read viewport aspect ratio

  • compute orthographic view half-extents from OrthoWidth and aspect

  • clamp camera center so camera edges stay inside the computed world rectangle

  • also clamp zoom so zoom-out can’t reveal outside the rectangle

What’s going wrong

Even though the math seems correct, the results are consistently wrong in-game:

  • when panning upward, the camera stops early and can’t reach ~9 top rows

  • horizontally it still leaks ~2 cells outside left and right

  • downward it leaks ~3 cells outside

  • the “margin” changes depending on zoom: when zoomed out the offsets become much larger, which makes the clamp feel broken

So it looks like my “grid bounds” calculation is not matching what the camera sees as world space.

Why I think it happens

Because the grid is mostly struct data, there is no actual per-cell scene component/actor transform to query. I’m generating world positions mathematically from config values, but the camera seems to be constrained by something else (level placement/transform/offset/rotation/scale, camera component offsets, or something in the actor hierarchy).

I suspect the mismatch is caused by some transform offset (GridActor moved/rotated/scaled in the level, background plane rotation/offset, camera pawn being a BP child with hidden component offsets, etc.). That would explain why the “correct” numbers don’t match what’s visible.

Question

What’s the recommended UE5 approach to clamp an orthographic camera to a grid area when the grid is data-driven, so the camera bounds are based on the actual world placement (what you see in the level), not just config numbers?

I’m open to any approach:

  • computing bounds from the GridActor’s transform/components (e.g., component bounds)

  • using a dedicated “bounds component/volume” driven by the grid

  • or even best practices for this kind of camera constraint in a grid-based side-view builder

I’m doing everything in C++. Any ideas or patterns that work reliably would be hugely appreciated.

Thanks!

Hello @eiDFirst how are you?

Your grid math is correct in grid-space, but you’re missing the Grid Actor’s world transform. Every offset, rotation, or scale applied to your GridActor invalidates your calculations.

The solution I found isto use Actor Transform + Component Bounds. Let me explain:

  1. Store Grid Bounds as a Component. Add a UBoxComponent to your GridActor that represents the visible playable area:
// In GridActor.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TObjectPtr<UBoxComponent> PlayableBounds;

// In GridActor.cpp constructor
PlayableBounds = CreateDefaultSubobject<UBoxComponent>(TEXT("PlayableBounds"));
RootComponent = PlayableBounds;
PlayableBounds->SetBoxExtent(FVector(1, 1, 1)); // Placeholder, will be set dynamically
  1. Update Bounds When Grid Changes. After your grid is initialized (or when forbidden columns/dimensions change):
void AGridActor::UpdatePlayableBounds()
{
    // Calculate playable grid dimensions (excluding forbidden columns)
    const int32 VisibleColumns = GridWidth - ForbiddenColumnsLeft - ForbiddenColumnsRight;
    const int32 VisibleRows = GridHeight;
    
    // Calculate world-space extents
    // For XZ grid: X = width, Z = height, Y = negligible depth
    const FVector Extents(
        VisibleColumns * CellSize * 0.5f,  // Half-width in X
        CellSize * 0.5f,                    // Minimal Y depth
        VisibleRows * CellSize * 0.5f       // Half-height in Z
    );
    
    PlayableBounds->SetBoxExtent(Extents);
    
    // Calculate center offset (accounting for forbidden columns)
    const FVector CenterOffset(
        (ForbiddenColumnsLeft - ForbiddenColumnsRight) * CellSize * 0.5f,
        0.0f,
        0.0f
    );
    
    PlayableBounds->SetRelativeLocation(GridOriginLocal + CenterOffset);
}
  1. Camera Clamping Using Component Bounds. In your camera tick/update function:
void ACameraActor::ClampToGridBounds()
{
    if (!GridActor || !GridActor->PlayableBounds) return;
    
    // Get WORLD-SPACE bounds (respects all transforms)
    const FBox WorldBounds = GridActor->PlayableBounds->Bounds.GetBox();
    
    // Calculate current orthographic view extents
    const float AspectRatio = GetCameraComponent()->AspectRatio;
    const float OrthoHeight = OrthoWidth / AspectRatio;
    
    // Camera views in XZ plane, so:
    const float ViewHalfWidth = OrthoWidth * 0.5f;   // X extent
    const float ViewHalfHeight = OrthoHeight * 0.5f; // Z extent
    
    // Get current camera position
    FVector CameraPos = GetActorLocation();
    
    // Clamp X (horizontal)
    const float MinX = WorldBounds.Min.X + ViewHalfWidth;
    const float MaxX = WorldBounds.Max.X - ViewHalfWidth;
    CameraPos.X = FMath::Clamp(CameraPos.X, MinX, MaxX);
    
    // Clamp Z (vertical)
    const float MinZ = WorldBounds.Min.Z + ViewHalfHeight;
    const float MaxZ = WorldBounds.Max.Z - ViewHalfHeight;
    CameraPos.Z = FMath::Clamp(CameraPos.Z, MinZ, MaxZ);
    
    SetActorLocation(CameraPos);
}
  1. and then Clamp Zoom to Prevent Over-Zoom:
void ACameraActor::ClampZoom()
{
    if (!GridActor || !GridActor->PlayableBounds) return;
    
    const FBox WorldBounds = GridActor->PlayableBounds->Bounds.GetBox();
    const float AspectRatio = GetCameraComponent()->AspectRatio;
    
    // Calculate maximum OrthoWidth that shows entire grid
    const float GridWidth = WorldBounds.Max.X - WorldBounds.Min.X;
    const float GridHeight = WorldBounds.Max.Z - WorldBounds.Min.Z;
    
    const float MaxOrthoWidthByWidth = GridWidth;
    const float MaxOrthoWidthByHeight = GridHeight * AspectRatio;
    
    const float MaxOrthoWidth = FMath::Min(MaxOrthoWidthByWidth, MaxOrthoWidthByHeight);
    
    OrthoWidth = FMath::Clamp(OrthoWidth, MinZoom, MaxOrthoWidth);
    GetCameraComponent()->OrthoWidth = OrthoWidth;
}

And that’s it! It should give you pixel-perfect camera bounds that respect all transforms!

Let me know if it worked or if you need more help with this!