当前位置:首页 > 技术 > 正文内容

虚幻引擎中基于纹理的简易小地图实现方案

访客 技术 2026年7月12日 1

获取小地图材质

首先在场景中央位置添加SceneCapture2D组件,参考下图进行布置 image 按照以下参数进行配置 image 创建一个新的RenderTarget对象作为SceneCapture2D的存储目标,然后提取图像,通过图像处理软件(可选处理,仅为了美观)获取如下图所示的纹理资源 image 使用此纹理创建材质,记得将材质属性设置为UI类型 image 在材质编辑器中进行以下操作,设置Zoom(缩放)、WorldDimension(世界尺寸)以及(X,Y)(角色当前位置)参数 image 后续将通过C++代码传入X、Y值来动态调整小地图的UV坐标,注意由于材质坐标系中向下为Y轴正方向,向右为X轴正方向,这与世界坐标系的XY轴方向相反,因此需要进行取反处理。 若需要设计为圆形小地图,可添加以下操作。 image 最后,基于该材质创建材质实例(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);
			}
		}
	}
}

完成

恭喜你现在已经拥有了一个基础功能完备的小地图系统,将其添加到项目中测试一下吧! image

参考链接

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

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。