虚幻引擎中基于纹理的简易小地图实现方案
获取小地图材质
首先在场景中央位置添加SceneCapture2D组件,参考下图进行布置
按照以下参数进行配置
创建一个新的RenderTarget对象作为SceneCapture2D的存储目标,然后提取图像,通过图像处理软件(可选处理,仅为了美观)获取如下图所示的纹理资源
使用此纹理创建材质,记得将材质属性设置为UI类型
在材质编辑器中进行以下操作,设置Zoom(缩放)、WorldDimension(世界尺寸)以及(X,Y)(角色当前位置)参数
后续将通过C++代码传入X、Y值来动态调整小地图的UV坐标,注意由于材质坐标系中向下为Y轴正方向,向右为X轴正方向,这与世界坐标系的XY轴方向相反,因此需要进行取反处理。
若需要设计为圆形小地图,可添加以下操作。
最后,基于该材质创建材质实例(MI_MiniMap)
创建小地图界面组件
新建继承自UserWidget类的C++类MiniMapBase
//内存中小地图元素的数据结构
USTRUCT()
struct FMapElementMemory
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(Transient)
uint32 Identifier;
UPROPERTY(Transient)
class UMiniMapElement* WidgetElement;
FMapElementMemory()
{
Identifier = 0;
WidgetElement = nullptr;
}
};
/**
* 小地图基础界面类
*/
UCLASS()
class MINIMAP_API UMiniMapBase : public UUserWidgetBase
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
protected:
virtual void OnInitialize();
virtual void SetupZoom();
virtual void SetupDimensions();
virtual void UpdateMinimapCenter();
virtual void OnMinimapElementInsert(UObject* InObject);
virtual void OnMinimapElementRemove(UObject* InObject);
void ProcessElementWidgetCreation();
public:
UPROPERTY(meta = (BindWidget))
UImage* MapDisplay;
//世界尺寸
UPROPERTY()
float WorldSize = 5000.0f;
//缩放比例
UPROPERTY()
float ScaleFactor = 0.5f;
protected:
// 当前活动的地图元素
UPROPERTY(Transient)
TMap<uint32,FMapElementMemory> ActiveElements = TMap<uint32,FMapElementMemory>();
// 待创建Widget的元素队列
UPROPERTY(Transient)
TSet<uint32> PendingWidgetCreation = TSet<uint32>();
// 每帧创建的Widget数量限制
UPROPERTY(EditAnywhere, Category = "Config | Widget")
int32 ElementsPerFrame = 2;
// 地图元素容器
UPROPERTY(BlueprintReadOnly , Category = "Widget" , meta = (BindWidgetOptional))
class UCanvasPanel* ElementContainer;
};
基于此类创建Widget蓝图WBP_Minimap,添加Image组件,命名必须与C++中一致,即Map_Display。将其默认值设置为已创建好的MI_MiniMap材质实例。
实现小地图核心逻辑
初始化小地图参数
void UMiniMapBase::SetupZoom()
{
if (UMaterialInstanceDynamic* DynamicMaterial = MapDisplay->GetDynamicMaterial())
{
DynamicMaterial->SetScalarParameterValue(TEXT("Zoom"), ScaleFactor);
}
}
void UMiniMapBase::SetupDimensions()
{
if (UMaterialInstanceDynamic* DynamicMaterial = MapDisplay->GetDynamicMaterial())
{
DynamicMaterial->SetScalarParameterValue(TEXT("WorldDimension"), WorldSize);
}
}
//确保玩家角色始终位于小地图中心
void UMiniMapBase::UpdateMinimapCenter()
{
if(!MapDisplay)
{
return;
}
AActor* PlayerCharacter = GetOwningPlayerPawn();
if (PlayerCharacter)
{
FVector PlayerPosition = PlayerCharacter->GetActorLocation();
if (UMaterialInstanceDynamic* DynamicMaterial = MapDisplay->GetDynamicMaterial())
{
float XCoord = PlayerPosition.X;
float YCoord = PlayerPosition.Y;
DynamicMaterial->SetScalarParameterValue(TEXT("WorldX"), XCoord);
DynamicMaterial->SetScalarParameterValue(TEXT("WorldY"), YCoord);
}
}
}
实现玩家图标组件
创建MinimapComponent类继承自ActorComponent,这样做是为了降低Character与小地图之间的耦合度,将此组件附加到玩家角色上,这样小地图系统就可以从组件中获取玩家属性
public:
FTransform GetWorldMapTransform();
FRotator GetCameraRotation();
virtual FName GetMapElementIdentifier();
protected:
//用于数据加载
UPROPERTY(EditAnywhere, Category = "Config")
FName ElementIdentifier = FName();
为了便于管理小地图系统,添加UMiniMapWorldSubsystem类继承WorldSubsystem,虽然仅实现玩家图标功能并不需要如此复杂的结构,但这样设计是为了后续功能扩展做准备
//地图组件内存数据结构
USTRUCT()
struct FElementDataStruct
{
GENERATED_USTRUCT_BODY()
public:
UObject* GetData()
{
return IsValid(ObjectRef) ? ObjectRef : nullptr ;
}
public:
UPROPERTY(Transient)
UObject* ObjectRef;
};
public:
//地图组件添加通知
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMinimapElementInsert, UObject*)
FOnMinimapElementInsert MinimapElementInsertDelegate;
//地图组件移除通知
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMinimapElementRemove, UObject*)
FOnMinimapElementRemove MinimapElementRemoveDelegate;
private:
UPROPERTY(Transient)
TMap<uint32 , FElementDataStruct> ElementMemoryList;
//单例模式获取
UMiniMapWorldSubsystem* UMiniMapWorldSubsystem::Get(UObject* WorldContextObject)
{
if(UWorld* World = WorldContextObject?WorldContextObject->GetWorld():nullptr)
{
return World->GetSubsystem<UMiniMapWorldSubsystem>();
}
return nullptr;
}
void UMiniMapWorldSubsystem::OnInsertElement(UObject* InObj)
{
if(IsValid(InObj))
{
if(!IsElementInMemory(InObj))
{
FElementDataStruct NewData;
NewData.ObjectRef = InObj;
ElementMemoryList.Add(InObj->GetUniqueID(),NewData);
MinimapElementInsertDelegate.Broadcast(InObj);
}
}
}
void UMiniMapWorldSubsystem::OnRemoveElement(UObject* InObj)
{
if(IsValid(InObj))
{
if(IsElementInMemory(InObj))
{
ElementMemoryList.Remove(InObj->GetUniqueID());
MinimapElementRemoveDelegate.Broadcast(InObj);
}
}
}
bool UMiniMapWorldSubsystem::IsElementInMemory(UObject* InObj)
{
if(InObj && ElementMemoryList.Contains(InObj->GetUniqueID()))
{
return true;
}
return false;
}
UObject* UMiniMapWorldSubsystem::GetObjectFromMemory(uint32 InId)
{
if(ElementMemoryList.Contains(InId) && ElementMemoryList[InId].GetData())
{
return ElementMemoryList[InId].GetData();
}
return nullptr;
}
别忘了在UMiniMapBase上绑定委托 ----2025/3/11更新:这里可能存在时序问题,因为MiniMapComponent可能在NativeConstruct之前调用OnInsertElement方法,解决方案是添加一个延迟调用
GetWorld()->GetTimerManager().SetTimer(DelayHandle, this, &UMiniMapComponent::InsertElement, 0.2f, false);
void UMiniMapBase::NativeConstruct()
{
Super::NativeConstruct();
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
if(!WorldSubsystem->MinimapElementInsertDelegate.IsBoundToObject(this))
{
WorldSubsystem->MinimapElementInsertDelegate.AddUObject(this,&UMiniMapBase::OnMinimapElementInsert);
}
if(!WorldSubsystem->MinimapElementRemoveDelegate.IsBoundToObject(this))
{
WorldSubsystem->MinimapElementRemoveDelegate.AddUObject(this,&UMiniMapBase::OnMinimapElementRemove);
}
}
}
void UMiniMapBase::OnMinimapElementInsert(UObject* InObject)
{
if(InObject)
{
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
PendingWidgetCreation.Add(InObject->GetUniqueID());
}
}
}
void UMiniMapBase::OnMinimapElementRemove(UObject* InObject)
{
if(InObject)
{
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
PendingWidgetCreation.Remove(InObject->GetUniqueID());
}
}
}
加载玩家图标资源
接下来将玩家图标资源加载到系统中,这里采用一种较为正式的方式,通过DataTable中读取FSoftObjectPath路径 首先,创建一个DataTable专用的结构体
//必须继承FTableRowBase才能用于DataTable
USTRUCT(BlueprintType)
struct FMinimapDataRow : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName CategoryName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (MetaClass = "UserWidget"))
FSoftClassPath ViewWidgetClass;
};
然后,创建一个全局数据管理类,基于此类创建一个蓝图实例
UCLASS(Blueprintable,Config=Engine)
class MINIMAP_API UMinimapDataManager : public UObject
{
GENERATED_BODY()
public:
UMinimapDataManager();
virtual ~UMinimapDataManager();
static UMinimapDataManager& Get();
const UDataTable* GetDataTable() const;
template<class T>
const T* GetDataRow(const UDataTable* DataTable ,const FName& RowName) const
{
if(DataTable)
{
const T* RowData = DataTable->FindRow<T>(RowName, ContextString);
if(RowData)
{
return RowData;
}
else
{
UE_LOG(LogTemp, Warning, TEXT("未找到行数据: %s"), *RowName.ToString());
return nullptr;
}
}
UE_LOG(LogTemp, Warning, TEXT("DataTable为空"));
return nullptr;
}
public:
//可在蓝图中设置要加载的DataTable
UPROPERTY(EditDefaultsOnly, Category = "Data Table", meta=(DisplayName="数据表资源"))
FSoftObjectPath DataTableReference;
protected:
FString ContextString = TEXT("数据表上下文");
};
const UDataTable* UMinimapDataManager::GetDataTable() const
{;
if(DataTableReference.IsValid())
{
UDataTable* DataTable = Cast<UDataTable>(DataTableReference.ResolveObject());
if(DataTable)
{
return DataTable;
}
else
{
DataTable = Cast<UDataTable>(DataTableReference.TryLoad());
return DataTable;
}
}
UE_LOG(LogTemp,Warning,TEXT("数据表资源不存在"));
return nullptr;
}
//同样使用单例模式获取
UMinimapDataManager& UMinimapDataManager::Get()
{
#if WITH_EDITOR
if(GEngine->GameSingleton)
{
return *static_cast<UMinimapDataManager*>(GEngine->GameSingleton);
}
else
{
//这里放置生成的蓝图的路径,必须包含_C后缀
FString FixedGlobalsClassName = TEXT("Blueprint'/Game/MiniMap/Data/BP_MiniDataManager.BP_MiniDataManager_C'");
UClass *GlobalsClass = LoadClass<UObject>(nullptr, *FixedGlobalsClassName);
UMinimapDataManager* DataManagerObj = NewObject<UMinimapDataManager>(GEngine, GlobalsClass);
GEngine->GameSingleton = DataManagerObj;
return *DataManagerObj;
}
#else
UMinimapDataManager* Singleton = NewObject<UMinimapDataManager>();
return *Singleton;
#endif
}
初始化图标组件
图标资源加载完成后需要初始化相关数据 首先,与小地图类似创建一个Widget类,命名为MinimapElement
class UMiniMapBase;
class UMiniMapComponent;
UCLASS()
class MINIMAP_API UMiniMapElement : public UUserWidgetBase
{
GENERATED_BODY()
public:
virtual void InitializeData(UObject* InObj, UMiniMapBase* InOwnerMap);
virtual void RefreshData(UObject* ViewComponent,UObject* ViewObject);
virtual void RefreshLayerOrder();
virtual void RefreshRotation(UObject* ViewComponent,UObject* ViewObject);
bool CalculatePosition(UMiniMapComponent* ViewComponent,UMiniMapComponent* TargetComponent,float& OutX,float& OutY);
FVector2D ConstrainElementPosition(UMiniMapComponent* ViewComponent,UMiniMapComponent* TargetComponent,float InX,float InY);
protected:
UPROPERTY(Transient)
UObject* OwnerMapComponent = nullptr;
UPROPERTY(Transient)
UMiniMapBase* OwnerMap = nullptr;
UPROPERTY(EditDefaultsOnly,Category="Config")
int32 ElementZOrder = 0;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "MiniMapElement.h"
#include "MiniMapBase.h"
#include "Components/CanvasPanelSlot.h"
#include "Kismet/KismetMathLibrary.h"
void UMiniMapElement::InitializeData(UObject* InObj, UMiniMapBase* InOwnerMap)
{
if(UMiniMapComponent* MapComp = Cast<UMiniMapComponent>(InObj))
{
OwnerMapComponent = MapComp;
}
if(UMiniMapBase* Map = Cast<UMiniMapBase>(InOwnerMap))
{
OwnerMap = Map;
}
}
bool UMiniMapElement::CalculatePosition(UMiniMapComponent* ViewComponent,UMiniMapComponent* TargetComponent, float& OutX, float& OutY)
{
if(!OwnerMap)
{
OutX = 0.f;
OutY = 0.f;
return false;
}
FVector2D RotatedWorldPosition =FVector2D::ZeroVector;
const float Scale = OwnerMap->ScaleFactor;
const FVector2D ViewLocation = FVector2D(ViewComponent->GetWorldMapTransform().GetLocation());
const FVector2D TargetLocation = FVector2D(ViewComponent->GetWorldMapTransform().GetLocation());
const FVector2D TargetToView = TargetLocation - ViewLocation;
RotatedWorldPosition = UKismetMathLibrary::GetRotated2D(TargetToView,ViewComponent->GetWorldMapTransform().Rotator().Yaw-90.f);
const FVector2D DisplaySize = OwnerMap->MapDisplay->GetDesiredSize();
const float SizeScale = DisplaySize.X / 256.f;
const FVector2D NewElementPosition = ((RotatedWorldPosition*SizeScale))/Scale;
OutX = NewElementPosition.X;
OutY = NewElementPosition.Y;
return true;
}
//玩家图标直接显示在地图中心
FVector2D UMiniMapElement::ConstrainElementPosition(UMiniMapComponent* ViewComponent, UMiniMapComponent* TargetComponent, float InX, float InY)
{
if(!OwnerMap)
{
return FVector2D::ZeroVector;
}
const FVector2D MinimapCenter = OwnerMap->MapDisplay->GetDesiredSize() / 2.f;
return FVector2D(InX,InY) + MinimapCenter;
}
//设置较高的ZOrder确保图标不被遮挡
void UMiniMapElement::RefreshLayerOrder()
{
Super::RefreshLayerOrder();
ElementZOrder = 5;
if(OwnerMap)
{
if(UCanvasPanelSlot* PanelSlot = Cast<UCanvasPanelSlot>(Slot))
{
PanelSlot->SetZOrder(ElementZOrder);
}
}
}
void UMiniMapElement::RefreshRotation(UObject* ViewComponent,UObject* ViewObject)
{
if(UMiniMapComponent* CharacterComponent = Cast<UMiniMapComponent>(ViewComponent))
{
FRotator NewRotation = CharacterComponent->GetCameraRotation();
if(PlayerDirectionIcon)
{
PlayerDirectionIcon->SetRenderTransformAngle(NewRotation.Yaw);
}
}
}
创建图标组件
好了,所有准备工作都已完成,现在可以在小地图上创建图标组件了
void UMiniMapBase::ProcessElementWidgetCreation()
{
//限制每帧加载数量,优化性能
int32 CurrentIndex = 0;
int32 EndIndex = CurrentIndex + ElementsPerFrame;
if(EndIndex > PendingWidgetCreation.Num() - 1)
{
EndIndex = PendingWidgetCreation.Num() - 1;
}
for(TSet<uint32>::TIterator It = PendingWidgetCreation.CreateIterator(); It; ++It)
{
if(CurrentIndex > EndIndex)
{
break;
}
CurrentIndex++;
if(!It.ElementIterator)
{
continue;
}
UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this);
if(WorldSubsystem)
{
UMiniMapComponent* Component = Cast<UMiniMapComponent>(WorldSubsystem->GetObjectFromMemory(It.ElementIterator->Value));
FName ElementName;
FSoftClassPath ElementClassPath;
if(Component)
{
ElementName = Component->GetMapElementIdentifier();
if(ElementName.IsValid())
{
const UDataTable* DataTable = UMinimapDataManager::Get().GetDataTable();
auto DataTableRow = UMinimapDataManager::Get().GetDataRow<FMinimapDataRow>(DataTable,ElementName);
if(DataTableRow)
{
ElementClassPath = DataTableRow->ViewWidgetClass;
}
}
if(ElementClassPath.IsValid())
{
if(UClass* ElementClass = ElementClassPath.ResolveClass())
{
const int32 UniqueID = Component->GetUniqueID();
if(!ActiveElements.Contains(UniqueID))
{
if(UMiniMapElement* Widget = CreateWidget<UMiniMapElement>(GetWorld(),ElementClass))
{
if(ElementContainer)
{
if(UCanvasPanelSlot* PanelSlot = ElementContainer->AddChildToCanvas(Widget))
{
FElementDataStruct Data;
Data.Identifier = UniqueID;
Data.ObjectRef = Widget;
ActiveElements.Add(UniqueID,Data);
if(It.ElementIterator->Value == UniqueID)
{
It.RemoveCurrent();
}
Widget->SetVisibility(ESlateVisibility::Collapsed);
Widget->InitializeData(Component,this);
}
}
}
}
else
{
if(It.ElementIterator->Value == UniqueID)
{
It.RemoveCurrent();
}
}
}
else
{
ElementClass=ElementClassPath.TryLoadClass<UClass>();
}
}
}
}
更新图标位置
此时创建的图标组件是静态的,需要每帧更新以实现动态效果
//为后续功能扩展预留的接口
void UMiniMapBase::UpdateElementSet(const TSet<UObject*>& InObjectSet, const AActor* ViewActor, float InDeltaTime)
{
if(!ViewActor)
{
return;
}
for(const UObject* Obj:InObjectSet)
{
if(ActiveElements.Contains(Obj->GetUniqueID()))
{
if(UMiniMapComponent* ViewComponent =ViewActor->FindComponentByClass<UMiniMapComponent>())
{
UpdateElement(ViewComponent,const_cast<UObject*>(Obj),InDeltaTime);
}
}
}
}
void UMiniMapBase::UpdateElement(UMiniMapComponent* ViewComponent,UObject* Obj,float InDeltaTime)
{
if(!Obj)
{
return;
}
const FElementDataStruct& ElementData = ActiveElements.FindRef(Obj->GetUniqueID());
if(UMiniMapElement* Widget = ElementData.WidgetElement)
{
UMiniMapComponent* ViewObject = Cast<UMiniMapComponent>(Obj);
if(ViewObject)
{
float XPosition = 0.f;
float YPosition = 0.f;
if(Widget->CalculatePosition(ViewComponent,ViewObject,XPosition,YPosition))
{
const FVector2D FinalPosition = Widget->ConstrainElementPosition(ViewComponent,ViewObject,XPosition,YPosition);
Widget->SetRenderTranslation(FinalPosition);
Widget->SetVisibility(ESlateVisibility::Visible);
Widget->RefreshData(ViewComponent,ViewObject);
}
}
}
}
完成
恭喜你现在已经拥有了一个基础功能完备的小地图系统,将其添加到项目中测试一下吧!

参考链接
YouTube小地图教程 https://youtu.be/Z6qzaT4ZOh0 【[中文直播] 第21期 | UE4数据驱动开发 | Epic 大钊】 https://www.bilibili.com/video/BV1dk4y1r752