Introduction
While navigating the turbulent waters of game development, you might’ve had to implement a system that showcases an object (like an unlockable or a status screen with interactable character). Now, there are multiple ways to achieve this feature like having a predefined camera position in a level or if you have multiple screens that are showcasing different types of objects, you might’ve hardcoded camera positions in a file that’s read from on demand. All of these are viable solutions, but I’m going to demo a way that’s a quite flexible by moving camera automatically and frame object to an area on viewport dynamically defined!!!
There’s also a video available here if that’s your preferred way to learn. Now let’s get into it!
Prelims
Let’s first setup a scene with an actor to frame. I have a very simple scene with a floor, podium, a sphere that should be framed to a portion of the screen and a camera that we are going to code to move around to meet our framing requirements. This is how it looks…
Next, we are going to setup a widget with a dedicated area to which the sphere should be framed to. To make the demo simple, this widget also has 2 buttons for gathering widget settings and sending them to C++ code and another button to initialize the “Frame” C++ code. Here’s my widget setup. I will be reading the dimensions of “FrameOverlay” and sending them over to C++ to move the camera accordingly.
The image inside the “FrameOverlay” panel is only used as visual confirmation for our logic. Serves no real purpose to the actual logic. We then spawn this widget inside level blueprint.
Setting Up!
Now that we have our scene and widget setup, we need a way to send information from widget to C++ and modify our active camera’s parameters from there. A GameInstancSubsystem fits perfectly for all our needs. So, I create a new C++ class inheriting from UGameInstanceSubsystem with following contents.
///MyGameInstanceSubsystem.h
UCLASS()
class FRAMEACTOR_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void SetCameraActor(ACameraActor* Camera);
UFUNCTION(BlueprintCallable)
void SetFrameActor(AActor* Actor);
UFUNCTION(BlueprintCallable)
void GatherSettings(const FVector2D& Offset, const FVector2D& Size);
UFUNCTION(BlueprintCallable)
void Frame();
ACameraActor* CameraActor;
AActor* ActorToFrame;
FVector2D FrameOffset;
FVector2D FrameSize;
};
///MyGameInstanceSubsystem.cpp
void UMyGameInstanceSubsystem::SetCameraActor(ACameraActor* Camera)
{
CameraActor = Camera;
}
void UMyGameInstanceSubsystem::SetFrameActor(AActor* Actor)
{
ActorToFrame = Actor;
}
void UMyGameInstanceSubsystem::GatherSettings(const FVector2D& Offset, const FVector2D& Size)
{
FrameOffset = Offset;
FrameSize = Size;
}
Core Logic!
To do anything, we first need to register a camera and the object to frame. They should be valid entities and this can be setup inside you Level blueprint (using “SetCameraActor” and “SetFrameActor”) immediately after adding widget to viewport.
After registering camera and actor, we need to register the area of the screen to frame our actor to. This is done inside our widget by pressing “Gather Settings” button. I’ve noticed that it’s not ideal to gather these settings during widget construction/initialization events as the geometry might not have been constructed yet and might give you invalid results. Therefore, gathering settings on a button press would be ideal for this demo!
Now let’s start coding the core logic to make this work. This can be broken down into multiple steps:
- We first get the actor bounds and retrieve the Screen Space coordinates of Top-Left and Bottom-Right positions. These are represented in code as “TopRightSP” and “BottomLeftSP”.
- These coordinates form a 2D rectangle. We then use the dimensions of this rectangle to find the ratio of width and height with respect to our “FrameSize” (dimensions of the overlay panel). The MAX of these ratios is used as our FOV multiplier. All our calculations assume that Camera and Actor doesn’t move further apart or come closer. This lets us scale FOV and and move the camera in the plane perpendicular to camera’s forward vector to frame the actor. If camera moves closer or away from actor, the bounding box would shrink/expand.
- We use the multiplier to modify the FOV of camera and force-update it.
- Next, we need to get a World-Space direction vector of the center of preview area.
- Finally, we start from the center of bounding box and move in a direction defined by the negative of vector and distance from camera. This will be the final position of our camera.
void UMyGameInstanceSubsystem::Frame()
{
APlayerController* Controller = GetWorld()->GetFirstPlayerController();
FVector Origin;
FVector Extents;
ActorToFrame->GetActorBounds(true, Origin, Extents, false);
DrawDebugBox(GetWorld(), Origin, Extents, FColor::Red, false, 10.0f);
float maxHorizontal = FMath::Max(Extents.X, Extents.Z);
float maxVertical = Extents.Y;
FVector TopRight = FVector(Origin.X + maxHorizontal, Origin.Y, Origin.Z + maxVertical);
FVector BottomLeft = FVector(Origin.X - maxHorizontal, Origin.Y, Origin.Z - maxVertical);
FVector2D TopRightSP;
FVector2D BottomLeftSP;
Controller->ProjectWorldLocationToScreen(TopRight, TopRightSP);
Controller->ProjectWorldLocationToScreen(BottomLeft, BottomLeftSP);
float Width = FMath::Abs(TopRightSP.X - BottomLeftSP.X);
float Height = FMath::Abs(TopRightSP.Y - BottomLeftSP.Y);
float FOVScaleX = Width / FrameSize.X;
float FOVScaleY = Height / FrameSize.Y;
float TargetScale = FMath::Max(FOVScaleX, FOVScaleY);
float FOV = CameraActor->GetCameraComponent()->FieldOfView;
CameraActor->GetCameraComponent()->SetFieldOfView(FOV * TargetScale);
Controller->PlayerCameraManager->UpdateCamera(0.f);
float FrameCenterX = FrameOffset.X + FrameSize.X * 0.5f;
float FrameCenterY = FrameOffset.Y + FrameSize.Y * 0.5f;
FVector WorldLocation;
FVector WorldDirection;
Controller->DeprojectScreenPositionToWorld(FrameCenterX, FrameCenterY, WorldLocation, WorldDirection);
float distance = FVector::Dist(Origin, CameraActor->GetActorLocation());
FVector TargetLocation = Origin - WorldDirection * distance;
CameraActor->SetActorLocation(TargetLocation);
}
Conclusion!
With that you should have a pretty good system that can frame actors to a dynamically defined area on screen. It’s not perfect though! If the camera is way out of view of the actor, the results might be skewed. A way to fix it would be to run this code in a loop 2 or 3 times or have additional checks that verifies the area of the resulting bounding box in the preview area and redo the logic.
If you learnt something or have suggestions/improvements, please do drop a comment ๐
2 responses to “Frame Object to specified area in Viewport(Unreal)”
Thanks for sharing your thoughts. I truly appreciate your efforts and I will be waiting for
your further write ups thanks once again.
You are my first commenter!! It made my day. Thanks so much ๐