UE中实现简单的小地图(Texture实现)

获取小地图材质

将SceneCapture2D组件添加到场景中心位置,如下图所示摆放
image
设置参数如下
image
新建一个RenderTarget对象作为SceneCapture2D的保存对象,然后把图像提取出来,通过PS(不P也没影响单纯为了好看)获取得到下图所示Texture
image
使用此Texture创建Material,记得把属性改成UI
image
在材质窗口,进行如下操作,Zoom(放缩),WorldDimension(尺度),(X,Y)(角色当前坐标)
image
后续通过C++传入X,Y的值对小地图UV坐标进行更改,注意由于材质向下为Y轴正方向,向右为X轴正方向,与世界的坐标XY轴相反,所以这里取反。
如果想设计成圆的可以添加以下操作。
image
最后,以该材质创建出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);
			}
		}
	}
}

完成

恭喜你现在有了一个初具雏形的小地图了,添加进项目欣赏一下吧!
image

参考链接

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

posted @ 2025-01-24 15:31  42今天吃什么  阅读(425)  评论(0)    收藏  举报