UE5多人MOBA+GAS 50、英雄选择(一)

该文章已生成可运行项目,


切换到英雄选择界面

大厅UI中ULobbyWidget

	// 英雄选择根节点
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UWidget> HeroSelectionRoot;
	
	// 开始英雄选择按钮点击事件处理
	UFUNCTION()
	void StartHeroSelectionButtonClicked();
	// 切换到英雄选择界面
	void SwitchToHeroSelection();
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);
}

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

	if (CGameState)
	{
		// 更新设置按钮是否可点击
		StartHeroSelectionButton->SetIsEnabled(CGameState->CanStartHeroSelection());
	}
}

void ULobbyWidget::StartHeroSelectionButtonClicked()
{
	if (LobbyPlayerController)
	{
		// 请求服务器开始英雄选择流程
		LobbyPlayerController->Server_StartHeroSelection();
	}
}

void ULobbyWidget::SwitchToHeroSelection()
{
	// 切换到英雄选择界面
	MainSwitcher->SetActiveWidget(HeroSelectionRoot);
}

大厅玩家控制器中添加英雄选择更新

/**
 * 玩家切换到英雄选择界面的委托声明
 * 当玩家控制器决定切换到英雄选择界面时触发
 */
DECLARE_DELEGATE(FOnSwitchToHeroSelection);
	/**
	 * 切换到英雄选择界面的委托实例
	 * 当服务器确认可以开始英雄选择时触发
	 */
	FOnSwitchToHeroSelection OnSwitchToHeroSelection;
	

	/**
	 * 服务器端处理英雄选择开始请求
	 */
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_StartHeroSelection();

	/**
	 * 服务器端处理英雄选择开始请求
	 */
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_StartHeroSelection();
	
	/**
	 * 客户端启动英雄选择流程
	 * 
	 * 网络调用:服务器确认后触发客户端切换界面
	 * 所有客户端收到调用后开始英雄选择
	 */
	UFUNCTION(Client, Reliable)
	void Client_StartHeroSelection();
void ALobbyPlayerController::Server_StartHeroSelection_Implementation()
{
	if (!HasAuthority() || !GetWorld()) return;

	// 遍历所有玩家控制器
	for (FConstPlayerControllerIterator PlayerControllerIterator = GetWorld()->GetPlayerControllerIterator(); PlayerControllerIterator; ++PlayerControllerIterator)
	{
		// 
		ALobbyPlayerController* PlayerController = Cast<ALobbyPlayerController>(*PlayerControllerIterator);
		if (PlayerController)
		{
			PlayerController->Client_StartHeroSelection();
		}
	}
}

bool ALobbyPlayerController::Server_StartHeroSelection_Validate()
{
	return true;
}

void ALobbyPlayerController::Client_StartHeroSelection_Implementation()
{
	// 触发界面切换委托
	OnSwitchToHeroSelection.ExecuteIfBound();
}

在这里插入图片描述

添加角色资产

PDA_CharacterDefinition
在这里插入图片描述

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GAS/Core/CGameplayAbilityTypes.h"
#include "PDA_CharacterDefinition.generated.h"

class ACCharacter;
class UGameplayAbility;
/**
 * 
 */
UCLASS()
class CRUNCH_API UPDA_CharacterDefinition : public UPrimaryDataAsset
{
	GENERATED_BODY()
public:
	// 获取当前数据资产的唯一标识符
	virtual FPrimaryAssetId GetPrimaryAssetId() const override;
	// 获取角色定义资产类型(用于资产管理)
	static FPrimaryAssetType GetCharacterDefinitionAssetType();

	// 获取角色显示名称
	FString GEtCharacterDisplayName() const { return CharacterName; }
	// 加载角色图标纹理
	UTexture2D* LoadIcon() const;
	// 加载角色蓝图类
	TSubclassOf<ACCharacter> LoadCharacterClass() const;
	// 加载显示用的动画蓝图
	TSubclassOf<UAnimInstance> LoadDisplayAnimationBP() const;
	// 加载显示用的骨骼网格
	class USkeletalMesh* LoadDisplayMesh() const;
	// 获取能力映射表(输入ID到技能类的映射)
	const TMap<ECAbilityInputID, TSubclassOf<UGameplayAbility>>* GetAbilities() const;

private:	
	// 角色显示名称
	UPROPERTY(EditDefaultsOnly, Category = "Character", meta = (DisplayName = "角色名称"))
	FString CharacterName;

	// 角色图标纹理
	UPROPERTY(EditDefaultsOnly, Category = "Character", meta = (DisplayName = "角色图像"))
	TSoftObjectPtr<UTexture2D> CharacterIcon;

	// 角色蓝图类
	UPROPERTY(EditDefaultsOnly, Category = "Character")
	TSoftClassPtr<ACCharacter> CharacterClass;

	// 显示用的动画蓝图
	UPROPERTY(EditDefaultsOnly, Category = "Character")
	TSoftClassPtr<UAnimInstance> DisplayAnimBP;
};
#include "PDA_CharacterDefinition.h"

#include "CCharacter.h"

FPrimaryAssetId UPDA_CharacterDefinition::GetPrimaryAssetId() const
{
	return FPrimaryAssetId(GetCharacterDefinitionAssetType(), GetFName());
}

FPrimaryAssetType UPDA_CharacterDefinition::GetCharacterDefinitionAssetType()
{
	return FPrimaryAssetType("CharacterDefinition");
}

UTexture2D* UPDA_CharacterDefinition::LoadIcon() const
{
	// 强制同步加载软引用资源
	CharacterIcon.LoadSynchronous();
	if (CharacterIcon.IsValid())
		return CharacterIcon.Get();

	return nullptr;
}

TSubclassOf<ACCharacter> UPDA_CharacterDefinition::LoadCharacterClass() const
{
	CharacterClass.LoadSynchronous();
	if (CharacterClass.IsValid())
		return CharacterClass.Get();
	
	return TSubclassOf<ACCharacter>();

}

TSubclassOf<UAnimInstance> UPDA_CharacterDefinition::LoadDisplayAnimationBP() const
{
	DisplayAnimBP.LoadSynchronous();
	if (DisplayAnimBP.IsValid())
		return DisplayAnimBP.Get();

	return TSubclassOf<UAnimInstance>();
}

class USkeletalMesh* UPDA_CharacterDefinition::LoadDisplayMesh() const
{
	// 加载角色蓝图类
	TSubclassOf<ACCharacter> LoadedCharacterClass = LoadCharacterClass();
	if (!LoadedCharacterClass)
		return nullptr;

	// 获取默认角色实例(仅用于资产获取)
	ACharacter* Character = Cast<ACharacter>(LoadedCharacterClass.GetDefaultObject());
	if (!Character)
		return nullptr;

	// 提取骨骼网格资产
	return Character->GetMesh()->GetSkeletalMeshAsset();
}

const TMap<ECAbilityInputID, TSubclassOf<UGameplayAbility>>* UPDA_CharacterDefinition::GetAbilities() const
{
	// 加载角色蓝图类
	TSubclassOf<ACCharacter> LoadedCharacterClass = LoadCharacterClass();
	if (!LoadedCharacterClass)
		return nullptr;

	// 获取角色默认对象
	ACCharacter* Character = Cast<ACCharacter>(LoadedCharacterClass.GetDefaultObject());
	if (!Character)
		return nullptr;

	// 返回能力映射表指针(直接访问角色内部数据)
	return &(Character->GetAbilities());
}

资产管理器中添加

	/**
	 * 异步加载所有角色定义资产
	 * @param LoadFinishedCallback - 加载完成时执行的回调
	 */
	void LoadCharacterDefinitions(const FStreamableDelegate& LoadFinishedCallback);
	
	/**
	 * 获取已加载的角色定义资产
	 * @param LoadedCharacterDefinitions - 输出加载的角色定义数组
	 * @return 是否成功获取
	 */
	bool GetLoadedCharacterDefinitions(TArray<UPDA_CharacterDefinition*>& LoadedCharacterDefinitions) const;
void UCAssetManager::LoadCharacterDefinitions(const FStreamableDelegate& LoadFinishedCallback)
{
	// 使用主资产类型加载角色定义资产
	LoadPrimaryAssetsWithType(
		UPDA_CharacterDefinition::GetCharacterDefinitionAssetType(), // 角色定义资产类型
		TArray<FName>(),                                           // 无特定资产名称
		LoadFinishedCallback                                   // 加载完成回调
	);
}

bool UCAssetManager::GetLoadedCharacterDefinitions(TArray<UPDA_CharacterDefinition*>& LoadedCharacterDefinitions) const
{
	TArray<UObject*> LoadedObjects;
	// 获取指定类型的主资产对象列表
	bool bLoaded = GetPrimaryAssetObjectList(
		UPDA_CharacterDefinition::GetCharacterDefinitionAssetType(), 
		LoadedObjects
	);

	if (bLoaded)
	{
		// 将加载的对象转换为角色定义类型
		for (UObject* LoadedObject : LoadedObjects)
		{
			LoadedCharacterDefinitions.Add(Cast<UPDA_CharacterDefinition>(LoadedObject));
		}
	}

	return bLoaded;
}

创建提供选择的角色的UI类

CharacterEntryWidget
在这里插入图片描述

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/IUserObjectListEntry.h"
#include "Blueprint/UserWidget.h"
#include "CharacterEntryWidget.generated.h"

class UTextBlock;
class UImage;
class UPDA_CharacterDefinition;
/**
 * 角色列表条目控件
 * 用于在角色选择界面中显示单个角色的图标和名称
 */
UCLASS()
class CRUNCH_API UCharacterEntryWidget : public UUserWidget, public IUserObjectListEntry
{
	GENERATED_BODY()
public:
	// 当列表项绑定数据对象时调用
	virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;

	// 获取当前绑定的角色定义数据
	FORCEINLINE const UPDA_CharacterDefinition* GetCharacterDefinition() const { return CharacterDefinition; }
	
	// 设置条目选中状态
	void SetSelected(bool bIsSelected);
private:	
	// 角色图标控件
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UImage> CharacterIcon;

	// 角色名称文本控件
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> CharacterNameText;

	// 材质参数名称:图标纹理参数(用于动态材质调整)
	UPROPERTY(EditDefaultsOnly, Category = "Character")
	FName IconTextureMatParamName = "Icon";

	// 材质参数名称:饱和度参数(用于选中状态高亮)
	UPROPERTY(EditDefaultsOnly, Category = "Character")
	FName SaturationMatParamName = "Saturation";

	// 当前绑定的角色定义数据
	UPROPERTY()
	const UPDA_CharacterDefinition* CharacterDefinition;
};
#include "CharacterEntryWidget.h"

#include "Character/PDA_CharacterDefinition.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"

void UCharacterEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
{
	IUserObjectListEntry::NativeOnListItemObjectSet(ListItemObject);

	CharacterDefinition= Cast<UPDA_CharacterDefinition>(ListItemObject);
	if (CharacterDefinition)
	{
		// 设置图标
		CharacterIcon->GetDynamicMaterial()->SetTextureParameterValue(IconTextureMatParamName, CharacterDefinition->LoadIcon());
		// 设置名称
		CharacterNameText->SetText(FText::FromString(CharacterDefinition->GEtCharacterDisplayName()));
	}
}

void UCharacterEntryWidget::SetSelected(bool bIsSelected)
{
	CharacterIcon->GetDynamicMaterial()->SetScalarParameterValue(SaturationMatParamName, bIsSelected ? 0.f : 1.f);
}

	// 角色选择列表
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UTileView> CharacterSelectionTileView;
	// 角色定义加载完成回调,设置角色选择列表项
	void CharacterDefinitionLoaded();
	
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);
	// 异步加载角色定义数据
	UCAssetManager::Get().LoadCharacterDefinitions(FStreamableDelegate::CreateUObject(this, &ULobbyWidget::CharacterDefinitionLoaded));
}

void ULobbyWidget::CharacterDefinitionLoaded()
{
	TArray<UPDA_CharacterDefinition*> LoadedCharacterDefinitions;
	// 获取已加载的角色定义
	if (UCAssetManager::Get().GetLoadedCharacterDefinitions(LoadedCharacterDefinitions))
	{
		// 设置角色选择列表数据
		CharacterSelectionTileView->SetListItems(LoadedCharacterDefinitions);
	}
}

创建简单动画蓝图
在这里插入图片描述
添加一个入场动画
在这里插入图片描述

添加资产
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

创建材质
在这里插入图片描述
创建角色UI
在这里插入图片描述
大厅UI中添加
在这里插入图片描述
在这里插入图片描述

实现选人逻辑

FPlayerSelection

	/**
	 * 获取玩家选择的角色资产
	 * @return 玩家选择的角色资产
	 */
	FORCEINLINE const UPDA_CharacterDefinition* GetCharacterDefinition() const { return CharacterDefinition; }

	/**
	 * 设置玩家选择的角色资产
	 * @param NewCharacterDefinition 新的角色资产
	 */
	FORCEINLINE void SetCharacterDefinition(const UPDA_CharacterDefinition* NewCharacterDefinition) { CharacterDefinition = NewCharacterDefinition; }
	// 玩家选择的角色资产
	UPROPERTY()
	TObjectPtr<const UPDA_CharacterDefinition> CharacterDefinition;

游戏状态中ACGameState

	/**
	 * 设置角色选择
	 * @param SelectingPlayer 正在选择的玩家状态
	 * @param SelectedDefinition 选择的角色定义
	 */
	void SetCharacterSelected(const APlayerState* SelectingPlayer, const UPDA_CharacterDefinition* SelectedDefinition);
	/**
	 * 检查角色是否已被选择
	 * @param Definition 要检查的角色定义
	 * @return 是否已被选择
	 */
	bool IsDefinitionSelected(const UPDA_CharacterDefinition* Definition) const;
	
	/**
	 * 取消角色选择
	 * @param DefinitionToDeselect 要取消选择的角色定义
	 */
	void SetCharacterDeselected(const UPDA_CharacterDefinition* DefinitionToDeselect);

void ACGameState::SetCharacterSelected(const APlayerState* SelectingPlayer,
	const UPDA_CharacterDefinition* SelectedDefinition)
{
	// 检查角色是否已被选择
	if (IsDefinitionSelected(SelectedDefinition)) return;

	// 查找玩家选择条目
	FPlayerSelection* FoundPlayerSelection = PlayerSelectionArray.FindByPredicate(
		[&](const FPlayerSelection& PlayerSelection)
		{
			return PlayerSelection.IsForPlayer(SelectingPlayer);
		}
	);

	if (FoundPlayerSelection)
	{
		// 更新角色定义
		FoundPlayerSelection->SetCharacterDefinition(SelectedDefinition);
		// 广播更新
		OnPlayerSelectionUpdated.Broadcast(PlayerSelectionArray);
	}
}
bool ACGameState::IsDefinitionSelected(const UPDA_CharacterDefinition* Definition) const
{
	// 遍历玩家选择数组检查指定角色是否已被选择
	const FPlayerSelection* FoundPlayerSelection = PlayerSelectionArray.FindByPredicate(
		[&](const FPlayerSelection& PlayerSelection)
		{
			return PlayerSelection.GetCharacterDefinition() == Definition;
		}
	);

	return FoundPlayerSelection != nullptr;
}

void ACGameState::SetCharacterDeselected(const UPDA_CharacterDefinition* DefinitionToDeselect)
{
	if (!DefinitionToDeselect) return;

	// 查找对应的角色选择条目
	FPlayerSelection* FoundPlayerSelection = PlayerSelectionArray.FindByPredicate(
		[&](const FPlayerSelection& PlayerSelection)
		{
			return PlayerSelection.GetCharacterDefinition() == DefinitionToDeselect;
		}
	);

	if (FoundPlayerSelection)
	{
		// 置空角色定义
		FoundPlayerSelection->SetCharacterDefinition(nullptr);
		// 广播更新
		OnPlayerSelectionUpdated.Broadcast(PlayerSelectionArray);
	}
}

到玩家状态中存储玩家的选择

#pragma once

#include "CoreMinimal.h"
#include "GenericTeamAgentInterface.h"
#include "PlayerInfoTypes.h"
#include "GameFramework/PlayerState.h"
#include "MPlayerState.generated.h"

class ACGameState;
class UPDA_CharacterDefinition;
/**
 * 自定义玩家状态类,存储玩家在游戏中的个性化状态数据
 * 包括角色选择、队伍分配等信息(网络同步)
 */
UCLASS()
class CRUNCH_API AMPlayerState : public APlayerState
{
	GENERATED_BODY()
public:
	AMPlayerState();
	// 网络复制属性
	virtual void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > &OutLifetimeProps) const override;
	virtual void BeginPlay() override;
	// 玩家状态复制
	virtual void CopyProperties(APlayerState* PlayerState) override;

	// 获取玩家选择的角色Pawn类
	TSubclassOf<APawn> GetSelectedPawnClass() const;
	
	// 根据玩家槽位分配队伍ID (0队或1队)
	FGenericTeamId GetTeamIdBasedOnSlot() const;

	// 服务器RPC:设置玩家选择的角色定义
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_SetSelectedCharacterDefinition(const UPDA_CharacterDefinition* NewDefinition);

private:	
	// 玩家选择信息(角色、槽位等),网络同步
	UPROPERTY(Replicated)
	FPlayerSelection PlayerSelection;

	// 指向游戏状态的引用
	UPROPERTY()	
	TObjectPtr<ACGameState> CGameState;

	// 当游戏状态中的玩家选择更新时调用
	void PlayerSelectionUpdated(const TArray<FPlayerSelection>& NewPlayerSelections);
};
#include "MPlayerState.h"

#include "Character/CCharacter.h"
#include "Character/PDA_CharacterDefinition.h"
#include "Framework/CGameState.h"
#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"
#include "Network/TNetStatics.h"

AMPlayerState::AMPlayerState()
{
	bReplicates = true;          // 开启网络复制
	NetUpdateFrequency = 100.f;  // 网络更新频率
}

void AMPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(AMPlayerState, PlayerSelection);
}

void AMPlayerState::BeginPlay()
{
	Super::BeginPlay();
	// 获取游戏状态引用
	CGameState = Cast<ACGameState>(UGameplayStatics::GetGameState(this));

	// 绑定玩家选择更新事件
	if (CGameState)
	{
		CGameState->OnPlayerSelectionUpdated.AddUObject(this, &AMPlayerState::PlayerSelectionUpdated);
	}
}

void AMPlayerState::CopyProperties(APlayerState* PlayerState)
{
	Super::CopyProperties(PlayerState);
	// 将当前玩家的选择状态复制到新玩家状态
	if (AMPlayerState* NewPlayerState = Cast<AMPlayerState>(PlayerState))
	{
		NewPlayerState->PlayerSelection = PlayerSelection;
	}
}

TSubclassOf<APawn> AMPlayerState::GetSelectedPawnClass() const
{
	// 如果已选择角色,加载对应的角色类
	if (PlayerSelection.GetCharacterDefinition())
	{
		return PlayerSelection.GetCharacterDefinition()->LoadCharacterClass();
	}
	return nullptr;
}

FGenericTeamId AMPlayerState::GetTeamIdBasedOnSlot() const
{
	return PlayerSelection.GetPlayerSlot() < UTNetStatics::GetPlayerCountPerTeam() 
		? FGenericTeamId{ 0 }  // 队伍0
		: FGenericTeamId{ 1 }; // 队伍1
}

void AMPlayerState::Server_SetSelectedCharacterDefinition_Implementation(const UPDA_CharacterDefinition* NewDefinition)
{
	// 安全检查
	if (!CGameState || !NewDefinition) return;
	
	// 如果角色已被其他玩家选择则退出
	if (CGameState->IsDefinitionSelected(NewDefinition)) return;
	
	// 如果玩家已有选择,先取消旧选择
	if (PlayerSelection.GetCharacterDefinition())
	{
		CGameState->SetCharacterDeselected(PlayerSelection.GetCharacterDefinition());
	}
	
	// 更新选择并通知游戏状态
	PlayerSelection.SetCharacterDefinition(NewDefinition);
	CGameState->SetCharacterSelected(this, NewDefinition);
}

bool AMPlayerState::Server_SetSelectedCharacterDefinition_Validate(const UPDA_CharacterDefinition* NewDefinition)
{
	return true;
}

void AMPlayerState::PlayerSelectionUpdated(const TArray<FPlayerSelection>& NewPlayerSelections)
{
	// 在更新列表中找到当前玩家的选择数据
	for (const FPlayerSelection& NewPlayerSelection : NewPlayerSelections)
	{
		if (NewPlayerSelection.IsForPlayer(this))
		{
			// 更新本地玩家选择状态
			PlayerSelection = NewPlayerSelection;
		}
	}
}

大厅UI中存储玩家状态

	// 该大厅UI拥有者的玩家状态
	UPROPERTY()
	TObjectPtr<AMPlayerState> MPlayerState;
	// 角色选择列表项点击事件处理
	void CharacterSelected(UObject* SelectedUObject);

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);
	// 异步加载角色定义数据
	UCAssetManager::Get().LoadCharacterDefinitions(FStreamableDelegate::CreateUObject(this, &ULobbyWidget::CharacterDefinitionLoaded));

	if (CharacterSelectionTileView)
	{
		// 绑定角色选择事件
		CharacterSelectionTileView->OnItemSelectionChanged().AddUObject(this, &ULobbyWidget::CharacterSelected);
	}
}

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 (CGameState)
	{
		// 更新设置按钮是否可点击
		StartHeroSelectionButton->SetIsEnabled(CGameState->CanStartHeroSelection());
	}
}

void ULobbyWidget::CharacterSelected(UObject* SelectedUObject)
{
	if (!MPlayerState)
	{
		MPlayerState = GetOwningPlayerState<AMPlayerState>();
	}

	if (!MPlayerState) return;
	
	// 获取选择的角色定义
	if (const UPDA_CharacterDefinition* SelectedCharacterDefinition = Cast<UPDA_CharacterDefinition>(SelectedUObject))
	{
		// 通知服务器更新角色选择
		MPlayerState->Server_SetSelectedCharacterDefinition(SelectedCharacterDefinition);
	}
}

创建展示用的Actor

CharacterDisplay
在这里插入图片描述

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CharacterDisplay.generated.h"

class UCameraComponent;
class UPDA_CharacterDefinition;

/**
 * 角色展示Actor - 用于在角色选择界面中展示3D角色模型
 */
UCLASS()
class CRUNCH_API ACharacterDisplay : public AActor
{
	GENERATED_BODY()

public:	
	// 构造函数
	ACharacterDisplay();
	
	/**
	 * 配置角色展示
	 * @param CharacterDefinition 角色定义数据资产,包含要展示的模型和动画信息
	 */
	void ConfigureWithCharacterDefinition(const UPDA_CharacterDefinition* CharacterDefinition);
protected:
	// 角色网格组件 - 用于显示角色模型
	UPROPERTY(VisibleDefaultsOnly, BlueprintReadWrite, Category = "Character Display")
	TObjectPtr<USkeletalMeshComponent> MeshComponent;
private:	

	// 摄像机组件 - 用于控制角色展示的视角
	UPROPERTY(VisibleDefaultsOnly, Category = "Character Display")
	TObjectPtr<UCameraComponent> ViewCameraComponent;
};

#include "CharacterDisplay.h"

#include "Camera/CameraComponent.h"
#include "Character/PDA_CharacterDefinition.h"
#include "Components/SkeletalMeshComponent.h"

// Sets default values
ACharacterDisplay::ACharacterDisplay()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	// 创建根组件(场景组件)
	SetRootComponent(CreateDefaultSubobject<USceneComponent>("Root Comp"));

	// 创建模型组件
	MeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>("Mesh Component");
	MeshComponent->SetupAttachment(RootComponent);

	// 创建摄像机组件并附加到根组件
	ViewCameraComponent = CreateDefaultSubobject<UCameraComponent>("View Camera Component");
	ViewCameraComponent->SetupAttachment(GetRootComponent());
}

void ACharacterDisplay::ConfigureWithCharacterDefinition(const UPDA_CharacterDefinition* CharacterDefinition)
{
	// 安全检查
	if (!CharacterDefinition) return;
	MeshComponent->SetRelativeRotation(FRotator(0.0f, 0.0f, 0.0f));

	// 加载并设置角色展示网格
	MeshComponent->SetSkeletalMesh(CharacterDefinition->LoadDisplayMesh());
	
	// 配置动画模式为蓝图驱动
	MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
	
	// 加载并设置展示动画蓝图
	MeshComponent->SetAnimClass(CharacterDefinition->LoadDisplayAnimationBP());
}

设置一下大厅玩家控制器的构造函数

ALobbyPlayerController::ALobbyPlayerController()
{
	// 设置bAutoManageActiveCameraTarget为false,表示不自动管理摄像机目标(由大厅逻辑手动控制)
	bAutoManageActiveCameraTarget = false;
}

大厅UI中添加Actor,顺便把技能一起放进去

	// 展示的角色类
	UPROPERTY(EditDefaultsOnly, Category = "Character Display")
	TSubclassOf<ACharacterDisplay> CharacterDisplayClass;
	// 存储用来展示的角色
	UPROPERTY()
	TObjectPtr<ACharacterDisplay> CharacterDisplay;
	// 创建角色展示
	void SpawnCharacterDisplay();
	// 更新角色展示
	void UpdateCharacterDisplay(const FPlayerSelection& PlayerSelection);

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);
	// 异步加载角色定义数据
	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());
	}
}

void ULobbyWidget::SpawnCharacterDisplay()
{
	// 已经生成或者未定义角色展示类
	if (CharacterDisplay || !CharacterDisplayClass) return;

	// 设置预览角色的初始变换
	FTransform CharacterDisplayTransform = FTransform::Identity;

	// 获取玩家出生点位置
	AActor* PlayerStart = UGameplayStatics::GetActorOfClass(GetWorld(), APlayerStart::StaticClass());
	if (PlayerStart)
	{
		// 找到玩家出生点,将出生点设置为角色展示的初始变换
		CharacterDisplayTransform = PlayerStart->GetActorTransform();
	}
	
	// 设置生成参数
	FActorSpawnParameters SpawnParams;
	SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	// 生成预览角色
	CharacterDisplay = GetWorld()->SpawnActor<ACharacterDisplay>(CharacterDisplayClass, CharacterDisplayTransform, SpawnParams);
	// 设置玩家视角到预览角色
	GetOwningPlayer()->SetViewTarget(CharacterDisplay);
}

void ULobbyWidget::UpdateCharacterDisplay(const FPlayerSelection& PlayerSelection)
{
	if (!PlayerSelection.GetCharacterDefinition())
		return;

	// 配置角色预览
	CharacterDisplay->ConfigureWithCharacterDefinition(PlayerSelection.GetCharacterDefinition());
	// 清空现有技能列表
	AbilityListView->ClearListItems();
	// 获取技能映射
	if (const TMap<ECAbilityInputID, TSubclassOf<UGameplayAbility>>* Abilities = PlayerSelection.GetCharacterDefinition()->GetAbilities())
	{
		// 配置技能列表
		AbilityListView->ConfigureAbilities(*Abilities);
	}
}

创建展览Actor 的蓝图版本
在这里插入图片描述
大厅中设置一下类
在这里插入图片描述
技能框把主游戏UI的Ctrl+C过来
在这里插入图片描述
在这里插入图片描述
锚点用这个
在这里插入图片描述
技能的
在这里插入图片描述
在这里插入图片描述

给技能UI复制一个新的,把等级用尺寸框给框住,压扁它
在这里插入图片描述
复制一个新的材质实例这里设置为1
在这里插入图片描述
给新的大厅技能用
最后配置一下
在这里插入图片描述
另外由于这个技能图标这里我当初没有对asc进行安全检查导致运行的时候会崩掉
在这里插入图片描述
就稍微改了一下
在这里插入图片描述
另外很多游戏在角色展览的时候可以让角色转起来,我在蓝图中粗糙的也加了这个东西
在这里插入图片描述

添加技能提示条

AbilityToolTip
在这里插入图片描述

#pragma once

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

class UImage;
class UTextBlock;
/**
 * 
 */
UCLASS()
class CRUNCH_API UAbilityToolTip : public UUserWidget
{
	GENERATED_BODY()
public:
	void SetAbilityInfo(const FName& AbilityName, UTexture2D* AbilityTexture, const FText& AbilityDescription, float AbilityCooldown, float AbilityCost);

private:
	// 技能名称
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UTextBlock> AbilityNameText;

	// 技能图标
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UImage> AbilityIcon;

	// 技能描述
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UTextBlock> AbilityDescriptionText;

	// 技能冷却
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UTextBlock> AbilityCooldownText;

	// 技能消耗
	UPROPERTY(meta=(BindWidget))	
	TObjectPtr<UTextBlock> AbilityCostText;
};

#include "AbilityToolTip.h"

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

void UAbilityToolTip::SetAbilityInfo(const FName& AbilityName, UTexture2D* AbilityTexture,
                                     const FText& AbilityDescription, float AbilityCooldown, float AbilityCost)
{
	AbilityNameText->SetText(FText::FromName(AbilityName));
	AbilityIcon->SetBrushFromTexture(AbilityTexture);

	AbilityDescriptionText->SetText(AbilityDescription);

	FNumberFormattingOptions FormattingOptions;
	FormattingOptions.MaximumFractionalDigits = 0;

	AbilityCooldownText->SetText(FText::AsNumber(AbilityCooldown, &FormattingOptions));
	AbilityCostText->SetText(FText::AsNumber(AbilityCost, &FormattingOptions));
}

在这里插入图片描述
在技能UI中添加提示框

	// 技能信息提示框
	UPROPERTY(EditDefaultsOnly, Category = "Tool Tip")
	TSubclassOf<class UAbilityToolTip> AbilityToolTipClass;
	// 创建提示框
	void CreateToolTipWidget(const FAbilityWidgetData* AbilityWidgetData);
void UAbilityGauge::ConfigureWithWidgetData(const FAbilityWidgetData* WidgetData)
{
	if (Icon && WidgetData)
	{
		// 设置图片
		Icon->GetDynamicMaterial()->SetTextureParameterValue(IconMaterialParamName, WidgetData->Icon.LoadSynchronous());
		// 创建技能提示
		CreateToolTipWidget(WidgetData);
	}
}

void UAbilityGauge::CreateToolTipWidget(const FAbilityWidgetData* AbilityWidgetData)
{
	if (!AbilityWidgetData || !AbilityToolTipClass)
		return;

	UAbilityToolTip* InstantiatedToolTip = CreateWidget<UAbilityToolTip>(GetOwningPlayer(), AbilityToolTipClass);
	if (InstantiatedToolTip)
	{
		float CooldownDuration = UCAbilitySystemStatics::GetStaticCooldownDurationForAbility(AbilityCDO);
		float Cost = UCAbilitySystemStatics::GetStaticCostForAbility(AbilityCDO);
		InstantiatedToolTip->SetAbilityInfo(AbilityWidgetData->AbilityName, AbilityWidgetData->Icon.LoadSynchronous(), AbilityWidgetData->Description, CooldownDuration, Cost);

		SetToolTip(InstantiatedToolTip);
	}
}

设置一下
在这里插入图片描述
在这里插入图片描述
在原本的基础上我进行了些许新的微调
在这里插入图片描述
弄成填充好看一点点
在这里插入图片描述
技能提示信息添加新的函数

	void SetAbilityInfo(const FName& AbilityName, UTexture2D* AbilityTexture, const FText& AbilityDescription, const FText& AbilityCooldown, const FText& AbilityCost);

void UAbilityToolTip::SetAbilityInfo(const FName& AbilityName, UTexture2D* AbilityTexture,
	const FText& AbilityDescription, const FText& AbilityCooldown, const FText& AbilityCost)
{
	AbilityNameText->SetText(FText::FromName(AbilityName));
	AbilityIcon->SetBrushFromTexture(AbilityTexture);

	AbilityDescriptionText->SetText(AbilityDescription);

	AbilityCooldownText->SetText(AbilityCooldown);
	AbilityCostText->SetText(AbilityCost);
}

void UAbilityGauge::CreateToolTipWidget(const FAbilityWidgetData* AbilityWidgetData)
{
	if (!AbilityWidgetData || !AbilityToolTipClass)
		return;

	UAbilityToolTip* InstantiatedToolTip = CreateWidget<UAbilityToolTip>(GetOwningPlayer(), AbilityToolTipClass);
	if (InstantiatedToolTip)
	{
		const float CooldownDuration = UCAbilitySystemStatics::GetStaticCooldownDurationForAbility(AbilityCDO);
		const float Cost = UCAbilitySystemStatics::GetStaticCostForAbility(AbilityCDO);
		InstantiatedToolTip->SetAbilityInfo(AbilityWidgetData->AbilityName, AbilityWidgetData->Icon.LoadSynchronous(), AbilityWidgetData->Description, CooldownDuration, Cost);

		if (AbilityCDO)
		{
			FText Cost_Text;
			const UGameplayEffect* CostEffect = AbilityCDO->GetCostGameplayEffect();
			if (CostEffect && CostEffect->Modifiers.Num() > 0)
			{
				TArray<FString> CostLevels;
				float TempCost_1 = 0.f;
				for (int32 Level = 1; Level <= 5; ++Level)
				{
					float TempCost = 0.f;
					CostEffect->Modifiers[0].ModifierMagnitude.GetStaticMagnitudeIfPossible(Level, TempCost);
					if (TempCost == TempCost_1) break;
					TempCost_1 = TempCost;
					const int32 IntCost = static_cast<int32>(TempCost);
					CostLevels.Add(FString::Printf(TEXT("%d"), FMath::Abs(IntCost)));
				}
				if (CostLevels.Num() > 0)
				{
					// 使用斜杠连接所有等级
					const FString FinalCostString = FString::Join(CostLevels, TEXT("/"));
					Cost_Text = FText::FromString(FinalCostString);
				}
			}
			FText Cooldown_Text;
			if (const UCGameplayAbility* CastedAbility = Cast<UCGameplayAbility>(AbilityCDO))
			{
				TArray<FString> CooldownLevels;
				float TempCooldown_1 = 0.f;
				for (int32 Level = 1; Level <= 5; ++Level)
				{
					const float TempCooldown = CastedAbility->CooldownDuration.GetValueAtLevel(Level);
					if (TempCooldown == TempCooldown_1) break;
					TempCooldown_1 = TempCooldown;
					const int32 IntCooldown = static_cast<int32>(TempCooldown);
					CooldownLevels.Add(FString::Printf(TEXT("%d"), FMath::Abs(IntCooldown)));
				}
				if (CooldownLevels.Num() > 0)
				{
					// 使用斜杠连接所有等级
					const FString FinalCooldownString = FString::Join(CooldownLevels, TEXT("/"));
					Cooldown_Text = FText::FromString(FinalCooldownString);
				}
			}
			InstantiatedToolTip->SetAbilityInfo(AbilityWidgetData->AbilityName, AbilityWidgetData->Icon.LoadSynchronous(), AbilityWidgetData->Description, Cooldown_Text, Cost_Text);
		}
		SetToolTip(InstantiatedToolTip);
	}
}
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值