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:
- 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.
It takes a long time to describe each item, so I'll show you the final result immediately.
This is the result video.
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.
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.
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.
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.
You can customize top view material, so installation will be easier.
Comments
Post a Comment