文章目录
创建各种类
创建大厅游戏模式
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());
}
}
创建游戏状态蓝图版本
游戏模式中设置游戏状态
创建玩家状态的蓝图版本
