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());
	}
}

创建游戏状态蓝图版本

游戏模式中设置游戏状态

创建玩家状态的蓝图版本


相关推荐
远离UE42 天前
UE5 母材质 编写笔记
笔记·ue5·材质
远离UE42 天前
UE5 HLSLTranslator 与 ShaderCompiler
ue5
1204157137 肖哥3 天前
UE5.7 场景打光管线流程_【雪山】
ue5
weixin_404679313 天前
虚幻5 替代小白人角色的完整逻辑
ue5
1204157137 肖哥6 天前
UE5.7 灯光详解
ue5
狂云歌7 天前
AI写UE的3Dgame
人工智能·3d·ue5·游戏程序
日月云棠11 天前
UE5源码分析之Editor——Blutility模块全面分析
ue5
日月云棠11 天前
UE5源码分析之Editor——BehaviorTreeEditor模块全面分析
ue5
日月云棠11 天前
UE5源码分析之Editor——AssetTagsEditor模块全面分析
ue5
远离UE412 天前
UE5 显存 虚拟内存 深入学习笔记
笔记·学习·ue5