UE5多人MOBA+GAS 50、英雄选择(一)
文章目录
- 切换到英雄选择界面
- 添加角色资产
- 创建提供选择的角色的UI类
- 实现选人逻辑
- 创建展示用的Actor
- 添加技能提示条
切换到英雄选择界面
大厅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);}
}