UE5多人MOBA+GAS 49、创建大厅

文章目录


创建各种类

创建大厅游戏模式

LobbyGameMode

创建新的玩家控制器

MenuPlayerController菜单玩家控制器

LobbyPlayerController大厅玩家控制器

创建玩家状态

创建大厅UI

LobbyWidget

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "LobbyWidget.generated.h"

class UUniformGridPanel;
class UButton;
class UWidgetSwitcher;
/**
 * 
 */
UCLASS()
class CRUNCH_API ULobbyWidget : public UUserWidget
{
	GENERATED_BODY()
private:
	// 主界面切换器
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UWidgetSwitcher> MainSwitcher;
	// 队伍选择根节点
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UWidget> TeamSelectionRoot;
	// 开始英雄选择按钮
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UButton> StartHeroSelectionButton;
	// 队伍选择格子面板
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UUniformGridPanel> TeamSelectionSlotGridPanel;
};

创建大厅UI蓝图

添加垂直框里面再放一个水平框,放入两个文本

添加一个统一网格面板

菜单玩家控制器中显示菜单

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MenuPlayerController.generated.h"

/**
 * 菜单界面专用玩家控制器
 * 负责菜单UI的生成与管理
 */
UCLASS()
class CRUNCH_API AMenuPlayerController : public APlayerController
{
	GENERATED_BODY()
public:	
	// 游戏开始时调用
	virtual void BeginPlay() override;

	// 玩家状态同步时调用
	virtual void OnRep_PlayerState() override;

private:
	// 菜单界面类
	UPROPERTY(EditDefaultsOnly, Category = "Menu")
	TSubclassOf<UUserWidget> MenuWidgetClass;

	// 菜单界面实例
	UPROPERTY()
	TObjectPtr<UUserWidget> MenuWidget;

	// 生成菜单界面
	void SpawnWidget();
};
cpp 复制代码
#include "MenuPlayerController.h"

#include "Blueprint/UserWidget.h"

void AMenuPlayerController::BeginPlay()
{
	Super::BeginPlay();
	// 设置为仅UI输入模式并显示鼠标
	SetInputMode(FInputModeUIOnly());
	SetShowMouseCursor(true);

	// 具有服务器权限而且是本地玩家控制器时生成菜单UI
	if (HasAuthority() && IsLocalPlayerController())
	{
		SpawnWidget();
	}
}

void AMenuPlayerController::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();
	if (IsLocalPlayerController())
	{
		SpawnWidget();
	}
}

void AMenuPlayerController::SpawnWidget()
{
	if (MenuWidgetClass)
	{
		MenuWidget = CreateWidget<UUserWidget>(this, MenuWidgetClass);
		if (MenuWidget)
		{
			MenuWidget->AddToViewport();
		}
	}
}

大厅游戏模式中添加构造函数

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "CGameMode.h"
#include "LobbyGameMode.generated.h"

/**
 * 
 */
UCLASS()
class CRUNCH_API ALobbyGameMode : public ACGameMode
{
	GENERATED_BODY()
public:	
	ALobbyGameMode();
};
cpp 复制代码
#include "LobbyGameMode.h"

ALobbyGameMode::ALobbyGameMode()
{
	bUseSeamlessTravel = true;
}

创建大厅的游戏模式和玩家控制器的蓝图版本

把大厅游戏模式放入大厅地图中

TeamSelectionWidget

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "TeamSelectionWidget.generated.h"

class UTextBlock;
class UButton;
// 声明一个委托:当这个插槽被点击时广播出去,并携带这个插槽的ID。
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSlotClicked, uint8 /*SlotID*/);

/**
 * 用于表示队伍选择界面中一个可选槽位(Slot)的UI控件。
 */
UCLASS()
class CRUNCH_API UTeamSelectionWidget : public UUserWidget
{
	GENERATED_BODY()
public:
	// 设置此UI控件代表的槽位ID。
	void SetSlotID(uint8 NewSlotID);

	// 更新此槽位显示的信息(通常是玩家昵称)。
	void UpdateSlotInfo(const FString& PlayerNickName);

	// 重写原生构建函数,用于初始化绑定。
	virtual void NativeConstruct() override;

	// 当用户点击这个槽位的选择按钮时,会广播此委托。
	FOnSlotClicked OnSlotClicked;

private:
	// 用于选择此槽位的按钮。
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UButton> SelectButton;  

	// 用于显示槽位信息(如玩家名)的文本控件。
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> InfoText;  

	// UFUNCTION宏标记,用于绑定按钮点击事件。
	UFUNCTION()
	void SelectButtonClicked(); // 内部处理按钮点击的函数。
	
	// 此控件所代表的槽位的唯一标识符(ID)。
	uint8 SlotID;  
};
cpp 复制代码
#include "TeamSelectionWidget.h"

#include "Components/Button.h"
#include "Components/TextBlock.h"

void UTeamSelectionWidget::SetSlotID(uint8 NewSlotID)
{
	SlotID = NewSlotID;
}

void UTeamSelectionWidget::UpdateSlotInfo(const FString& PlayerNickName)
{
	// 显示玩家昵称
	InfoText->SetText(FText::FromString(PlayerNickName));
}

void UTeamSelectionWidget::NativeConstruct()
{
	Super::NativeConstruct();
	// 将按钮控件的"OnClicked"事件动态绑定到本类的SelectButtonClicked函数。
	// 当用户点击按钮时,就会调用SelectButtonClicked。
	SelectButton->OnClicked.AddDynamic(this, &UTeamSelectionWidget::SelectButtonClicked);
}

void UTeamSelectionWidget::SelectButtonClicked()
{
	// 广播委托
	OnSlotClicked.Broadcast(SlotID);
}

创建蓝图版本

添加边界

添加网络函数库类

这里选错了,选第二个

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TNetStatics.generated.h"

/**
 * 
 */
UCLASS()
class CRUNCH_API UTNetStatics : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
public:	
	/**
	 * 获取每支队伍的基础玩家数量
	 * @return 玩家数量(默认值为5)
	 */
	static uint8 GetPlayerCountPerTeam();
};
cpp 复制代码
#include "TNetStatics.h"

uint8 UTNetStatics::GetPlayerCountPerTeam()
{
	return 5;
}

大厅UI中添加队伍插槽

cpp 复制代码
public:
	virtual void NativeConstruct() override;
private:
	// 队伍选择格子
	UPROPERTY(EditDefaultsOnly, Category = "TeamSelection")
	TSubclassOf<UTeamSelectionWidget> TeamSelectionWidgetClass;

	// 所有队伍选择格子
	UPROPERTY()
	TArray<UTeamSelectionWidget*> TeamSelectionSlots;
	
	/**
	 * 清空并重新生成队伍选择槽位
	 * 根据玩家数量生成双队列网格布局
	 */
	void ClearAndPopulateTeamSelectionSlots();
	/**
	 * 处理槽位点击事件
	 * @param NewSlotID 新选择的槽位ID
	 */
	void SlotSelected(uint8 NewSlotID);
cpp 复制代码
void ULobbyWidget::NativeConstruct()
{
	Super::NativeConstruct();
	ClearAndPopulateTeamSelectionSlots();
}

void ULobbyWidget::ClearAndPopulateTeamSelectionSlots()
{
	TeamSelectionSlotGridPanel->ClearChildren();

	// 生成两队玩家槽
	for (int i = 0; i < UTNetStatics::GetPlayerCountPerTeam() * 2; ++i)
	{
		// 创建槽位
		if (UTeamSelectionWidget* NewSelectionSlot = CreateWidget<UTeamSelectionWidget>(this, TeamSelectionWidgetClass))
		{
			// 设置槽ID
			NewSelectionSlot->SetSlotID(i);
			// 添加到网格布局
			if (UUniformGridSlot* NewGridSlot = TeamSelectionSlotGridPanel->AddChildToUniformGrid(NewSelectionSlot))
			{
				// 计算行列位置
				int Row = i % UTNetStatics::GetPlayerCountPerTeam();
				int Column = i < UTNetStatics::GetPlayerCountPerTeam() ? 0 : 1;

				NewGridSlot->SetRow(Row);
				NewGridSlot->SetColumn(Column);
			}
			// 绑定槽点击事件产生的广播委托
			NewSelectionSlot->OnSlotClicked.AddUObject(this, &ULobbyWidget::SlotSelected);
			TeamSelectionSlots.Add(NewSelectionSlot);
		}
	}
}

void ULobbyWidget::SlotSelected(uint8 NewSlotID)
{
	UE_LOG(LogTemp, Warning, TEXT("切换的槽位: %d"), NewSlotID)
}

新建一个空白类PlayerInfoTypes用于存储玩家的网络ID、名字以及对应的队伍插槽

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "PlayerInfoTypes.generated.h"

class APlayerState;

/**
 * 玩家选择信息结构体
 * 存储玩家在游戏大厅中的选择状态,包括槽位、角色定义等信息
 */
USTRUCT()
struct FPlayerSelection
{
	GENERATED_BODY()
public:
	/**
	 * 默认构造函数
	 * 初始化空的玩家选择
	 */
	FPlayerSelection();
	
	/**
	 * 带参数的构造函数
	 * @param InSlot 玩家槽位ID
	 * @param InPlayerState 玩家状态对象
	 */
	FPlayerSelection(uint8 InSlot, const APlayerState* InPlayerState);

	/**
	 * 设置玩家槽位
	 * @param NewSlot 新槽位ID
	 */
	FORCEINLINE void SetSlot(uint8 NewSlot) { Slot = NewSlot; }
	
	/**
	 * 获取玩家槽位ID
	 * @return 当前玩家槽位
	 */
	FORCEINLINE uint8 GetPlayerSlot() const { return Slot; }
	
	/**
	 * 获取玩家唯一ID
	 * @return 网络唯一ID副本
	 */
	FORCEINLINE FUniqueNetIdRepl GetPLayerUniqueId() const { return PlayerUniqueId; }
	
	/**
	 * 获取玩家昵称
	 * @return 玩家显示名称
	 */
	FORCEINLINE FString GetPlayerNickName() const { return PlayerNickName; }
	
	/**
	 * 检查是否属于指定玩家
	 * @param PlayerState 待比较的玩家状态对象
	 * @return 是否匹配
	 */
	bool IsForPlayer(const APlayerState* PlayerState) const;
	
	/**
	 * 验证选择数据有效性
	 * @return 是否为有效选择
	 */
	bool IsValid() const;

	/**
	 * 获取无效槽位标识符
	 * @return 无效槽位的数值表示
	 */
	static uint8 GetInvalidSlot();

private:
	/** 玩家在队伍中的槽位ID */
	UPROPERTY()
	uint8 Slot;

	/** 玩家的网络唯一ID */
	UPROPERTY()
	FUniqueNetIdRepl PlayerUniqueId;

	/** 玩家显示名称 */
	UPROPERTY() 
	FString PlayerNickName;
};
cpp 复制代码
#include "Player/PlayerInfoTypes.h"

#include "GameFramework/PlayerState.h"
#include "Network/TNetStatics.h"

FPlayerSelection::FPlayerSelection()
		:Slot{ GetInvalidSlot() },
		PlayerUniqueId{ FUniqueNetIdRepl::Invalid() },
		PlayerNickName{""}
{
}

FPlayerSelection::FPlayerSelection(uint8 InSlot, const APlayerState* InPlayerState)
			:Slot{ InSlot }
{
	if (InPlayerState)
	{
		// 获取网络唯一ID和名称
		PlayerUniqueId = InPlayerState->GetUniqueId();
		PlayerNickName = InPlayerState->GetPlayerName();
	}
}

bool FPlayerSelection::IsForPlayer(const APlayerState* PlayerState) const
{
	if (!PlayerState)
		return false;
	// 编辑器启动的时候用玩家名字判断
#if WITH_EDITOR
	return PlayerState->GetPlayerName() == PlayerNickName;
#else
	// 运行时用网络唯一ID判断
	return PlayerState->GetUniqueId() == GetPLayerUniqueId();
#endif
}

bool FPlayerSelection::IsValid() const
{
#if WITH_EDITOR
	return true; // 编辑器模式始终返回有效(用于测试)
#else
	// 检查网络唯一ID是否有效
	if (!PlayerUniqueId.IsValid())
		return false;
	// 槽位是否有效
	if (Slot == GetInvalidSlot())
		return false;

	// 检查槽位是否超出双队列限制
	if (Slot >= UCNetStatics::GetPlayerCountPerTeam() * 2)
		return false;

	return true;
#endif
}

uint8 FPlayerSelection::GetInvalidSlot()
{
	// 无效槽位
	return 255;
}

创建游戏状态

CGameState

cpp 复制代码
// 幻雨喜欢小猫咪

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "Player/PlayerInfoTypes.h"
#include "CGameState.generated.h"

/**
 * 玩家选择更新委托
 * 当玩家选择状态发生变化时广播
 * @param NewPlayerSelection 新的玩家选择数组
 */
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPlayerSelectionUpdated, const TArray<FPlayerSelection>& /*NewPlayerSelection*/);

/**
 * 游戏状态类(CGameState)
 * 管理大厅阶段的玩家选择状态,处理角色选择和队伍槽位分配
 * 负责维护玩家选择数据并提供相关操作接口
 */
UCLASS()
class CRUNCH_API ACGameState : public AGameStateBase
{
	GENERATED_BODY()
public:
	/**
	 * 请求更改玩家选择槽位
	 * @param RequestingPlayer 请求更改的玩家状态
	 * @param DesiredSlot 目标槽位ID
	 */
	void RequestPlayerSelectionChange(const APlayerState* RequestingPlayer, uint8 DesiredSlot);

	/**
	 * 检查槽位是否被占用
	 * @param SlotId 要检查的槽位ID
	 * @return 是否被占用
	 */
	bool IsSlotOccupied(uint8 SlotId) const;
	
	/** 玩家选择更新事件(多播委托) */
	FOnPlayerSelectionUpdated OnPlayerSelectionUpdated;

	/**
	 * 获取玩家选择数组
	 * @return 当前所有玩家的选择信息数组
	 */
	const TArray<FPlayerSelection>& GetPlayerSelection() const;

	/**
	 * 检查是否可以开始英雄选择
	 * @return 是否满足开始条件
	 */
	bool CanStartHeroSelection() const;

	/**
	 * 复制属性接口实现
	 * 声明需要网络复制的属性
	 */
	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > &OutLifetimeProps) const override;

private:
	/**
	 * 玩家选择数组(网络复制)
	 * 存储所有玩家当前的槽位和角色选择信息
	 */
	UPROPERTY(ReplicatedUsing = OnRep_PlayerSelectionArray)
	TArray<FPlayerSelection> PlayerSelectionArray;

	/**
	 * 玩家选择数组复制回调
	 * 当网络同步玩家选择数据时触发
	 */
	UFUNCTION()
	void OnRep_PlayerSelectionArray();
};
cpp 复制代码
// 幻雨喜欢小猫咪


#include "CGameState.h"

#include "Net/UnrealNetwork.h"

void ACGameState::RequestPlayerSelectionChange(const APlayerState* RequestingPlayer, uint8 DesiredSlot)
{
	// 仅服务器处理且目标槽位未被占用
	if (!HasAuthority() || IsSlotOccupied(DesiredSlot))
		return;

	// 查找当前玩家的已有选择
	FPlayerSelection* PlayerSelectionPtr = PlayerSelectionArray.FindByPredicate([&](const FPlayerSelection& PlayerSelection)
		{
			return PlayerSelection.IsForPlayer(RequestingPlayer);
		}
	);

	if (PlayerSelectionPtr)
	{
		// 更新现有槽位
		PlayerSelectionPtr->SetSlot(DesiredSlot);
	}
	else
	{
		// 添加新的玩家选择
		PlayerSelectionArray.Add(FPlayerSelection(DesiredSlot, RequestingPlayer));
	}

	// 广播玩家选择更新
	OnPlayerSelectionUpdated.Broadcast(PlayerSelectionArray);
}

bool ACGameState::IsSlotOccupied(uint8 SlotId) const
{
	// 寻找已经选择的玩家数组,查看是否有该插槽,如果找到说明给占了
	for (const FPlayerSelection& PlayerSelection : PlayerSelectionArray)
	{
		if (PlayerSelection.GetPlayerSlot() == SlotId)
		{
			return true;
		}
	}

	return false;
}

const TArray<FPlayerSelection>& ACGameState::GetPlayerSelection() const
{
	return PlayerSelectionArray;
}

bool ACGameState::CanStartHeroSelection() const
{
	// 玩家数量和已选择的玩家数量相等
	return PlayerSelectionArray.Num() == PlayerArray.Num();
}

void ACGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	// 将玩家选择数组声明为网络复制属性
	DOREPLIFETIME_CONDITION_NOTIFY(ACGameState, PlayerSelectionArray, COND_None, REPNOTIFY_Always);
}

void ACGameState::OnRep_PlayerSelectionArray()
{
	// 广播玩家选择更新事件
	OnPlayerSelectionUpdated.Broadcast(PlayerSelectionArray);
}

玩家选择和大厅连接

大厅玩家控制器中

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "MenuPlayerController.h"
#include "LobbyPlayerController.generated.h"

/**
 * 
 */
UCLASS()
class CRUNCH_API ALobbyPlayerController : public AMenuPlayerController
{
	GENERATED_BODY()
public:
	/**
	 * 服务器端处理槽位选择变更请求
	 * @param NewSlotID 新的槽位ID
	 * 
	 * 网络调用:客户端请求变更槽位选择时调用
	 * 服务器验证后更新玩家槽位
	 */
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_RequestSlotSelectionChange(uint8 NewSlotID);
};
cpp 复制代码
#include "LobbyPlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Framework/CGameState.h"

void ALobbyPlayerController::Server_RequestSlotSelectionChange_Implementation(uint8 NewSlotID)
{
	if (!GetWorld())
		return;

	// 获取当前游戏状态
	ACGameState* CGameState = GetWorld()->GetGameState<ACGameState>();
	if (!CGameState)
		return;

	// 委托给游戏状态管理器处理槽位变更
	CGameState->RequestPlayerSelectionChange(GetPlayerState<APlayerState>(), NewSlotID);
} 

bool ALobbyPlayerController::Server_RequestSlotSelectionChange_Validate(uint8 NewSlotID)
{
	return true;
}

大厅UI

cpp 复制代码
	// 玩家控制器
	UPROPERTY()
	TObjectPtr<ALobbyPlayerController> LobbyPlayerController;
	
	/**
	 * 配置游戏状态监听
	 * 尝试获取游戏状态对象并绑定更新事件
	 */
	void ConfigureGameState();
	
	// 配置游戏状态定时器句柄
	FTimerHandle ConfigureGameStateTimerHandle;

	// 游戏状态
	UPROPERTY()
	TObjectPtr<ACGameState> CGameState;

	/**
	 * 更新玩家选择显示
	 * @param PlayerSelections 玩家选择信息数组
	 */
	void UpdatePlayerSelectionDisplay(const TArray<FPlayerSelection>& PlayerSelections);
cpp 复制代码
void ULobbyWidget::NativeConstruct()
{
	Super::NativeConstruct();
	ClearAndPopulateTeamSelectionSlots();
	// 获取玩家控制器
	LobbyPlayerController = GetOwningPlayer<ALobbyPlayerController>();
	// 配置游戏状态
	ConfigureGameState();
}

void ULobbyWidget::SlotSelected(uint8 NewSlotID)
{
	if (LobbyPlayerController)
	{
		LobbyPlayerController->Server_RequestSlotSelectionChange(NewSlotID);
	}
}

void ULobbyWidget::ConfigureGameState()
{
	UWorld* World = GetWorld();
	if (!World) return;

	// 获取游戏状态
	CGameState = World->GetGameState<ACGameState>();
	if (!CGameState)
	{
		// 如果没有找到,设置定时器定期重试
		World->GetTimerManager().SetTimer(ConfigureGameStateTimerHandle, this, &ULobbyWidget::ConfigureGameState, 1.f);
	}else
	{
		// 绑定玩家选择更新事件
		CGameState->OnPlayerSelectionUpdated.AddUObject(this, &ULobbyWidget::UpdatePlayerSelectionDisplay);
		// 初始化显示当前玩家选择
		UpdatePlayerSelectionDisplay(CGameState->GetPlayerSelection());
	}
}

void ULobbyWidget::UpdatePlayerSelectionDisplay(const TArray<FPlayerSelection>& PlayerSelections)
{
	// 清空所有槽位显示
	for (UTeamSelectionWidget* SelectionSlot : TeamSelectionSlots)
	{
		SelectionSlot->UpdateSlotInfo("Empty");
	}
	// 更新每个玩家的槽位显示
	for (const FPlayerSelection& PlayerSelection : PlayerSelections)
	{
		if (!PlayerSelection.IsValid())
			continue;
		// 更新槽位名称显示
		TeamSelectionSlots[PlayerSelection.GetPlayerSlot()]->UpdateSlotInfo(PlayerSelection.GetPlayerNickName());
	}
}

创建游戏状态蓝图版本

游戏模式中设置游戏状态

创建玩家状态的蓝图版本


相关推荐
幻雨様2 天前
UE5多人MOBA+GAS 43、制作激光技能
ue5
幻雨様2 天前
UE5多人MOBA+GAS 48、制作闪现技能
ue5
ue星空4 天前
UE5配置MRQ编解码器输出MP4视频
ue5·音视频
吴梓穆10 天前
UE5 图片9宫格切割
ue5
Kingsdesigner12 天前
游戏开发流程革命:我用Substance插件,在UE5内实现材质的实时“创世纪”
游戏·adobe·ue5·游戏引擎·游戏开发·设计师·substance 3d
幻雨様13 天前
UE5多人MOBA+GAS 37、库存系统(四)
ue5
DoomGT13 天前
Physics Simulation - UE中Projectile相关事项
ue5·游戏引擎·虚幻·虚幻引擎·unreal engine
右弦GISer15 天前
【UE5医学影像可视化】读取本地Dicom生成VolumeTexture,实现2D显示和自动翻页
ue5·dicom·医学图像
小梦白15 天前
RPG增容3:尝试使用MVC结构搭建玩家升级UI(一)
游戏·ui·ue5·mvc