![](https://habrastorage.org/getpro/habr/upload_files/c3d/6cf/a6a/c3d6cfa6a99551e0250eb489b2146d93.png)
Для своей новой игры Code Of Person (Как-нибудь в следующих статьях про её разработку расскажу) я решил использовать и расширить плагин Gameplay Ability System.
В серии статьей я расскажу о моём процессе создания плагина Combat Abilities System, который расширяет возможности стандартного плагина Gameplay Ability System в Unreal Engine. Мы рассмотрим шаги разработки, архитектурные решения и особенности, которые делают наш плагин уникальным и полезным для создания боевых механик в играх.
В этой части покажу свою реализацию интеграции с Enhanced Input и Game Features.
Что такое Gameplay Ability System и для чего он нужен?
Gameplay Ability System — это комплексная система, предназначенная для создания и управления способностями. Она позволяет разработчикам создавать разнообразные способности, управлять их активацией, эффектами и взаимодействием с другими системами игры. Однако, базового функционала мне недостаточно, и я хотел сделать так чтобы можно переиспользовать свои разработки между проектами + заявить себя
Для себя я определил основные желания для разработки данной системы:
боевая система
система управление ввода игрока
модульность
Привязка к Enhanced Input
Для начала я создал класс UPlayerControlsComponent, который будет отвечать за функциональность настройками пользовательского ввода и управления через Enhanced Input
PlayerControlsComponent.h
#pragma once
#include "EnhancedInputComponent.h"
#include "Components/PawnComponent.h"
#include "PlayerControlsComponent.generated.h"
class UEnhancedInputLocalPlayerSubsystem;
class UInputAction;
class UInputMappingContext;
/**
*
*/
UCLASS(BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent))
class COMBATABILITIESSYSTEMRUNTIME_API UPlayerControlsComponent : public UPawnComponent
{
GENERATED_BODY()
public:
virtual void OnRegister() override;
virtual void OnUnregister() override;
protected:
UFUNCTION(BlueprintNativeEvent, Category="Player Controls")
void SetupPlayerControls(UEnhancedInputComponent* InPlayerInputComponent);
UFUNCTION(BlueprintNativeEvent, Category="Player Controls")
void TeardownPlayerControls(UEnhancedInputComponent* InPlayerInputComponent);
template<class UserClass, typename FuncType>
bool BindInputAction(const UInputAction* InAction, const ETriggerEvent InEventType, UserClass* InObject, FuncType InFunction)
{
if(ensure(InputComponent != nullptr) && ensure(InAction != nullptr))
{
InputComponent->BindAction(InAction, InEventType, InObject, InFunction);
return true;
}
return false;
}
UFUNCTION()
virtual void OnPawnRestarted(APawn* InPawn);
UFUNCTION()
virtual void OnControllerChanged(APawn* InPawn, AController* InOldController, AController* NewController);
virtual void SetupInputComponent(APawn* InPawn);
virtual void ReleaseInputComponent(AController* OldController = nullptr);
UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem(AController* OldController = nullptr) const;
UEnhancedInputComponent* GetInputComponent() const { return InputComponent;}
private:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Player Controls", meta=(AllowPrivateAccess="true"))
TObjectPtr<UInputMappingContext> InputMappingContext{nullptr};
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Player Controls", meta=(AllowPrivateAccess="true"))
int32 InputPriority{0};
UPROPERTY(Transient)
UEnhancedInputComponent* InputComponent;
};
UPlayerControlsComponent.cpp
#include "Components/PlayerControlsComponent.h"
#include "EnhancedInputSubsystems.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(PlayerControlsComponent)
void UPlayerControlsComponent::SetupPlayerControls_Implementation(UEnhancedInputComponent* InPlayerInputComponent)
{
}
void UPlayerControlsComponent::TeardownPlayerControls_Implementation(UEnhancedInputComponent* InPlayerInputComponent)
{
}
void UPlayerControlsComponent::OnRegister()
{
Super::OnRegister();
const UWorld* World {GetWorld()};
APawn* PawnOwner{GetPawn<APawn>()};
if(!ensure(PawnOwner) && !World->IsGameWorld()) return;
PawnOwner->ReceiveRestartedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnPawnRestarted);
PawnOwner->ReceiveControllerChangedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnControllerChanged);
if(PawnOwner->InputComponent)
{
OnPawnRestarted(PawnOwner);
}
}
void UPlayerControlsComponent::OnUnregister()
{
if(const UWorld* World {GetWorld()}; World && World->IsGameWorld())
{
ReleaseInputComponent();
if(auto* PawnOwner{GetPawn<APawn>()}; PawnOwner)
{
PawnOwner->ReceiveRestartedDelegate.RemoveAll(this);
PawnOwner->ReceiveControllerChangedDelegate.RemoveAll(this);
}
}
Super::OnUnregister();
}
void UPlayerControlsComponent::OnPawnRestarted(APawn* InPawn)
{
if(ensure(InPawn && InPawn == GetOwner()) && InPawn->InputComponent)
{
ReleaseInputComponent();
if(InPawn->InputComponent)
{
SetupInputComponent(InPawn);
}
}
}
void UPlayerControlsComponent::OnControllerChanged(APawn* InPawn, AController* InOldController,
AController* NewController)
{
if(ensure(InPawn && InPawn == GetOwner()) && InOldController)
{
ReleaseInputComponent(InOldController);
}
}
void UPlayerControlsComponent::SetupInputComponent(APawn* InPawn)
{
InputComponent = CastChecked<UEnhancedInputComponent>(InPawn->InputComponent);
UEnhancedInputLocalPlayerSubsystem* Subsystem = {GetEnhancedInputSubsystem()};
check(Subsystem);
if(InputMappingContext)
{
Subsystem->AddMappingContext(InputMappingContext, InputPriority);
}
SetupPlayerControls(InputComponent);
}
void UPlayerControlsComponent::ReleaseInputComponent(AController* OldController)
{
if(UEnhancedInputLocalPlayerSubsystem* Subsystem {GetEnhancedInputSubsystem(OldController)}; Subsystem && InputComponent)
{
TeardownPlayerControls(InputComponent);
if(InputMappingContext)
{
Subsystem->RemoveMappingContext(InputMappingContext);
}
}
InputComponent = nullptr;
}
UEnhancedInputLocalPlayerSubsystem* UPlayerControlsComponent::GetEnhancedInputSubsystem(
AController* OldController) const
{
const APlayerController* PlayerController {GetController<APlayerController>()};
if(!PlayerController)
{
PlayerController = Cast<APlayerController>(OldController);
if(!PlayerController)
{
return nullptr;
}
}
const ULocalPlayer* LocalPlayer {PlayerController->GetLocalPlayer()};
if(!LocalPlayer) return nullptr;
return LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
}
Далее нужно создать от компонента, описанного выше, дочерний класс UAbilityInputBindingComponent, который будет отвечать за привязку ввода и игровыми способностями:
AbilityInputBindingComponent.h
#pragma once
#include "GameplayAbilitySpec.h"
#include "GameplayAbilitySpecHandle.h"
#include "Components/PlayerControlsComponent.h"
#include "AbilityInputBindingComponent.generated.h"
class UAbilitySystemComponent;
USTRUCT()
struct FAbilityInputBinding
{
GENERATED_BODY()
int32 InputID{0};
uint32 OnPressedHandle{0};
uint32 OnReleasedHandle{0};
TArray<FGameplayAbilitySpecHandle> BoundAbilitiesStack;
};
UCLASS(meta=(BlueprintSpawnableComponent))
class COMBATABILITIESSYSTEMRUNTIME_API UAbilityInputBindingComponent : public UPlayerControlsComponent
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category="Abilities")
void SetInputBinding(UInputAction* InInputAction, FGameplayAbilitySpecHandle AbilitySpecHandle);
UFUNCTION(BlueprintCallable, Category="Abilities")
void ClearInputBinding(FGameplayAbilitySpecHandle InAbilityHandle);
UFUNCTION(BlueprintCallable, Category="Abilities")
void ClearAbilityBindings(UInputAction* InInputAction);
virtual void SetupPlayerControls_Implementation(UEnhancedInputComponent* InPlayerInputComponent) override;
virtual void ReleaseInputComponent(AController* OldController) override;
private:
void ResetBindings();
void RunAbilitySystemSetup();
void OnAbilityInputPressed(UInputAction* InInputAction);
void OnAbilityInputReleased(UInputAction* InInputAction);
void RemoveEntry(UInputAction* InInputAction);
FGameplayAbilitySpec* FindAbilitySpec(FGameplayAbilitySpecHandle InHandle);
void TryBindAbilityInput(UInputAction* InInputAction, FAbilityInputBinding& InAbilityInputBinding);
private:
UPROPERTY(Transient)
UAbilitySystemComponent* AbilityComponent;
UPROPERTY(Transient)
TMap<UInputAction*, FAbilityInputBinding> MappedAbilities;
};
AbilityInputBindingComponent.cpp
#include "Input/AbilityInputBindingComponent.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemGlobals.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AbilityInputBindingComponent)
namespace AbilityInputBindingComponent_Impl
{
constexpr int32 InvalidInputID{0};
int32 IncrementingInputID{InvalidInputID};
static int32 GetNextInputID()
{
return ++IncrementingInputID;
}
}
void UAbilityInputBindingComponent::SetInputBinding(UInputAction* InInputAction,
FGameplayAbilitySpecHandle AbilitySpecHandle)
{
FGameplayAbilitySpec* BindingAbility {FindAbilitySpec(AbilitySpecHandle)};
FAbilityInputBinding* AbilityInputBinding {MappedAbilities.Find(InInputAction)};
if(AbilityInputBinding)
{
if(FGameplayAbilitySpec* OldBoundAbility {FindAbilitySpec(AbilityInputBinding->BoundAbilitiesStack.Top())}; OldBoundAbility && OldBoundAbility->InputID == AbilityInputBinding->InputID)
{
OldBoundAbility->InputID = AbilityInputBindingComponent_Impl::InvalidInputID;
}
}
else
{
AbilityInputBinding = &MappedAbilities.Add(InInputAction);
AbilityInputBinding->InputID = AbilityInputBindingComponent_Impl::GetNextInputID();
}
if(BindingAbility)
{
BindingAbility->InputID = AbilityInputBinding->InputID;
}
AbilityInputBinding->BoundAbilitiesStack.Push(AbilitySpecHandle);
TryBindAbilityInput(InInputAction, *AbilityInputBinding);
}
void UAbilityInputBindingComponent::ClearInputBinding(FGameplayAbilitySpecHandle InAbilityHandle)
{
FGameplayAbilitySpec* FoundAbility {FindAbilitySpec(InAbilityHandle)};
if(!FoundAbility) return;
auto MappedIterator = MappedAbilities.CreateIterator();
while (MappedIterator)
{
if(MappedIterator.Value().InputID == FoundAbility->InputID)
{
break;
}
++MappedIterator;
}
if(!MappedIterator) return;
FAbilityInputBinding& AbilityInputBinding = MappedIterator.Value();
if(AbilityInputBinding.BoundAbilitiesStack.Remove(InAbilityHandle) > 0)
{
if(AbilityInputBinding.BoundAbilitiesStack.Num() > 0)
{
FGameplayAbilitySpec* StackedAbility {FindAbilitySpec(AbilityInputBinding.BoundAbilitiesStack.Top())};
if(StackedAbility && StackedAbility->InputID == 0)
{
StackedAbility->InputID = AbilityInputBinding.InputID;
}
}
else
{
RemoveEntry(MappedIterator.Key());
}
FoundAbility->InputID = AbilityInputBindingComponent_Impl::InvalidInputID;
}
}
void UAbilityInputBindingComponent::ClearAbilityBindings(UInputAction* InInputAction)
{
RemoveEntry(InInputAction);
}
void UAbilityInputBindingComponent::SetupPlayerControls_Implementation(UEnhancedInputComponent* InPlayerInputComponent)
{
for(auto& InputBinding : MappedAbilities)
{
if(auto* CurrentInputComponent {GetInputComponent()}; CurrentInputComponent)
{
CurrentInputComponent->RemoveBindingByHandle(InputBinding.Value.OnPressedHandle);
CurrentInputComponent->RemoveBindingByHandle(InputBinding.Value.OnReleasedHandle);
}
if(AbilityComponent)
{
const int32 ExpectedInputID = InputBinding.Value.InputID;
for(FGameplayAbilitySpecHandle AbilityHandle : InputBinding.Value.BoundAbilitiesStack)
{
if(FGameplayAbilitySpec* FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(AbilityHandle); FoundAbility && FoundAbility->InputID == ExpectedInputID)
{
FoundAbility->InputID = AbilityInputBindingComponent_Impl::InvalidInputID;
}
}
}
}
AbilityComponent = nullptr;
}
void UAbilityInputBindingComponent::ReleaseInputComponent(AController* OldController)
{
ResetBindings();
Super::ReleaseInputComponent(OldController);
}
void UAbilityInputBindingComponent::ResetBindings()
{
for(auto& InputBinding : MappedAbilities)
{
if(auto* CurrentInputComponent {GetInputComponent()}; CurrentInputComponent)
{
CurrentInputComponent->RemoveBindingByHandle(InputBinding.Value.OnPressedHandle);
CurrentInputComponent->RemoveBindingByHandle(InputBinding.Value.OnReleasedHandle);
}
if(AbilityComponent)
{
const int32 ExpectedInputID = InputBinding.Value.InputID;
for(FGameplayAbilitySpecHandle AbilitySpecHandle : InputBinding.Value.BoundAbilitiesStack)
{
FGameplayAbilitySpec* FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(AbilitySpecHandle);
if(FoundAbility && FoundAbility->InputID == ExpectedInputID)
{
FoundAbility->InputID = AbilityInputBindingComponent_Impl::InvalidInputID;
}
}
}
}
AbilityComponent = nullptr;
}
void UAbilityInputBindingComponent::RunAbilitySystemSetup()
{
const AActor* Owner {GetOwner()};
check(Owner);
AbilityComponent = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Owner);
if(!AbilityComponent) return;
for (auto& InputBinding : MappedAbilities)
{
const int32 NewInputID {AbilityInputBindingComponent_Impl::GetNextInputID()};
InputBinding.Value.InputID = NewInputID;
for(const FGameplayAbilitySpecHandle AbilityHandle : InputBinding.Value.BoundAbilitiesStack)
{
if(FGameplayAbilitySpec* FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(AbilityHandle); FoundAbility)
{
FoundAbility->InputID = NewInputID;
}
}
}
}
void UAbilityInputBindingComponent::OnAbilityInputPressed(UInputAction* InInputAction)
{
if(!AbilityComponent)
{
RunAbilitySystemSetup();
}
if(AbilityComponent)
{
if(const FAbilityInputBinding* FoundBinding {MappedAbilities.Find(InInputAction)}; FoundBinding && ensure(FoundBinding->InputID) != AbilityInputBindingComponent_Impl::InvalidInputID)
{
AbilityComponent->AbilityLocalInputPressed(FoundBinding->InputID);
}
}
}
void UAbilityInputBindingComponent::OnAbilityInputReleased(UInputAction* InInputAction)
{
if(!AbilityComponent) return;
if(const FAbilityInputBinding* FoundBinding {MappedAbilities.Find(InInputAction)}; FoundBinding && ensure(FoundBinding->InputID != AbilityInputBindingComponent_Impl::InvalidInputID))
{
AbilityComponent->AbilityLocalInputReleased(FoundBinding->InputID);
}
}
void UAbilityInputBindingComponent::RemoveEntry(UInputAction* InInputAction)
{
if(FAbilityInputBinding* Binding {MappedAbilities.Find(InInputAction)})
{
if(auto* CurrentInputComponent {GetInputComponent()}; CurrentInputComponent)
{
CurrentInputComponent->RemoveBindingByHandle(Binding->OnPressedHandle);
CurrentInputComponent->RemoveBindingByHandle(Binding->OnReleasedHandle);
}
for(FGameplayAbilitySpecHandle AbilityHandle : Binding->BoundAbilitiesStack)
{
FGameplayAbilitySpec* AbilitySpec = FindAbilitySpec(AbilityHandle);
if(AbilitySpec && AbilitySpec->InputID == Binding->InputID)
{
AbilitySpec->InputID = AbilityInputBindingComponent_Impl::InvalidInputID;
}
}
MappedAbilities.Remove(InInputAction);
}
}
FGameplayAbilitySpec* UAbilityInputBindingComponent::FindAbilitySpec(FGameplayAbilitySpecHandle InHandle)
{
FGameplayAbilitySpec* FoundAbility{nullptr};
if(AbilityComponent)
{
FoundAbility = AbilityComponent->FindAbilitySpecFromHandle(InHandle);
}
return FoundAbility;
}
void UAbilityInputBindingComponent::TryBindAbilityInput(UInputAction* InInputAction,
FAbilityInputBinding& InAbilityInputBinding)
{
if(auto* CurrentInputComponent {GetInputComponent()}; CurrentInputComponent)
{
if(InAbilityInputBinding.OnPressedHandle == 0)
{
InAbilityInputBinding.OnPressedHandle = CurrentInputComponent->BindAction(InInputAction, ETriggerEvent::Started, this, &UAbilityInputBindingComponent::OnAbilityInputPressed, InInputAction).GetHandle();
}
if(InAbilityInputBinding.OnReleasedHandle == 0)
{
InAbilityInputBinding.OnReleasedHandle = CurrentInputComponent->BindAction(InInputAction, ETriggerEvent::Completed, this, &UAbilityInputBindingComponent::OnAbilityInputReleased, InInputAction).GetHandle();
}
}
}
Game Features
Плагин Game Features помогает создавать отдельные функциональности, которые можно включать и выключать в зависимости от потребностей проекта, также помогает избегать лишних зависимостей.
После создания компонентов для контроля компонентов, нужно создать действие в Game Features, которое будет добавлять Mapping Context в общую субсистему пользовательского ввода.
AddInputContextMapping_GameFeatureAction.h
#pragma once
#include "CoreMinimal.h"
#include "GameFeature/GameFeature_WorldActionBase.h"
#include "AddInputContextMapping_GameFeatureAction.generated.h"
class UInputMappingContext;
struct FComponentRequestHandle;
UCLASS(DisplayName="Add Input Context Mapping")
class COMBATABILITIESSYSTEMRUNTIME_API UAddInputContextMapping_GameFeatureAction : public UGameFeature_WorldActionBase
{
GENERATED_BODY()
public:
virtual void OnGameFeatureActivating() override;
virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
#if WITH_EDITORONLY_DATA
virtual void AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData) override;
#endif
#if WITH_EDITOR
virtual EDataValidationResult IsDataValid(TArray<FText>& ValidationErrors) override;
#endif
private:
virtual void AddToWorld(const FWorldContext& InWorldContext) override;
void Reset();
void HandleControllerExtension(AActor* InActor, FName EventName);
void AddInputMappingForPlayer(UPlayer* InPlayer);
void RemoveInputMapping(APlayerController* InPlayerController);
private:
UPROPERTY(EditAnywhere, Category="Input", meta=(AllowPrivateAccess="true"))
TSoftObjectPtr<UInputMappingContext> InputMapping;
UPROPERTY(EditAnywhere, Category="Input", meta=(AllowPrivateAccess="true"))
int32 Priority{0};
TArray<TSharedPtr<FComponentRequestHandle>> ExtensionRequestHandles;
TArray<TWeakObjectPtr<APlayerController>> ControllersAddedTo;
};
AddInputContextMapping_GameFeatureAction.cpp
#include "GameFeature/AddInputContextMapping_GameFeatureAction.h"
#include "CombatAbilitiesSystemRuntime/Public/CombatAbilitiesSystemRuntimeModule.h"
#include "Engine/AssetManager.h"
#include "GameFeaturesSubsystemSettings.h"
#include "Components/GameFrameworkComponentManager.h"
#include "GameFramework/PlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#define LOCTEXT_NAMESPACE "CombatAbilitiesSystemRuntimeFeatures"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AddInputContextMapping_GameFeatureAction)
void UAddInputContextMapping_GameFeatureAction::OnGameFeatureActivating()
{
if (!ensure(ExtensionRequestHandles.IsEmpty()) ||
!ensure(ControllersAddedTo.IsEmpty()))
{
Reset();
}
Super::OnGameFeatureActivating();
}
void UAddInputContextMapping_GameFeatureAction::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
Super::OnGameFeatureDeactivating(Context);
Reset();
}
#if WITH_EDITORONLY_DATA
void UAddInputContextMapping_GameFeatureAction::AddAdditionalAssetBundleData(FAssetBundleData& AssetBundleData)
{
if (UAssetManager::IsValid())
{
AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateClient, InputMapping.ToSoftObjectPath());
AssetBundleData.AddBundleAsset(UGameFeaturesSubsystemSettings::LoadStateServer, InputMapping.ToSoftObjectPath());
}
}
#endif
#if WITH_EDITOR
EDataValidationResult UAddInputContextMapping_GameFeatureAction::IsDataValid(TArray<FText>& ValidationErrors)
{
EDataValidationResult Result {CombineDataValidationResults(Super::IsDataValid(ValidationErrors), EDataValidationResult::Valid)};
if (InputMapping.IsNull())
{
Result = EDataValidationResult::Invalid;
ValidationErrors.Add(LOCTEXT("NullInputMapping", "Null InputMapping."));
}
return Result;
}
#endif
void UAddInputContextMapping_GameFeatureAction::AddToWorld(const FWorldContext& WorldContext)
{
const UWorld* World {WorldContext.World()};
const UGameInstance* GameInstance {WorldContext.OwningGameInstance.Get()};
if ((GameInstance != nullptr) && (World != nullptr) && World->IsGameWorld())
{
if (UGameFrameworkComponentManager* ComponentMan {UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance)})
{
if (!InputMapping.IsNull())
{
const UGameFrameworkComponentManager::FExtensionHandlerDelegate AddAbilitiesDelegate {UGameFrameworkComponentManager::FExtensionHandlerDelegate::CreateUObject(
this, &UAddInputContextMapping_GameFeatureAction::HandleControllerExtension)};
const TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle {ComponentMan->AddExtensionHandler(APlayerController::StaticClass(), AddAbilitiesDelegate)};
ExtensionRequestHandles.Add(ExtensionRequestHandle);
}
}
}
}
void UAddInputContextMapping_GameFeatureAction::Reset()
{
ExtensionRequestHandles.Empty();
while (!ControllersAddedTo.IsEmpty())
{
TWeakObjectPtr<APlayerController> ControllerPtr {ControllersAddedTo.Top()};
if (ControllerPtr.IsValid())
{
RemoveInputMapping(ControllerPtr.Get());
}
else
{
ControllersAddedTo.Pop();
}
}
}
void UAddInputContextMapping_GameFeatureAction::HandleControllerExtension(AActor* InActor, FName InEventName)
{
APlayerController* AsController {CastChecked<APlayerController>(InActor)};
if (InEventName == UGameFrameworkComponentManager::NAME_ExtensionRemoved || InEventName == UGameFrameworkComponentManager::NAME_ReceiverRemoved)
{
RemoveInputMapping(AsController);
}
else if (InEventName == UGameFrameworkComponentManager::NAME_ExtensionAdded || InEventName == UGameFrameworkComponentManager::NAME_GameActorReady)
{
AddInputMappingForPlayer(AsController->GetLocalPlayer());
}
}
void UAddInputContextMapping_GameFeatureAction::AddInputMappingForPlayer(UPlayer* InPlayer)
{
if (const ULocalPlayer* LocalPlayer {Cast<ULocalPlayer>(InPlayer)})
{
if (UEnhancedInputLocalPlayerSubsystem* InputSystem {LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>()})
{
if (!InputMapping.IsNull())
{
InputSystem->AddMappingContext(InputMapping.LoadSynchronous(), Priority);
}
}
else
{
UE_LOG(LogCombatAbilitySystem, Error, TEXT("Failed to find `UEnhancedInputLocalPlayerSubsystem` for local player. Input mappings will not be added. Make sure you're set to use the EnhancedInput system via config file."));
}
}
}
void UAddInputContextMapping_GameFeatureAction::RemoveInputMapping(APlayerController* InPlayerController)
{
if (const ULocalPlayer* LocalPlayer {InPlayerController->GetLocalPlayer()})
{
if (UEnhancedInputLocalPlayerSubsystem* InputSystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
InputSystem->RemoveMappingContext(InputMapping.Get());
}
}
ControllersAddedTo.Remove(InPlayerController);
}
#undef LOCTEXT_NAMESPACE
Для того чтобы была возможность добавлять компоненты к актору, надо сделать чтобы он зарегистрировал себя на получение компонентов у Game Framework Component Manager. Рассмотрим на примере для PlayerController, с другими акторами тоже самое, но без ReceivedPlayer() и PlayerTick():
AModularPlayerController.h
#pragma once
#include "GameFramework/PlayerController.h"
#include "ModularPlayerController.generated.h"
UCLASS(Blueprintable)
class MODULARGAMEPLAYACTORS_API AModularPlayerController : public APlayerController
{
GENERATED_BODY()
public:
virtual void PreInitializeComponents() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void ReceivedPlayer() override;
virtual void PlayerTick(float DeltaTime) override;
};
ModularPlayerController.cpp
#include "ModularPlayerController.h"
#include "Components/ControllerComponent.h"
#include "Components/GameFrameworkComponentManager.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(ModularPlayerController)
void AModularPlayerController::PreInitializeComponents()
{
Super::PreInitializeComponents();
UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);
}
void AModularPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);
Super::EndPlay(EndPlayReason);
}
void AModularPlayerController::ReceivedPlayer()
{
UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);
Super::ReceivedPlayer();
TArray<UControllerComponent*> ModularComponents;
GetComponents(ModularComponents);
for (UControllerComponent* Component : ModularComponents)
{
Component->ReceivedPlayer();
}
}
void AModularPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
TArray<UControllerComponent*> ModularComponents;
GetComponents(ModularComponents);
for (UControllerComponent* Component : ModularComponents)
{
Component->PlayerTick(DeltaTime);
}
}
Применение
После всей создания необходимых классов, пришло время это использовать.
В GameMode выставляем в Player Controller Class AModularPlayerController.
Далле добавляем AddInputContextMapping_GameFeatureAction (Add Input Context Mapping) и указываем InputMappingContext в файл GameFeatureData:
Далее чтобы это чудо заработало при запуске игры, надо в чертеже уровня прописать выполнение консольной команды — "LoadGameFeaturePlugin CombatAbilitiesSystem":
![](https://habrastorage.org/getpro/habr/upload_files/c1f/eca/568/c1feca5685098c0a20a7cd03b9a4b56b.png)
Заключение
Далее в следующей части будет рассмотрено расширение самого AbilitySystemComponent и создание Abilities.
Спасибо за внимание! Ставьте лайки, подписывайтесь и оставляйте комментарии.
Sadler
Лично у меня нет задачи биндить отдельные абилки: я всегда вызываю их с помощью тэгов, за которыми могут скрываться разные имплементации для разных персонажей. Но спасибо, что кто-то занимается GAS в UE, не я один повернутый на унификации :D