切换到英雄选择界面
大厅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, &layerState::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);
}
}
&spm=1001.2101.3001.5002&articleId=150489354&d=1&t=3&u=a2124255f71d4588868389a92f1c58f8)
929

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



