UE中实现简单的小地图(Texture实现)
获取小地图材质
将SceneCapture2D组件添加到场景中心位置,如下图所示摆放

设置参数如下

新建一个RenderTarget对象作为SceneCapture2D的保存对象,然后把图像提取出来,通过PS(不P也没影响单纯为了好看)获取得到下图所示Texture

使用此Texture创建Material,记得把属性改成UI

在材质窗口,进行如下操作,Zoom(放缩),WorldDimension(尺度),(X,Y)(角色当前坐标)

后续通过C++传入X,Y的值对小地图UV坐标进行更改,注意由于材质向下为Y轴正方向,向右为X轴正方向,与世界的坐标XY轴相反,所以这里取反。
如果想设计成圆的可以添加以下操作。

最后,以该材质创建出Material Instance(MI_MiniMap)
创建小地图Widget
新建继承于UserWidget类的C++类MiniMapBase
//内存中小地图Item的数据结构
USTRUCT()
struct FMapItemMemory
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY(Transient)
uint32 Id;
UPROPERTY(Transient)
class UMiniMapItem* Widget;
FMapItemMemory()
{
Id = 0;
Widget = 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 InitZoom();
virtual void InitDimensions();
virtual void UpdateMiniMapCenter();
virtual void OnMiniMapItemInsert(UObject* InObject);
virtual void OnMiniMapItemRemove(UObject* InObject);
void OnHandleItemWidgetCreate();
public:
UPROPERTY(meta = (BindWidget))
UImage* Image_Map;
//尺寸
UPROPERTY()
float Dimension = 5000.0f;
//缩放
UPROPERTY()
float Zoom = 0.5f;
protected:
// 可运行的Item
UPROPERTY(Transient)
TMap<uint32,FMapItemMemory> MapItemList = TMap<uint32,FMapItemMemory>();
// 待创建Widget的Item
UPROPERTY(Transient)
TSet<uint32> MapItemCreateWidgetList = TSet<uint32>();
// 每帧创建Widget数量
UPROPERTY(EditAnywhere, Category = "Config | Widget")
int32 CreateWidgetPerFrame = 2;
// 地图元素容器
UPROPERTY(BlueprintReadOnly , Category = "Widget" , meta = (BindWidgetOptional))
class UCanvasPanel* Container;
};
基于此类创建Widget蓝图WBP_MiniMap,添加Image,命名必须与C++中相同,即Image_Map。将其默认值设置为已创建好的MI_MiniMap。
实现小地图逻辑
初始化小地图
void UMiniMapBase::InitZoom()
{
if (UMaterialInstanceDynamic* DynamicMaterial = Image_Map->GetDynamicMaterial())
{
DynamicMaterial->SetScalarParameterValue(TEXT("Zoom"), Zoom);
}
}
void UMiniMapBase::InitDimensions()
{
if (UMaterialInstanceDynamic* DynamicMaterial = Image_Map->GetDynamicMaterial())
{
DynamicMaterial->SetScalarParameterValue(TEXT("WorldDimension"), Dimension);
}
}
//这里确保角色一直处于小地图中心
void UMiniMapBase::UpdateMiniMapCenter()
{
if(!Image_Map)
{
return;
}
AActor* ViewPlayer = GetOwningPlayerPawn();
if (ViewPlayer)
{
FVector PlayerLoc = ViewPlayer->GetActorLocation();
if (UMaterialInstanceDynamic* DynamicMaterial = Image_Map->GetDynamicMaterial())
{
float X = PlayerLoc.X;
float Y = PlayerLoc.Y;
DynamicMaterial->SetScalarParameterValue(TEXT("WorldX"), X);
DynamicMaterial->SetScalarParameterValue(TEXT("WorldY"), Y);
}
}
}
实现玩家图标
创建MiniMapComponent类继承自ActorComponent,这样做的原因是我不希望Character和小地图耦合太强,将这个Component挂在玩家身上,这样小地图就可以从Component上面取玩家的属性
public:
FTransform GetSceneMapTransform();
FRotator GetViewRotation();
virtual FName GetSceneMapItemName();
protected:
//这是为了数据加载
UPROPERTY(EditAnywhere, Category = "Config")
FName SceneMapItemName = FName();
为了方便管理小地图,添加UMiniMapWorldSubsystem类继承WorldSubsystem,单纯玩家图标其实不用这么复杂,这里为以后的拓展做准备(挖坑)
//地图组件内存结构
USTRUCT()
struct FMemoryDataStruct
{
GENERATED_USTRUCT_BODY()
public:
UObject* GetData()
{
return IsValid(Obj) ? Obj : nullptr ;
}
public:
UPROPERTY(Transient)
UObject* Obj;
};
public:
//地图组件预加入
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMiniMapInsertItem, UObject*)
FOnMiniMapInsertItem MiniMapInsertItemDelegate;
//地图组件预移除
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMiniMapRemoveItem, UObject*)
FOnMiniMapRemoveItem MiniMapRemoveItemDelegate;
private:
UPROPERTY(Transient)
TMap<uint32 , FMemoryDataStruct> MemoryList;
//单例模式获取
UMiniMapWorldSubsystem* UMiniMapWorldSubsystem::Get(UObject* WorldContextObject)
{
if(UWorld* World = WorldContextObject?WorldContextObject->GetWorld():nullptr)
{
return World->GetSubsystem<UMiniMapWorldSubsystem>();
}
return nullptr;
}
void UMiniMapWorldSubsystem::OnInsertItem(UObject* InObj)
{
if(IsValid(InObj))
{
if(!IsObjectInMemory(InObj))
{
FMemoryDataStruct NewData;
NewData.Obj = InObj;
MemoryList.Add(InObj->GetUniqueID(),NewData);
MiniMapInsertItemDelegate.Broadcast(InObj);
}
}
}
void UMiniMapWorldSubsystem::OnRemoveItem(UObject* InObj)
{
if(IsValid(InObj))
{
if(IsObjectInMemory(InObj))
{
MemoryList.Remove(InObj->GetUniqueID());
MiniMapRemoveItemDelegate.Broadcast(InObj);
}
}
}
bool UMiniMapWorldSubsystem::IsObjectInMemory(UObject* InObj)
{
if(InObj && MemoryList.Contains(InObj->GetUniqueID()))
{
return true;
}
return false;
}
UObject* UMiniMapWorldSubsystem::GetObjectFromMemory(uint32 InId)
{
if(MemoryList.Contains(InId) && MemoryList[InId].GetData())
{
return MemoryList[InId].GetData();
}
return nullptr;
}
别忘了在UMiniMapBase上绑定委托
----2025/3/11更新:这里可能有bug,因为MiniMapComponent可能早于NativeConstruct调用OnInsertItem方法,解决办法是加一个
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UMiniMapComponent::InsertItem, 0.2f, false);
void UMiniMapBase::NativeConstruct()
{
Super::NativeConstruct();
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
if(!WorldSubsystem->MiniMapInsertItemDelegate.IsBoundToObject(this))
{
WorldSubsystem->MiniMapInsertItemDelegate.AddUObject(this,&UMiniMapBase::OnMiniMapItemInsert);
}
if(!WorldSubsystem->MiniMapRemoveItemDelegate.IsBoundToObject(this))
{
WorldSubsystem->MiniMapRemoveItemDelegate.AddUObject(this,&UMiniMapBase::OnMiniMapItemRemove);
}
}
}
void UMiniMapBase::OnMiniMapItemInsert(UObject* InObject)
{
if(InObject)
{
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
MapItemCreateWidgetList.Add(InObject->GetUniqueID());
}
}
}
void UMiniMapBase::OnMiniMapItemRemove(UObject* InObject)
{
if(InObject)
{
if(UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this))
{
MapItemCreateWidgetList.Remove(InObject->GetUniqueID());
}
}
}
加载玩家图标
接下来就要把玩家图标给加载进来了,这里采用一种麻烦的方式,如果从DataTable中读取FSoftObjectPath是不是看上去很正式(滑稽)
首先,创建一个struct给DataTable用
//必须继承FTableRowBase不然用不了
USTRUCT(BlueprintType)
struct FDataTableRowType : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName PageName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (MetaClass = "UserWidget"))
FSoftClassPath ViewClass;
};
然后,创建一个Global类用于管理数据加载,基于该类创建一个蓝图出来
UCLASS(Blueprintable,Config=Engine)
class MINIMAP_API UMiniGlobal : public UObject
{
GENERATED_BODY()
public:
UMiniGlobal();
virtual ~UMiniGlobal();
static UMiniGlobal& Get();
const UDataTable* GetDataTable() const;
template<class T>
const T* GetDataTableRow(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("Can't find row %s"), *RowName.ToString());
return nullptr;
}
}
UE_LOG(LogTemp, Warning, TEXT("DataTable is nullptr"));
return nullptr;
}
public:
//可以在蓝图上设置要加载的DataTable
UPROPERTY(EditDefaultsOnly, Category = "Data Table", meta=(DisplayName="Data Table"))
FSoftObjectPath DataTableRef;
protected:
FString ContextString = TEXT("DataTable Context");
};
const UDataTable* UMiniGlobal::GetDataTable() const
{;
if(DataTableRef.IsValid())
{
UDataTable* DataTable = Cast<UDataTable>(DataTableRef.ResolveObject());
if(DataTable)
{
return DataTable;
}
else
{
DataTable = Cast<UDataTable>(DataTableRef.TryLoad());
return DataTable;
}
}
UE_LOG(LogTemp,Warning,TEXT("DataTableAssetRef does not exist"));
return nullptr;
}
//同样用单例模式获取
UMiniGlobal& UMiniGlobal::Get()
{
#if WITH_EDITOR
if(GEngine->GameSingleton)
{
return *static_cast<UMiniGlobal*>(GEngine->GameSingleton);
}
else
{
//这里放的是生成的蓝图的地址,一定要带_C不然会出问题
FString FixedGlobalsClassName = TEXT("Blueprint'/Game/MiniMap/DT/BP_MiniGlobal.BP_MiniGlobal_C'");
UClass *GlobalsClass = LoadClass<UObject>(nullptr, *FixedGlobalsClassName);
UMiniGlobal* GlobslObj = NewObject<UMiniGlobal>(GEngine, GlobalsClass);
GEngine->GameSingleton = GlobslObj;
return *GlobslObj;
}
#else
UMiniGlobal* Singleton = NewObject<UMiniGlobal>();
return *Singleton;
#endif
}
初始化图标
图标加载进来后不是马上就能用的,肯定要初始化数据的
首先,要和小地图一样创建一个Widget,命名为MiniMapItem
class UMiniMapBase;
class UMiniMapComponent;
UCLASS()
class MINIMAP_API UMiniMapItem : public UUserWidgetBase
{
GENERATED_BODY()
public:
virtual void InitData(UObject* InObj, UMiniMapBase* InOwnerMap);
virtual void RefreshData(UObject* ViewCom,UObject* ViewObj);
virtual void RefreshZorder();
virtual void RefreshRotation(UObject* ViewCom,UObject* ViewObj);
bool GetIsUpdate(UMiniMapComponent* ViewCom,UMiniMapComponent* TargetCom,float& OutX,float& OutY);
FVector2D ClampMiniMapItemLoc(UMiniMapComponent* ViewCom,UMiniMapComponent* TargetCom,float InX,float InY);
protected:
UPROPERTY(Transient)
UObject* OwnerMapComponent = nullptr;
UPROPERTY(Transient)
UMiniMapBase* OwnerMap = nullptr;
UPROPERTY(EditDefaultsOnly,Category="Config")
int32 ItemZorder = 0;
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "MiniMapItem.h"
#include "MiniMapBase.h"
#include "Components/CanvasPanelSlot.h"
#include "Kismet/KismetMathLibrary.h"
void UMiniMapItem::InitData(UObject* InObj, UMiniMapBase* InOwnerMap)
{
if(UMiniMapComponent* MapComp = Cast<UMiniMapComponent>(InObj))
{
OwnerMapComponent = MapComp;
}
if(UMiniMapBase* Map = Cast<UMiniMapBase>(InOwnerMap))
{
OwnerMap = Map;
}
}
bool UMiniMapItem::GetIsUpdate(UMiniMapComponent* ViewCom,UMiniMapComponent* TargetCom, float& OutX, float& OutY)
{
if(!OwnerMap)
{
OutX = 0.f;
OutY = 0.f;
return false;
}
FVector2D RotateWorldLocToChar =FVector2D::ZeroVector;
const float Zoom = OwnerMap->Zoom;
const FVector2D ViewLoc = FVector2D(ViewCom->GetSceneMapTransform().GetLocation());
const FVector2D TargetLoc = FVector2D(ViewCom->GetSceneMapTransform().GetLocation());
const FVector2D TargetToView = TargetLoc - ViewLoc;
RotateWorldLocToChar = UKismetMathLibrary::GetRotated2D(TargetToView,ViewCom->GetSceneMapTransform().Rotator().Yaw-90.f);
const FVector2D PicSize = OwnerMap->Image_Map->GetDesiredSize();
const float PicSizeScale = PicSize.X / 256.f;
const FVector2D NewItem2D = ((RotateWorldLocToChar*PicSizeScale))/Zoom;
OutX = NewItem2D.X;
OutY = NewItem2D.Y;
return true;
}
//玩家图标直接在地图中心就行了
FVector2D UMiniMapItem::ClampMiniMapItemLoc(UMiniMapComponent* ViewCom, UMiniMapComponent* TargetCom, float InX, float InY)
{
if(!OwnerMap)
{
return FVector2D::ZeroVector;
}
const FVector2D MiniMapCenter = OwnerMap->Image_Map->GetDesiredSize() / 2.f;
return FVector2D(InX,InY) + MiniMapCenter;
}
//设置在小地图上面不然会被挡住
void UMiniMapItem::RefreshZorder()
{
Super::RefreshZorder();
ItemZorder = 5;
if(OwnerMap)
{
if(UCanvasPanelSlot* PanelSlot = Cast<UCanvasPanelSlot>(Slot))
{
PanelSlot->SetZOrder(ItemZorder);
}
}
}
void UMiniMapItem::RefreshRotation(UObject* ViewCom,UObject* ViewObj)
{
if(UMiniMapComponent* CharacterComp = Cast<UMiniMapComponent>(ViewCom))
{
FRotator NewRotator = CharacterComp->GetViewRotation();
if(Image_CharacterDir)
{
Image_CharacterDir->SetRenderTransformAngle(NewRotator.Yaw);
}
}
}
创建图标
好了,我们所有准备工作都完成了,现在可以在小地图上创建图标了
void UMiniMapBase::OnHandleItemWidgetCreate()
{
//限制每帧加载数量,虽然说现在用不到
int32 CurrentIndex = 0;
int32 EndIndex = CurrentIndex + CreateWidgetPerFrame;
if(EndIndex > MapItemCreateWidgetList.Num() - 1)
{
EndIndex = MapItemCreateWidgetList.Num() - 1;
}
for(TSet<uint32>::TIterator It = MapItemCreateWidgetList.CreateIterator(); It; ++It)
{
if(CurrentIndex > EndIndex)
{
break;
}
CurrentIndex++;
if(!It.ElementIt)
{
continue;
}
UMiniMapWorldSubsystem* WorldSubsystem = UMiniMapWorldSubsystem::Get(this);
if(WorldSubsystem)
{
UMiniMapComponent* Component = Cast<UMiniMapComponent>(WorldSubsystem->GetObjectFromMemory(It.ElementIt->Value));
FName MapItemName;
FSoftClassPath ItemClassPath;
if(Component)
{
MapItemName = Component->GetSceneMapItemName();
if(MapItemName.IsValid())
{
const UDataTable* DataTable = UMiniGlobal::Get().GetDataTable();
auto DataTableRow = UMiniGlobal::Get().GetDataTableRow<FDataTableRowType>(DataTable,MapItemName);
if(DataTableRow)
{
ItemClassPath = DataTableRow->ViewClass;
}
}
if(ItemClassPath.IsValid())
{
if(UClass* ItemClass = ItemClassPath.ResolveClass())
{
const int32 UniqueID = Component->GetUniqueID();
if(!MapItemList.Contains(UniqueID))
{
if(UMiniMapItem* Widget = CreateWidget<UMiniMapItem>(GetWorld(),ItemClass))
{
if(Container)
{
if(UCanvasPanelSlot* PanelSlot = Container->AddChildToCanvas(Widget))
{
FMapItemMemory Memory;
Memory.Id = UniqueID;
Memory.Widget = Widget;
MapItemList.Add(UniqueID,Memory);
if(It.ElementIt->Value == UniqueID)
{
It.RemoveCurrent();
}
Widget->SetVisibility(ESlateVisibility::Collapsed);
Widget->InitData(Component,this);
}
}
}
}
else
{
if(It.ElementIt->Value == UniqueID)
{
It.RemoveCurrent();
}
}
}
else
{
ItemClass=ItemClassPath.TryLoadClass<UClass>();
}
}
}
}
更新图标
这时候创建出来的图标还是死的不会动,所以需要每帧更新让它动起来
//也是为了后续拓展
void UMiniMapBase::UpdateMapItemSet(const TSet<UObject*>& InObjSet, const AActor* ViewActor, float InDeltaTime)
{
if(!ViewActor)
{
return;
}
for(const UObject* Obj:InObjSet)
{
if(MapItemList.Contains(Obj->GetUniqueID()))
{
if(UMiniMapComponent* ViewCom =ViewActor->FindComponentByClass<UMiniMapComponent>())
{
UpdateMapItem(ViewCom,const_cast<UObject*>(Obj),InDeltaTime);
}
}
}
}
void UMiniMapBase::UpdateMapItem(UMiniMapComponent* ViewCom,UObject* Obj,float InDeltaTime)
{
if(!Obj)
{
return;
}
const FMapItemMemory& MapItemMemory = MapItemList.FindRef(Obj->GetUniqueID());
if(UMiniMapItem* Widget = MapItemMemory.Widget)
{
UMiniMapComponent* ViewObj = Cast<UMiniMapComponent>(Obj);
if(ViewObj)
{
float XScale = 0.f;
float YScale = 0.f;
if(Widget->GetIsUpdate(ViewCom,ViewObj,XScale,YScale))
{
const FVector2D Result = Widget->ClampMiniMapItemLoc(ViewCom,ViewObj,XScale,YScale);
Widget->SetRenderTranslation(Result);
Widget->SetVisibility(ESlateVisibility::Visible);
Widget->RefreshData(ViewCom,ViewObj);
}
}
}
}
完成
恭喜你现在有了一个初具雏形的小地图了,添加进项目欣赏一下吧!

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

浙公网安备 33010602011771号