Building an asset free minimap system in Unreal Engine
Introduction
With the thousands of games available in all corners of the internet there is one very common feature that is often overlooked, Minimaps. This post goes over a very basic system for rendering a minimap based off the players location without the need of any assets.
Inspiration
Building my own game without the funding of a studio or Kickstarter is expensive, assets from Fab and external sources can often be on the pricier side, and for good reason! Exploring Fab really shows the creativity and skill set of artists out there. When creating my current project I decided to create almost everything in C++ and very little with Blueprints, additionally the end goal is to have the games map be procedurally generated, so creating a piece of art the minimap uses while possible would be on the harder side.
The minimap while nothing special provides what is required, a top down view of the players position, the mobs (in my case i call them pests) around them, and the direction they are facing. In the future I would love to add:
- A time of day indicator
- The current weather (for if you're not outside)
- Objective visualisation
- Markers
Setup
To start this is being kept within the HUD component and is permenantly visible.
To start we are going to need some starting components:
cppUPROPERTY() UMinimapComponent* MinimapComponent; UPROPERTY() UTexture* MinimapBorderTexture; UPROPERTY(EditAnywhere, Category = "Minimap|Display") float MinimapSize = 200.f; UPROPERTY(EditAnywhere, Category = "Minimap|Display") float MinimapPadding = 2.f; UPROPERTY(EditAnywhere, Category = "Minimap|Display") bool bShowMinimapBorder = true; UPROPERTY(EditAnywhere, Category = "Minimap|Display") FLinearColor MinimapBorderColor = FLinearColor::White; UPROPERTY() UTexture2D* WhiteTexture;
These properties should be somewhat familiar if you have worked with Unreal Engine 5 before, excluding UMinimapComponent, lets get into that a little bit.
Here is the full MinimapComponent.h file:
cpp#pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "MinimapComponent.generated.h" class USpringArmComponent; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class YOURGAME_API UMinimapComponent : public UActorComponent { GENERATED_BODY() public: UMinimapComponent(); // =========================================================================== // Functions // =========================================================================== virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; UTextureRenderTarget2D* GetRenderTarget() const { return MinimapRenderTarget; } FORCEINLINE void SetZoom(float NewZoom); FORCEINLINE float GetZoom() const { return MinimapZoom; } private: // =========================================================================== // Properties - components // =========================================================================== UPROPERTY() USceneCaptureComponent2D* SceneCaptureComponent; UPROPERTY() USpringArmComponent* MinimapSpringArm; UPROPERTY() UTextureRenderTarget2D* MinimapRenderTarget; // =========================================================================== // Properties - settings // =========================================================================== UPROPERTY(EditAnywhere, Category = "Minimap|Settings") float MinimapHeight = 1000.f; UPROPERTY(EditAnywhere, Category = "Minimap|Settings") int32 MinimapResolution = 256; UPROPERTY(EditAnywhere, Category = "Minimap|Settings") float MinimapZoom = 1.f; protected: virtual void BeginPlay() override; };
You may be wondering why there are fields in both the HUD header and there is a component on its own, so let me explain:
MinimapComponent is designed to have and control the world capture mechanics, it manages rendering the scene component, creating and updating render targets, following the players around and most importantly WHAT gets captured.
The HUD components handles the display, the positioning of the map (in this case top right), how large it should be displayed and the visual styling.
You may be thinking, okay that makes sense but why? In short there are a couple of reasons, the first being that certain parts can be re-used (possibly for a full map), performance optimizations, we can specify specific capture rates and when to redraw parts of the map but most importantly: ownership! The minimap component is attached to the players pawn and follows them around, but the HUD is owned (by default in Unreal) by the player controller.
Functions
Now that the prerequisites are ready, of course we are going to need some functions to handle everything, a few you may have noticed above in the MinimapComponent header file, but there are a couple that are required in the HUD header.
cppvoid DrawMinimap(); void DrawMinimapBorder(const float X, const float Y, const float Width, const float Height); void FindMinimapContent();
Now these methods combined with the methods from the MinimapComponent will all come together to make one glorious mini map, hopefully at least!
Lets go into each method one by one.
Setup of components
cppvoid UMinimapComponent::BeginPlay() { AActor* Owner = GetOwner(); if (!Owner) return; // Set up the spring arm that will capture the minimap. MinimapSpringArm = NewObject<USpringArmComponent>(Owner, TEXT("MinimapSpringArm")); MinimapSpringArm->SetupAttachment(Owner->GetRootComponent()); MinimapSpringArm->TargetArmLength = MinimapHeight; MinimapSpringArm->SetWorldRotation(FRotator(-90.f, 0.f, 0.f)); // Looks straight down MinimapSpringArm->bDoCollisionTest = false; MinimapSpringArm->bUsePawnControlRotation = false; MinimapSpringArm->bInheritPitch = false; MinimapSpringArm->bInheritRoll = false; MinimapSpringArm->bInheritYaw = false; MinimapSpringArm->RegisterComponent(); // Create the capture component. SceneCaptureComponent = NewObject<USceneCaptureComponent2D>(Owner, TEXT("MinimapSceneCapture")); SceneCaptureComponent->SetupAttachment(MinimapSpringArm, USpringArmComponent::SocketName); SceneCaptureComponent->ProjectionType = ECameraProjectionMode::Orthographic; SceneCaptureComponent->OrthoWidth = 2048.f * MinimapZoom; SceneCaptureComponent->CaptureSource = SCS_SceneColorHDR; SceneCaptureComponent->bCaptureEveryFrame = false; // Only when the player moves otherwise it's going to lag to shit. SceneCaptureComponent->bCaptureOnMovement = true; // Set what is shown on the map, in the future we may want some of this stuff. SceneCaptureComponent->ShowFlags.SetFog(false); SceneCaptureComponent->ShowFlags.SetVolumetricFog(false); SceneCaptureComponent->ShowFlags.SetMotionBlur(false); SceneCaptureComponent->ShowFlags.SetBloom(false); SceneCaptureComponent->ShowFlags.SetDynamicShadows(false); SceneCaptureComponent->ShowFlags.SetAtmosphere(false); // Hide the player because it looks weird as fuck otherwise. SceneCaptureComponent->HiddenActors.Add(Owner); // Create the render target, this is the initial one the other is updated later. MinimapRenderTarget = NewObject<UTextureRenderTarget2D>(this); MinimapRenderTarget->InitAutoFormat(MinimapResolution, MinimapResolution); MinimapRenderTarget->ClearColor = FLinearColor::Black; MinimapRenderTarget->TargetGamma = 2.2f; MinimapRenderTarget->bAutoGenerateMips = false; MinimapRenderTarget->UpdateResourceImmediate(true); SceneCaptureComponent->TextureTarget = MinimapRenderTarget; SceneCaptureComponent->RegisterComponent(); }
MinimapComponent tick
cppvoid UMinimapComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (GetOwner() && MinimapSpringArm) { // Update the arm location. const FVector OwnerLocation = GetOwner()->GetActorLocation(); MinimapSpringArm->SetWorldLocation(OwnerLocation); // Rotate the minimap with the player. const float PlayerYaw = GetOwner()->GetActorRotation().Yaw; MinimapSpringArm->SetWorldRotation(FRotator(-90.f, PlayerYaw, 0.f)); } }
Adjusting the zoom
cppvoid UMinimapComponent::SetZoom(float NewZoom) { MinimapZoom = FMath::Clamp(NewZoom, 0.5f, 5.f); if (SceneCaptureComponent) { SceneCaptureComponent->OrthoWidth = 2048.f * MinimapZoom; } }
Drawing the minimap
cppvoid AGameHUD::DrawMinimap() { UTextureRenderTarget2D* MinimapTexture = MinimapComponent->GetRenderTarget(); if (!MinimapTexture) return; // Calculate the screen position, this is the top right corner. float ScreenX = Canvas->ClipX - MinimapSize - MinimapPadding; float ScreenY = MinimapPadding; // Draw the minimap texture. FCanvasTileItem TileItem( FVector2D(ScreenX, ScreenY), MinimapTexture->GetResource(), FVector2D(MinimapSize, MinimapSize), FLinearColor::White ); TileItem.BlendMode = SE_BLEND_Opaque; Canvas->DrawItem(TileItem); // Draw the border, for now there isn't one, but we can specify one later. if (bShowMinimapBorder) { DrawMinimapBorder(ScreenX, ScreenY, MinimapSize, MinimapSize); } // Draw the player indicator in the center. float CenterX = ScreenX + MinimapSize / 2.f; float CenterY = ScreenY + MinimapSize / 2.f; // Handle the player rotation. float PlayerYaw = 0.f; if (const APlayerController* PC = GetOwningPlayerController()) { if (const APawn* PlayerPawn = PC->GetPawn()) { PlayerYaw = FMath::DegreesToRadians(PlayerPawn->GetActorRotation().Yaw); } } // Draw the player as an arrow/triangle TArray<FVector2D> Points; float ArrowSize = 8.f; // Define arrow points relative to origin (pointing up initially) TArray<FVector2D> LocalPoints; LocalPoints.Add(FVector2D(0, -ArrowSize)); // Tip (front) LocalPoints.Add(FVector2D(-ArrowSize * 0.5f, ArrowSize * 0.5f)); // Left wing LocalPoints.Add(FVector2D(ArrowSize * 0.5f, ArrowSize * 0.5f)); // Right wing // Rotate and translate points based on player rotation for (const FVector2D& LocalPoint : LocalPoints) { float RotatedX = LocalPoint.X * FMath::Cos(PlayerYaw) - LocalPoint.Y * FMath::Sin(PlayerYaw); float RotatedY = LocalPoint.X * FMath::Sin(PlayerYaw) + LocalPoint.Y * FMath::Cos(PlayerYaw); Points.Add(FVector2D(CenterX + RotatedX, CenterY + RotatedY)); } if (WhiteTexture) { FCanvasTriangleItem TriangleItem(Points[0], Points[1], Points[2], WhiteTexture->GetResource()); TriangleItem.SetColor(FLinearColor::Blue); Canvas->DrawItem(TriangleItem); } else { // Draw lines if the texture is not available. DrawLine(Points[0].X, Points[0].Y, Points[1].X, Points[1].Y, FLinearColor::Blue); DrawLine(Points[1].X, Points[1].Y, Points[2].X, Points[2].Y, FLinearColor::Blue); DrawLine(Points[2].X, Points[2].Y, Points[0].X, Points[0].Y, FLinearColor::Blue); } }
Minimap border
cppvoid AGameHUD::DrawMinimapBorder(const float X, const float Y, const float Width, const float Height) { DrawLine(X, Y, X + Width, Y, MinimapBorderColor, MinimapPadding); DrawLine(X, Y + Height, X + Width, Y + Height, MinimapBorderColor, MinimapPadding); DrawLine(X, Y, X, Y + Height, MinimapBorderColor, MinimapPadding); DrawLine(X + Width, Y, X + Width, Y + Height, MinimapBorderColor, MinimapPadding); }
Finding the content
cppvoid AGameHUD::FindMinimapContent() { if (APlayerController* PC = GetOwningPlayerController()) { if (APawn* PlayerPawn = PC->GetPawn()) { MinimapComponent = PlayerPawn->FindComponentByClass<UMinimapComponent>(); if (!MinimapComponent) { // Properly create and initialize the component MinimapComponent = NewObject<UMinimapComponent>( PlayerPawn, UMinimapComponent::StaticClass(), TEXT("MinimapComponent") ); if (MinimapComponent) { MinimapComponent->RegisterComponent(); } } } } }
Make sure it draws
cppvoid AGameHUD::DrawHUD() { Super::DrawHUD(); if (Canvas && MinimapComponent) { DrawMinimap(); } }
Closing thoughts
This really is just a rough draft on how it works, for now it works pretty nicely no assets required, not even the arrow! There is much more that can be done with this as mentioned above.
In the future, you may want to experiment with making the minimap interactable, layer in more widgets, or draw additional features like pings and markers. The underlying technique here should generalize to most custom HUD elements in Unreal.
Remember that optimization may become important if your minimap starts to display large numbers of actors or dynamic elements. Consider culling or level of detail techniques for performance.
If you run into specific problems or have ideas for expansion such as rotating the map with the player or overlaying points of interest don't hesitate to prototype them! UE's component system makes this kind of modular HUD development quite maintainable.
Happy mapping!