UE5多人MOBA+GAS 51、英雄选择(二)


创建队伍的选择英雄显示

创建队伍插槽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里很多命令(包括 OpenLevelServerTravel)都用 ?参数 来附加选项。

  • ?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);
}

设置一个默认类
在这里插入图片描述
独有游戏进程运行
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值