Mini map : Match 2D images to the 3D world

In this post, Lets's talk about how to match 2D images to the 3D world with Unreal Engine 4.

A lot of things are needed to create a AAA game style mini-map. Here are a few typical requirements:
 - Priority for indoor and outdoor support. 
 - Indoor mini map should working with various house shape.
 - The top direction of the 2D image may not be the north direction.
 - Easy to setup.



If you have a wide world and a lot of mini-maps, the easiness of installation is most important.In such cases, you should be able to install and verify immediately in the UE4 editor.

You must use C++ class this time to make it working in the editor. This is because the 'PostEditChangeProperty' function is required and editor's viewport action does not run 'Blueprint' at all.


Step 1.

It takes a long time to describe each item, so I'll show you the final result immediately.
This is the result video.


Step 2.

This is the code of MiniMapActor.

MiniMapActor.h
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/BoxComponent.h"
#include "MiniMapActor.generated.h"

class UMaterial;
class UBoxComponent;

UCLASS()
class OFFICIALUE4_API AMiniMapActor : public AActor
{
 GENERATED_BODY()
 
public: 
 // Sets default values for this actor's properties
 AMiniMapActor();

protected:
 // Called when the game starts or when spawned
 virtual void BeginPlay() override;

public: 
 // Called every frame
 virtual void Tick(float DeltaTime) override;

 

 virtual void PostEditChangeProperty( FPropertyChangedEvent& PropertyChangedEvent ) override;

 /* Return -0.5~0.5, Center of MappingBox is 0. If OverrideMiniMap is not none, return OverrideMiniMap's result. */
 UFUNCTION( BlueprintCallable, Category = "Game" )
 FVector GetTargetLocationOnMiniMap( FVector TargetLocation, float CustomRotation );

 FVector GetTargetLocationOnMiniMap_Internal( FVector TargetLocation, float CustomRotation );

 /* Return minimap texture, If OverrideMiniMap is not none, return OverrideMiniMap's result. */
 UFUNCTION( BlueprintCallable, Category = "Game" )
 UTexture2D* GetMapImage();

 UTexture2D* GetMapImage_Internal();

 UPROPERTY()
 class UBoxComponent* MappingBox;

 UPROPERTY( EditInstanceOnly, Category = "UI Information" )
 UTexture2D *MapImage;

 /** You can change the north direction. (Generally, Value is 0,90,180,270) */
 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "UI Information", meta = ( ClampMin = "0.0", UIMin = "0.0", ClampMax = "360.0", UIMax = "360.0" ) )
 float RotationAngleDeg;

 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "UI Information" )
 FText ZoneName;

 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "UI Information" )
 FText ZoneDescription;


 /** Turn off MappingBox's collision. You can add collision boxes manually for complex shape area. */
 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "Collision" )
 bool bCustomCollision;

 /** If target pawn collided with multiple MiniMapZoneActors, consider this value. */
 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "Collision" )
 int32 Priority;

 /** Override collision and texture. Useful for named area without additional image setting.*/
 UPROPERTY( EditanyWhere, BlueprintReadOnly, Category = "Collision", AdvancedDisplay )
 class AMiniMapActor *OverrideMiniMap;


#if WITH_EDITOR
 UPROPERTY( Transient )
 class UStaticMeshComponent* TopViewMesh;

 UPROPERTY( Transient )
 UMaterialInstanceDynamic *TopViewMaterialInst;

 /** Make TopViewMesh visible. Transient. */
 UPROPERTY( EditInstanceOnly, Category = "Top View Setup", Transient )
 bool bEnableSetupViewState;

 /** Use custom a material if you don't like default(BlinkingCaret) material. */
 UPROPERTY( EditInstanceOnly, Category = "Top View Setup", Transient, NoClear, AdvancedDisplay )
 UMaterial* TopViewMaterial;

 /** If you can't see the minimap from top view, check this. */
 UPROPERTY( EditInstanceOnly, Category = "Top View Setup" )
 bool bRender10kHigher;

 void UpdateCollision();
#endif // WITH_EDITOR


 
};

MiniMapActor.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Fill out your copyright notice in the Description page of Project Settings.

#include "MiniMapActor.h"


// Sets default values
AMiniMapActor::AMiniMapActor()
{
  // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
 PrimaryActorTick.bCanEverTick = true;


 MappingBox = CreateDefaultSubobject<UBoxComponent>(TEXT("MappingBox"));
 MappingBox->SetMobility(EComponentMobility::Static);

#if WITH_EDITOR
 TopViewMesh = CreateEditorOnlyDefaultSubobject<UStaticMeshComponent>(TEXT("TopViewMesh"));
 if (TopViewMesh)
 {
  TopViewMesh->SetVisibility(false);
  TopViewMesh->SetupAttachment(MappingBox);
  TopViewMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
  TopViewMesh->SetCastShadow(false);
  TopViewMesh->SetMobility(EComponentMobility::Static);

  UStaticMesh* FloorMesh = LoadObject<UStaticMesh>(NULL, TEXT("/Engine/BasicShapes/Cube.Cube"));
  if (FloorMesh)
  {
   TopViewMesh->SetStaticMesh(FloorMesh);
   TopViewMaterial = LoadObject<UMaterial>(NULL, TEXT("/Engine/EngineMaterials/BlinkingCaret.BlinkingCaret"));
  }
 }

 UpdateCollision();
#endif // WITH_EDITOR

 Priority = 50;
}

// Called when the game starts or when spawned
void AMiniMapActor::BeginPlay()
{
 Super::BeginPlay();
 
}

// Called every frame
void AMiniMapActor::Tick(float DeltaTime)
{
 Super::Tick(DeltaTime);

}

FVector AMiniMapActor::GetTargetLocationOnMiniMap(FVector TargetLocation, float CustomRotation)
{
 if (OverrideMiniMap)
 {
  return OverrideMiniMap->GetTargetLocationOnMiniMap_Internal(TargetLocation, CustomRotation);
 }

 return GetTargetLocationOnMiniMap_Internal(TargetLocation, CustomRotation);
}

FVector AMiniMapActor::GetTargetLocationOnMiniMap_Internal(FVector TargetLocation, float CustomRotation)
{
 FVector ScaledBoxExtent = MappingBox->GetScaledBoxExtent();
 if (ScaledBoxExtent.SizeSquared() <= 0.0)
 {
  return FVector(0, 0, 0);
 }

 FVector TempLocation = (TargetLocation - GetActorLocation()).RotateAngleAxis(RotationAngleDeg + CustomRotation, FVector(0, 0, 1));
 return TempLocation / (ScaledBoxExtent * 2);
}

UTexture2D* AMiniMapActor::GetMapImage()
{
 if (OverrideMiniMap)
 {
  return OverrideMiniMap->GetMapImage_Internal();
 }

 return GetMapImage_Internal();
}

UTexture2D* AMiniMapActor::GetMapImage_Internal()
{
 return MapImage;
}

void AMiniMapActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
 Super::PostEditChangeProperty(PropertyChangedEvent);

#if WITH_EDITOR

 if (TopViewMesh == NULL)
 {
  return;
 }

 UProperty* PropertyThatChanged = PropertyChangedEvent.Property;
 if (PropertyThatChanged)
 {
  if (PropertyThatChanged->GetFName() == FName(TEXT("bCustomCollision")))
  {
   UpdateCollision();
  }

  if (PropertyThatChanged->GetFName() == FName(TEXT("RotationAngleDeg")))
  {
   TopViewMesh->SetWorldRotation(FRotator(0, -RotationAngleDeg, 0));
  }

  if (bEnableSetupViewState)
  {
   if (TopViewMaterialInst == NULL ||
    PropertyThatChanged->GetFName() == FName(TEXT("TopViewMaterial")))
   {
    TopViewMaterialInst = UMaterialInstanceDynamic::Create(TopViewMaterial, NULL);
   }

   if (TopViewMaterialInst != NULL ||
    PropertyThatChanged->GetFName() == FName(TEXT("MapImage")))
   {
    TopViewMaterialInst->SetTextureParameterValue(FName(TEXT("SourceTexture")), MapImage);
   }

   TopViewMesh->SetMaterial(0, TopViewMaterialInst);
   TopViewMesh->SetWorldLocation(MappingBox->GetComponentLocation() + FVector(0, 0, bRender10kHigher ? 10000 : 10));
   TopViewMesh->SetWorldScale3D(FVector(0.64, 0.64, 0.0001) * MappingBox->RelativeScale3D);
  }

  if (PropertyThatChanged->GetFName() == FName(TEXT("bEnableSetupViewState")))
  {
   TopViewMesh->SetVisibility(bEnableSetupViewState);
  }
 }
#endif // WITH_EDITOR
}

#if WITH_EDITOR
void AMiniMapActor::UpdateCollision()
{
 if (bCustomCollision)
 {
  MappingBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
 }
 else
 {
  MappingBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
  MappingBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap);
  MappingBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_Vehicle, ECollisionResponse::ECR_Overlap);
  MappingBox->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldDynamic, ECollisionResponse::ECR_Overlap);
  MappingBox->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
 }
}
#endif // WITH_EDITOR

Step 2.

I will not implement a 'UI widget' in this post. Instead, you can do the following:

You can customize top view material, so installation will be easier.


Code highlight by http://hilite.me/

Comments

Popular posts from this blog

Liquid material in a bottle

MatCap material

How to Make Circular Progress Bar(or Rounded Rectangle)