文章目录
创建队伍的选择英雄显示
创建队伍插槽UI
PlayerTeamSlotWidget

#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "PlayerTeamSlotWidget.generated.h"
class UTextBlock;
class UImage;
class UPDA_CharacterDefinition;
/**
* 队伍槽位UI控件 - 用于在队伍界面中显示单个玩家信息
* 功能:
* - 显示玩家名称和选择的角色图标
* - 支持鼠标悬停效果(显示角色名称)
* - 动态更新玩家状态
*/
UCLASS()
class CRUNCH_API UPlayerTeamSlotWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 控件初始化
virtual void NativeConstruct() override;
/**
* 更新槽位信息
* @param PlayerName 玩家名称
* @param CharacterDefinition 角色定义资产(包含图标和名称)
*/
void UpdateSlot(const FString& PlayerName, const UPDA_CharacterDefinition* CharacterDefinition);
// 鼠标悬停事件处理
virtual void NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
// 鼠标离开事件处理
virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override;
private:
// 悬停动画
UPROPERTY(Transient, meta = (BindWidgetAnim))
TObjectPtr<UWidgetAnimation> HoverAnim;
// 角色图标
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> PlayerCharacterIcon;
// 名称文本
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> NameText;
// 材质参数:角色图标纹理
UPROPERTY(EditDefaultsOnly, Category = "Visual")
FName CharacterIconMatParamName = "Icon";
// 材质参数:空槽位状态
UPROPERTY(EditDefaultsOnly, Category = "Visual")
FName CharacterEmptyMatParamName = "Empty";
// 缓存的玩家名称
FString CachedPlayerNameStr;
// 缓存的角色名称
FString CachedCharacterNameStr;
// 更新名称文本显示(根据悬停状态)
void UpdateNameText();
};
#include "PlayerTeamSlotWidget.h"
#include "Character/PDA_CharacterDefinition.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
void UPlayerTeamSlotWidget::NativeConstruct()
{
Super::NativeConstruct();
// 初始化:设置为空槽位状态
PlayerCharacterIcon->GetDynamicMaterial()->SetScalarParameterValue(CharacterEmptyMatParamName, 1);
// 清空缓存的角色名称
CachedCharacterNameStr = "";
}
void UPlayerTeamSlotWidget::UpdateSlot(const FString& PlayerName, const UPDA_CharacterDefinition* CharacterDefinition)
{
// 缓存玩家名称
CachedPlayerNameStr = PlayerName;
if (CharacterDefinition)
{
// 设置角色图片
PlayerCharacterIcon->GetDynamicMaterial()->SetTextureParameterValue(
CharacterIconMatParamName, CharacterDefinition->LoadIcon());
// 设置角色为非空
PlayerCharacterIcon->GetDynamicMaterial()->SetScalarParameterValue(
CharacterEmptyMatParamName, 0);
// 缓存角色名称
CachedCharacterNameStr = CharacterDefinition->GetCharacterDisplayName();
}else
{
// 无角色选择时:
// 标记为空槽位
PlayerCharacterIcon->GetDynamicMaterial()->SetScalarParameterValue(CharacterEmptyMatParamName, 1);
// 清空角色名称缓存
CachedCharacterNameStr = "";
}
// 根据当前状态更新文本显示
UpdateNameText();
}
void UPlayerTeamSlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseEnter(InGeometry, InMouseEvent);
// 鼠标悬停时:
// 1. 显示角色名称
NameText->SetText(FText::FromString(CachedCharacterNameStr));
// 2. 正向播放悬停动画
PlayAnimationForward(HoverAnim);
}
void UPlayerTeamSlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseLeave(InMouseEvent);
// 鼠标离开时:
// 1. 恢复显示玩家名称
NameText->SetText(FText::FromString(CachedPlayerNameStr));
// 2. 反向播放悬停动画(返回原始状态)
PlayAnimationReverse(HoverAnim);
}
void UPlayerTeamSlotWidget::UpdateNameText()
{
// 根据悬停状态决定显示内容
if (IsHovered())
{
// 悬停时显示角色名称
NameText->SetText(FText::FromString(CachedCharacterNameStr));
}
else
{
// 非悬停时显示玩家名称
NameText->SetText(FText::FromString(CachedPlayerNameStr));
}
}
创建材质






这样就可以让图片不会大出框框

创建放置队伍插槽的UI
PlayerTeamLayoutWidget

#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Player/PlayerInfoTypes.h"
#include "PlayerTeamLayoutWidget.generated.h"
class UHorizontalBox;
class UPlayerTeamSlotWidget;
/**
* 队伍布局UI控件 - 管理并显示两支队伍(红队/蓝队)的玩家槽位
* 功能:
* - 动态创建队伍槽位控件
* - 将槽位分配到两个水平布局框(队伍1和队伍2)
* - 根据玩家选择数据更新所有槽位
*/
UCLASS()
class CRUNCH_API UPlayerTeamLayoutWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
/**
* 更新所有玩家选择状态
* @param PlayerSelections 玩家选择数据数组(包含所有玩家的选择信息)
*/
void UpdatePlayerSelection(const TArray<FPlayerSelection>& PlayerSelections);
private:
// 槽位控件之间的间距
UPROPERTY(EditDefaultsOnly, Category = "Visual")
float PlayerTeamWidgetSlotMargin = 5.f;
// 单个玩家槽位控件的类引用(蓝图类)
UPROPERTY(EditDefaultsOnly, Category = "Visual")
TSubclassOf<UPlayerTeamSlotWidget> PlayerTeamSlotWidgetClass;
// 队伍1的槽位水平布局框
UPROPERTY(meta = (BindWidget))
TObjectPtr<UHorizontalBox> TeamOneLayoutBox;
// 队伍2的槽位水平布局框
UPROPERTY(meta = (BindWidget))
TObjectPtr<UHorizontalBox> TeamTwoLayoutBox;
// 槽位数组
UPROPERTY()
TArray<TObjectPtr<UPlayerTeamSlotWidget>> TeamSlotWidgets;
};
#include "PlayerTeamLayoutWidget.h"
#include "PlayerTeamSlotWidget.h"
#include "Components/HorizontalBox.h"
#include "Components/HorizontalBoxSlot.h"
#include "Network/TNetStatics.h"
void UPlayerTeamLayoutWidget::NativeConstruct()
{
Super::NativeConstruct();
// 清空现有布局
TeamOneLayoutBox->ClearChildren();
TeamTwoLayoutBox->ClearChildren();
if (!PlayerTeamSlotWidgetClass) return;
// 计算总玩家数量(每队人数 * 2)
const int32 TotalPlayers = UTNetStatics::GetPlayerCountPerTeam() * 2;
for (int32 i = 0; i < TotalPlayers; ++i)
{
// 创建新的槽位控件实例
UPlayerTeamSlotWidget* NewSlotWidget = CreateWidget<UPlayerTeamSlotWidget>(GetOwningPlayer(), PlayerTeamSlotWidgetClass);
// 添加到控件数组
TeamSlotWidgets.Add(NewSlotWidget);
// 队伍选择
UHorizontalBoxSlot* NewSlot = i < UTNetStatics::GetPlayerCountPerTeam()
? TeamOneLayoutBox->AddChildToHorizontalBox(NewSlotWidget) // 添加到第一队
: TeamTwoLayoutBox->AddChildToHorizontalBox(NewSlotWidget); // 添加到第二队
// 设置间距
NewSlot->SetPadding(FMargin{PlayerTeamWidgetSlotMargin});
}
}
void UPlayerTeamLayoutWidget::UpdatePlayerSelection(const TArray<FPlayerSelection>& PlayerSelections)
{
// 清空所有槽位
for (UPlayerTeamSlotWidget* SlotWidget : TeamSlotWidgets)
{
SlotWidget->UpdateSlot("", nullptr);
}
// 遍历所有玩家选择
for (const FPlayerSelection& PlayerSelection : PlayerSelections)
{
if (!PlayerSelection.IsValid())
continue;
TeamSlotWidgets[PlayerSelection.GetPlayerSlot()]->UpdateSlot(PlayerSelection.GetPlayerNickName(), PlayerSelection.GetCharacterDefinition());
}
}
大厅UIULobbyWidget中添加队伍UI
// 玩家队伍布局
UPROPERTY(meta=(BindWidget))
TObjectPtr<UPlayerTeamLayoutWidget> PlayerTeamLayoutWidget;
void ULobbyWidget::UpdatePlayerSelectionDisplay(const TArray<FPlayerSelection>& PlayerSelections)
{
// 清空所有槽位显示
for (UTeamSelectionWidget* SelectionSlot : TeamSelectionSlots)
{
SelectionSlot->UpdateSlotInfo("Empty");
}
// 重置角色选择项的选中状态
for (UUserWidget* CharacterEntryAsWidget : CharacterSelectionTileView->GetDisplayedEntryWidgets())
{
if (UCharacterEntryWidget* CharacterEntryWidget = Cast<UCharacterEntryWidget>(CharacterEntryAsWidget))
{
CharacterEntryWidget->SetSelected(false);
}
}
// 更新每个玩家的槽位显示
for (const FPlayerSelection& PlayerSelection : PlayerSelections)
{
if (!PlayerSelection.IsValid())
continue;
// 更新槽位名称显示
TeamSelectionSlots[PlayerSelection.GetPlayerSlot()]->UpdateSlotInfo(PlayerSelection.GetPlayerNickName());
// 已选择的角色变成灰色让别人知道不能选了
if (UCharacterEntryWidget* SelectedEntry = CharacterSelectionTileView->GetEntryWidgetFromItem<UCharacterEntryWidget>(PlayerSelection.GetCharacterDefinition()))
{
SelectedEntry->SetSelected(true);
}
// 如果是当前玩家,更新角色预览
if (PlayerSelection.IsForPlayer(GetOwningPlayerState()))
{
UpdateCharacterDisplay(PlayerSelection);
}
}
if (CGameState)
{
// 更新设置按钮是否可点击
StartHeroSelectionButton->SetIsEnabled(CGameState->CanStartHeroSelection());
}
// 更新队伍布局显示
if (PlayerTeamLayoutWidget)
{
PlayerTeamLayoutWidget->UpdatePlayerSelection(PlayerSelections);
}
}
创建UI
中间添加的间隔

在左边的边界,变换处的修剪


最后设置一下插槽类

放进大厅UI中,设置一下


创建游戏实例
MGameInstance

#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MGameInstance.generated.h"
/**
* 自定义游戏实例类 - 管理游戏全局状态和在线服务
* 功能:
* - 玩家登录和身份验证
* - 会话创建、搜索和加入
* - 服务器协调和匹配
* - 关卡管理和切换
*/
UCLASS()
class CRUNCH_API UMGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
// 开始匹配(服务器端调用)
void StartMatch();
private:
// 主菜单关卡引用
UPROPERTY(EditDefaultsOnly, Category = "Map")
TSoftObjectPtr<UWorld> MainMenuLevel;
// 大厅关卡引用
UPROPERTY(EditDefaultsOnly, Category = "Map")
TSoftObjectPtr<UWorld> LobbyLevel;
// 游戏关卡引用
UPROPERTY(EditDefaultsOnly, Category = "Map")
TSoftObjectPtr<UWorld> GameLevel;
// 加载关卡并监听连接
void LoadLevelAndListen(TSoftObjectPtr<UWorld> Level);
};
#include "MGameInstance.h"
void UMGameInstance::StartMatch()
{
// 检查是否为专用服务器或监听服务器, 在服务器中切地图
if (GetWorld()->GetNetMode() == ENetMode::NM_DedicatedServer || GetWorld()->GetNetMode() == ENetMode::NM_ListenServer)
{
// 加载游戏关卡并监听
LoadLevelAndListen(GameLevel);
}
}
void UMGameInstance::LoadLevelAndListen(TSoftObjectPtr<UWorld> Level)
{
// 从软引用的 UWorld 获取包路径(比如 /Game/Maps/MyMap.MyMap)
const FName LevelURL = FName(*FPackageName::ObjectPathToPackageName(Level.ToString()));
if (LevelURL != "")
{
// 切换到指定关卡,并加上 "?listen" 参数
GetWorld()->ServerTravel(LevelURL.ToString() + "?listen");
}
}
enum ENetMode : int
{
NM_Standalone, // 单机模式(没有联网)
NM_DedicatedServer, // 专用服务器(Dedicated Server)
NM_ListenServer, // 监听服务器(Listen Server,本地当服务器+客户端)
NM_Client, // 客户端
NM_MAX // NM_MAX:枚举上限(不是实际的网络模式,用来做边界/有效性检查)。
};
1. ServerTravel
ServerTravel是 服务器端切换关卡的函数。- 它不仅仅是本地加载地图,还会通知所有连接到服务器的客户端一起切换到新地图。
- 本质上它会把玩家迁移到新的地图,并维持现有的网络连接。
2. ?listen
-
这是 URL 参数,UE里很多命令(包括
OpenLevel和ServerTravel)都用?参数来附加选项。 -
?listen的作用:告诉引擎 在切换关卡后,让当前进程作为一个 Listen Server(监听服务器)运行。- Listen Server = 一边当服务器,一边自己也能当客户端玩。
- 如果没有
?listen,那只是单机加载关卡,客户端们无法连接进来。
ServerTravel= 服务器换地图 + 客户端跟随?listen= 指定成为 Listen Server,否则只是单机加载
创建蓝图游戏实例

项目中设置游戏实例
设置为刚刚创建好的游戏实例

添加开始游戏按钮
// 开始游戏按钮
UPROPERTY(meta=(BindWidget))
TObjectPtr<UButton> StartMatchButton;
UFUNCTION()
void StartMatchButtonClicked();
void ULobbyWidget::NativeConstruct()
{
Super::NativeConstruct();
ClearAndPopulateTeamSelectionSlots();
// 配置游戏状态
ConfigureGameState();
// 获取玩家控制器
LobbyPlayerController = GetOwningPlayer<ALobbyPlayerController>();
if (LobbyPlayerController)
{
// 绑定英雄选择切换界面事件
LobbyPlayerController->OnSwitchToHeroSelection.BindUObject(this, &ULobbyWidget::SwitchToHeroSelection);
}
// 绑定开始英雄选择按钮事件
StartHeroSelectionButton->SetIsEnabled(false);
StartHeroSelectionButton->OnClicked.AddDynamic(this, &ULobbyWidget::StartHeroSelectionButtonClicked);
// 绑定开始比赛按钮事件
StartMatchButton->SetIsEnabled(false);
StartMatchButton->OnClicked.AddDynamic(this, &ULobbyWidget::StartMatchButtonClicked);
// 异步加载角色定义数据
UCAssetManager::Get().LoadCharacterDefinitions(FStreamableDelegate::CreateUObject(this, &ULobbyWidget::CharacterDefinitionLoaded));
if (CharacterSelectionTileView)
{
// 绑定角色选择事件
CharacterSelectionTileView->OnItemSelectionChanged().AddUObject(this, &ULobbyWidget::CharacterSelected);
}
SpawnCharacterDisplay(); // 生成角色预览Actor
}
void ULobbyWidget::UpdatePlayerSelectionDisplay(const TArray<FPlayerSelection>& PlayerSelections)
{
// 清空所有槽位显示
for (UTeamSelectionWidget* SelectionSlot : TeamSelectionSlots)
{
SelectionSlot->UpdateSlotInfo("Empty");
}
// 重置角色选择项的选中状态
for (UUserWidget* CharacterEntryAsWidget : CharacterSelectionTileView->GetDisplayedEntryWidgets())
{
if (UCharacterEntryWidget* CharacterEntryWidget = Cast<UCharacterEntryWidget>(CharacterEntryAsWidget))
{
CharacterEntryWidget->SetSelected(false);
}
}
// 更新每个玩家的槽位显示
for (const FPlayerSelection& PlayerSelection : PlayerSelections)
{
if (!PlayerSelection.IsValid())
continue;
// 更新槽位名称显示
TeamSelectionSlots[PlayerSelection.GetPlayerSlot()]->UpdateSlotInfo(PlayerSelection.GetPlayerNickName());
// 已选择的角色变成灰色让别人知道不能选了
if (UCharacterEntryWidget* SelectedEntry = CharacterSelectionTileView->GetEntryWidgetFromItem<UCharacterEntryWidget>(PlayerSelection.GetCharacterDefinition()))
{
SelectedEntry->SetSelected(true);
}
// 如果是当前玩家,更新角色预览
if (PlayerSelection.IsForPlayer(GetOwningPlayerState()))
{
UpdateCharacterDisplay(PlayerSelection);
}
}
if (CGameState)
{
// 更新设置按钮是否可点击
StartHeroSelectionButton->SetIsEnabled(CGameState->CanStartHeroSelection());
// 更新设置开始按钮是否可点击
StartMatchButton->SetIsEnabled(CGameState->CanStartMatch());
}
// 更新队伍布局显示
if (PlayerTeamLayoutWidget)
{
PlayerTeamLayoutWidget->UpdatePlayerSelection(PlayerSelections);
}
}
void ULobbyWidget::StartMatchButtonClicked()
{
if (LobbyPlayerController)
{
// 请求服务器开始比赛
LobbyPlayerController->Server_RequestStartMatch();
}
}
游戏状态ACGameState中添加开始比赛的检查
/**
* 检查是否可以开始比赛
* @return 是否满足开始比赛条件
*/
bool CanStartMatch() const;
bool ACGameState::CanStartMatch() const
{
// 遍历玩家选择数组,检查每个玩家是否已选择角色
for (const FPlayerSelection& PlayerSelection : PlayerSelectionArray)
{
if (PlayerSelection.GetCharacterDefinition() == nullptr)
{
return false;
}
}
return true;
}
大厅玩家控制器ALobbyPlayerController中请求游戏开始
/**
* 服务器端处理开始比赛请求
*/
UFUNCTION(Server, Reliable, WithValidation)
void Server_RequestStartMatch();
void ALobbyPlayerController::Server_RequestStartMatch_Implementation()
{
// 获取当前游戏实例
if (UMGameInstance* CGameInstance = GetGameInstance<UMGameInstance>())
{
// 启动比赛流程
CGameInstance->StartMatch();
}
}
bool ALobbyPlayerController::Server_RequestStartMatch_Validate()
{
return true;
}
添加按钮


游戏模式中设置一下生成的类
// 为控制器获取默认Pawn类
virtual UClass* GetDefaultPawnClassForController_Implementation(AController* Controller) override;
// 为玩家生成默认Pawn
virtual APawn* SpawnDefaultPawnFor_Implementation(AController* NewPlayer, AActor* StartSpot) override;
// 玩家备份Pawn
UPROPERTY(EditDefaultsOnly, Category = "Team")
TSubclassOf<APawn> BackupPawn;
UClass* ACGameMode::GetDefaultPawnClassForController_Implementation(AController* Controller)
{
// 获取玩家控制器对应的自定义玩家状态
AMPlayerState* MPlayerState = Controller->GetPlayerState<AMPlayerState>();
// 如果玩家状态存在,并且玩家已经选择了自己的 Pawn 类
if (MPlayerState && MPlayerState->GetSelectedPawnClass())
{
// 使用玩家选择的 Pawn 类
return MPlayerState->GetSelectedPawnClass();
}
// 否则返回备用 Pawn(BackupPawn)
return BackupPawn;
}
APawn* ACGameMode::SpawnDefaultPawnFor_Implementation(AController* NewPlayer, AActor* StartSpot)
{
IGenericTeamAgentInterface* NewPlayerTeamInterface = Cast<IGenericTeamAgentInterface>(NewPlayer);
// 获取团队ID
FGenericTeamId TeamId = GetTeamIDForPlayer(NewPlayer);
if (NewPlayerTeamInterface)
{
// 设置团队ID
NewPlayerTeamInterface->SetGenericTeamId(TeamId);
}
// 分配出生点
StartSpot = FindNextStartSpotForTeam(TeamId);
// 设置玩家控制器的出生点
NewPlayer->StartSpot = StartSpot;
return Super::SpawnDefaultPawnFor_Implementation(NewPlayer, StartSpot);
}
FGenericTeamId ACGameMode::GetTeamIDForPlayer(const AController* InController) const
{
// 获取玩家状态
AMPlayerState* MPlayerState = InController->GetPlayerState<AMPlayerState>();
// 如果玩家状态存在,并且玩家已选择 Pawn
if (MPlayerState && MPlayerState->GetSelectedPawnClass())
{
// 根据玩家在队伍槽位信息计算队伍 ID
return MPlayerState->GetTeamIdBasedOnSlot();
}
// 没有玩家状态时,简单轮流分配队伍
static int PlayerCount = 0;
++PlayerCount;
return FGenericTeamId(PlayerCount % 2);
}
设置一个默认类

独有游戏进程运行


&spm=1001.2101.3001.5002&articleId=150529181&d=1&t=3&u=c79925a4e87340c1907316b59b88f0b7)
146

被折叠的 条评论
为什么被折叠?



