Для своей новой игры 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":

Заключение

Далее в следующей части будет рассмотрено расширение самого AbilitySystemComponent и создание Abilities.

Спасибо за внимание! Ставьте лайки, подписывайтесь и оставляйте комментарии.

Комментарии (1)


  1. Sadler
    31.07.2024 22:08
    +1

    Лично у меня нет задачи биндить отдельные абилки: я всегда вызываю их с помощью тэгов, за которыми могут скрываться разные имплементации для разных персонажей. Но спасибо, что кто-то занимается GAS в UE, не я один повернутый на унификации :D