AbilityInputComponent,支持EnhancedInputComponent和GAS的操作映射

发布时间 2023-08-11 22:07:05作者: 浪兮一

目前完成进度

输入系统的基本搭建

​ 支持输入按键触发对应的Ability,同时支持按键的按下和释放两种状态的区分并可以在代码或蓝图中自定义特殊逻辑

能力输入系统详解

​ 通过配置文件来保存Action和Ability的对应关系,对应的关键词条就是GameplayTag。通过一个GameplayTag可以找到对应的Ability和Action,它们之间可以是多对多的关系。

配置完成之后就可以利用GameplayAbility来完成对应输入操作所需要完成的操作。

在输入Action后是如何找到对应的Ability的呢?

在初始化配置文件时,Ability就已经被添加到了ASC中,同时添加的还有这个Ability对应的Tag。

而Action则是在BindAction函数中完成匹配的,通过BindAction可以传递多个参数并交给代理函数。在代理函数中,我们通过AbilitySpec.DynamicAbilityTags.HasTag(InTag)来利用触发的Tag获取到对应Ability的SpecHandle,最后通过PlayerController的PostProcessInput函数来激活能力。

AbilityInputComponent,支持EnhancedInputComponent和GAS的操作映射

image-20230811214804088

支持的功能:

输入操作后自动触发Ability

能够区分Pressed和Released并调用不同的处理函数(Pressed和Held并未区分,若只需要在按下时触发Ability则可以在IMC或者IA中设置触发器就可以了)

提供的PawnData可以方便对数据进行管理以及扩展

输入系统的重要类

SC_InputComponent

​ BindAbilityAction() 和 BindNativeAction()

​ 对EnhancedInputComponent提供BindAction进行封装,将我们所需要的Tag传递到BindAction的代理函数中。

​ PressedAction() 和 ReleasedAction()

​ 开放给BindAction绑定的代理函数调用,可以方便利用Action传入的Tag来获取AbilitySpecHandle。

​ ProcessInputAction()

​ 由PlayerController的PostProcessInput调用,主要是用于执行Released和Pressed两种不同行为所带来的不同的执行。

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class SHOOTERCORE_API USC_InputComponent : public UEnhancedInputComponent
{
    GENERATED_BODY()

public:
    USC_InputComponent();

    //将AbilityData中Action的AbilityTag作为参数传递给两个处理函数
    //BindAction(Action, ETriggerEvent::Triggered, InObject, InPressedFuncName, AbilityTag);
    template <class InClass, typename PressedFunc, typename ReleasedFunc>
    void BindAbilityAction(const TArray<FInputActionFragment>& InputActions, InClass* InObject,
                           PressedFunc InPressedFuncName, ReleasedFunc InReleasedFuncName);

    //正常使用EnhancedInput进行绑定
    void BindNativeAction(const UInputMappingContext* IMC, ETriggerEvent TriggerEvent, UObject* InObject);

    //释放按键时触发,保存对应AbilityTag的AbilitySpecHandle
    void ReleasedAction(const UAbilitySystemComponent* ASC, const FGameplayTag InTag);

    //按下按键时触发,保存对应AbilityTag的AbilitySpecHandle
    void PressedAction(const UAbilitySystemComponent* ASC, const FGameplayTag InTag);

    //尝试激活所有的Input行为所带来的Ability。
    void ProcessInputAction(UAbilitySystemComponent* ASC, float DeltaTime, bool bGamePaused);

protected:
    TArray<FGameplayAbilitySpecHandle> PressedAbility;
    TArray<FGameplayAbilitySpecHandle> ReleasedAbility;
    //TArray<FGameplayAbilitySpecHandle> HeldAbility;
};

template <class InClass, typename PressedFunc, typename ReleasedFunc>
void USC_InputComponent::BindAbilityAction(const TArray<FInputActionFragment>& InputActions, InClass* InObject,
                                           PressedFunc InPressedFuncName, ReleasedFunc InReleasedFuncName)
{
    for (const FInputActionFragment& Fragment : InputActions)
    {
        if (InPressedFuncName)
        {
            BindAction(Fragment.Action, ETriggerEvent::Triggered, InObject, InPressedFuncName, Fragment.AbilityTag);
        }
        if (InReleasedFuncName)
        {
            BindAction(Fragment.Action, ETriggerEvent::Completed, InObject, InReleasedFuncName, Fragment.AbilityTag);
        }
    }
}

// 以下为Cpp 文件 =============================================================================================

void USC_InputComponent::BindNativeAction(const UInputMappingContext* IMC, ETriggerEvent TriggerEvent,
                                          UObject* InObject)
{
    TSet<TObjectPtr<const UInputAction>> UniqueAction;
    for (const FEnhancedActionKeyMapping& Mapping : IMC->GetMappings())
    {
        UniqueAction.Add(Mapping.Action);
    }

    for (const UInputAction* Action : UniqueAction)
    {
        BindAction(Action, TriggerEvent, InObject, Action->GetFName());
    }
}

void USC_InputComponent::ReleasedAction(const UAbilitySystemComponent* ASC, const FGameplayTag InTag)
{
    check(ASC);

    if (!InTag.IsValid())
    {
        return;
    }
	
    for (const FGameplayAbilitySpec& Spec : ASC->GetActivatableAbilities())
    {
        if (Spec.DynamicAbilityTags.HasTag(InTag))
        {
            ReleasedAbility.Add(Spec.Handle);
        }
    }
}

void USC_InputComponent::PressedAction(const UAbilitySystemComponent* ASC, const FGameplayTag InTag)
{
    check(ASC);

    if (!InTag.IsValid())
    {
        return;
    }

    for (FGameplayAbilitySpec AbilitySpec : ASC->GetActivatableAbilities())
    {
        if (AbilitySpec.DynamicAbilityTags.HasTag(InTag))
        {
            PressedAbility.Add(AbilitySpec.Handle);
        }
    }
}

void USC_InputComponent::ProcessInputAction(UAbilitySystemComponent* ASC, float DeltaTime, bool bGamePaused)
{
    check(ASC);

    static TArray<FGameplayAbilitySpecHandle> ActiveAbilitySet;
    ActiveAbilitySet.Reset();

    //当执行Pressed时,第一帧必然会执行TryActivateAbility。
    //在第二帧时若玩家仍然存在输入则此时执行InputPressed。因为在第一帧的时候已经将Ability激活,且后续Ability可以由Pressed或者Released结束。
    //代码来源:void UAbilitySystemComponent::AbilityLocalInputReleased(int32 InputID)
    for (const FGameplayAbilitySpecHandle& SpecHandle : PressedAbility)
    {
        FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromHandle(SpecHandle);
        if (Spec->Ability)
        {
            Spec->InputPressed = true;
            if (Spec->IsActive())
            {
                if (Spec->Ability->bReplicateInputDirectly && ASC->IsOwnerActorAuthoritative() == false)
                {
                    ASC->ServerSetInputPressed(Spec->Handle);
                }

                ASC->AbilitySpecInputPressed(*Spec);

                // Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
                ASC->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec->Handle,
                                           Spec->ActivationInfo.GetActivationPredictionKey());
            }
            else
            {
                // Ability is not active, so try to activate it
                ActiveAbilitySet.Add(SpecHandle);
            }
        }
    }

    for (FGameplayAbilitySpecHandle& SpecHandle : ReleasedAbility)
    {
        FGameplayAbilitySpec* AbilitySpec = ASC->FindAbilitySpecFromHandle(SpecHandle);
        FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromHandle(SpecHandle);

        if (Spec->Ability)
        {
            Spec->InputPressed = false;
            if (Spec->IsActive())
            {
                if (Spec->Ability->bReplicateInputDirectly && ASC->IsOwnerActorAuthoritative() == false)
                {
                    ASC->ServerSetInputReleased(Spec->Handle);
                }

                ASC->AbilitySpecInputReleased(*Spec);

                // Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
                ASC->InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputReleased, Spec->Handle,
                                           Spec->ActivationInfo.GetActivationPredictionKey());
            }
            else
            {
                // Ability is not active, so try to activate it
                ActiveAbilitySet.Add(SpecHandle);
            }
        }
    }

    //Try Active Ability ---------------------------------------------------------------------
    for (const FGameplayAbilitySpecHandle& SpecHandle : ActiveAbilitySet)
    {
        ASC->TryActivateAbility(SpecHandle);
    }
    //Try Active Ability ---------------------------------------------------------------------


    ReleasedAbility.Reset();
    PressedAbility.Reset();
}

SC_PlayerController

​ SetupInputComponent()

​ 初始化自定义InputComponent

​ PostProcessInput()

​ 每次输入操作后自动调用函数。

#define BIND_INPUT_CALLED_FUNC(FuncName, DelegateFuncName , ...)\
UFUNCTION()\
FORCEINLINE void FuncName(FGameplayTag Tag)\
{\
    if (USC_InputComponent* ShooterIC = Cast<USC_InputComponent>(InputComponent))\
    {\
        if(const ASC_PlayerState* PS = GetPlayerState<ASC_PlayerState>())\
        {\
            ShooterIC->DelegateFuncName(PS->GetAbilitySystemComponent(),Tag);\
        }\
    }\
}

UCLASS()
class SHOOTERCORE_API ASC_PlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    ASC_PlayerController();

    /*
     * void Input_PressedAction(FGameplayTag Tag);
     * void Input_ReleasedAction(FGameplayTag Tag);
     * 这两个函数主要用在BindAbilityAction中作为InputAction的代理函数
     *  SC_IC->BindAbilityAction(InputActions, PC,
                                 &ASC_PlayerController::Input_PressedAction,
                                 &ASC_PlayerController::Input_ReleasedAction);
     * 
     * FGameplayTag Tag: 是在BindAbilityAction中通过BindAction传入的额外参数,可以被代理函数接收,然后传入InputComponent中保存。
     */
    BIND_INPUT_CALLED_FUNC(Input_PressedAction, PressedAction);
    BIND_INPUT_CALLED_FUNC(Input_ReleasedAction, ReleasedAction);

protected:
    // Override default input component.
    virtual void SetupInputComponent() override;

    //会在每次输入后之后执行,执行期间会将InputComponent保存的所有数据进行处理,完成后清除。
    virtual void PostProcessInput(const float DeltaTime, const bool bGamePaused) override;

    // Native Bind Action ------------------------------------------------
    UFUNCTION()
    void IA_Move(const FInputActionValue& Value);

    UFUNCTION()
    void IA_Look(const FInputActionValue& Value);

    UFUNCTION()
    void IA_Jump(const FInputActionValue& Value);
    // Native Bind Action ------------------------------------------------

    //作为混合空间横纵轴的数据
    UPROPERTY(BlueprintReadOnly, Category = "InputValue")
    FVector2D InputDirectionValue;
};

// 以下为Cpp 文件 =============================================================================================

void ASC_PlayerController::SetupInputComponent()
{
    //Override InputComponent Begin--------------------------------------------------------
    if (InputComponent == NULL)
    {
        InputComponent = NewObject<USC_InputComponent>(this, USC_InputComponent::StaticClass(),
                                                       TEXT("PC_InputComponent0"));
        InputComponent->RegisterComponent();
    }

    if (UInputDelegateBinding::SupportsInputDelegate(GetClass()))
    {
        InputComponent->bBlockInput = bBlockInput;
        UInputDelegateBinding::BindInputDelegatesWithSubojects(this, InputComponent);
    }
    //Override InputComponent End----------------------------------------------------------
}

void ASC_PlayerController::PostProcessInput(const float DeltaTime, const bool bGamePaused)
{
    if (USC_InputComponent* ShooterIC = Cast<USC_InputComponent>(InputComponent))
    {
        if (const ASC_PlayerState* PS = GetPlayerState<ASC_PlayerState>())
        {
            ShooterIC->ProcessInputAction(PS->GetAbilitySystemComponent(), DeltaTime, bGamePaused);
        }
    }
}

void ASC_PlayerController::IA_Move(const FInputActionValue& Value)
{
    InputDirectionValue = Value.Get<FVector2D>();
    FRotator Rotation = GetControlRotation();
    FVector ForwardVector = UKismetMathLibrary::GetForwardVector(Rotation);
    FVector RightVector = UKismetMathLibrary::GetRightVector(Rotation);

    GetPawn()->AddMovementInput(RightVector, InputDirectionValue.X);
    GetPawn()->AddMovementInput(ForwardVector, InputDirectionValue.Y);
}

void ASC_PlayerController::IA_Look(const FInputActionValue& Value)
{
    const FVector2D InputLookValue = Value.Get<FVector2D>();
    AddPitchInput(InputLookValue.Y);
    AddYawInput(InputLookValue.X);
}

void ASC_PlayerController::IA_Jump(const FInputActionValue& Value)
{
    //...
    if (ASC_Character* Possess_Character = Cast<ASC_Character>(GetPawn()))
    {
        //Character->Jump();
    }
}

SC_PawnData

​ 继承自AbilityData,提供IMC和IA和处理自身数据的方法。

USTRUCT()
struct FInputActionFragment
{
    GENERATED_BODY()

    UPROPERTY(EditDefaultsOnly)
    TObjectPtr<class UInputAction> Action;

    UPROPERTY(EditDefaultsOnly)
    FGameplayTag AbilityTag;
};

USTRUCT()
struct FInputAbilityFragment
{
    GENERATED_BODY()

    UPROPERTY(EditDefaultsOnly)
    TSubclassOf<class UGameplayAbility> Ability;

    UPROPERTY(EditDefaultsOnly)
    int32 Level = 1;

    UPROPERTY(EditDefaultsOnly)
    FGameplayTag AbilityTag;
};

UCLASS()
class SHOOTERCORE_API USC_AbilityData : public UDataAsset
{
    GENERATED_BODY()

protected:
    UPROPERTY(EditDefaultsOnly, Category = Ability)
    TArray<FInputAbilityFragment> Abilities;

    UPROPERTY(EditDefaultsOnly, Category = "GameplayEffect")
    TArray<TSubclassOf<UGameplayEffect>> InitialEffects;

public:
    /**
     * @brief 将AbilitySet中所有数据给予ASC
     * @param AbilityComponent
     * @param OutArray
     * @param SourceObject Ability来自那个对象
     */
    void InitializeAbilityData(UAbilitySystemComponent* AbilityComponent, TArray<FGameplayAbilitySpecHandle>& OutArray,
                               UObject* SourceObject = nullptr);
};


UCLASS()
class SHOOTERCORE_API USC_PawnData : public USC_AbilityData
{
    GENERATED_BODY()

public:
    UPROPERTY(EditDefaultsOnly, Category = Mapping)
    TObjectPtr<UInputMappingContext> InputMapping;

    UPROPERTY(EditDefaultsOnly, Category = Action)
    TArray<FInputActionFragment> InputActions;

    void BindAbilityInputAction(AController* InController);

    void BindInputMappingContext(const AController* InController) const;

    void ClearInputMappingContext(const AController* InController) const;
};

UCLASS()
class SHOOTERCORE_API USC_WeaponData : public USC_PawnData
{
    GENERATED_BODY()
};

// 以下为Cpp 文件 =============================================================================================

void USC_AbilityData::InitializeAbilityData(UAbilitySystemComponent* AbilityComponent,
                                            TArray<FGameplayAbilitySpecHandle>& OutArray, UObject* SourceObject)
{
    check(AbilityComponent);

    //Give all the abilities
    for (const FInputAbilityFragment AbilityFragment : Abilities)
    {
        FGameplayAbilitySpec AbilitySpec(AbilityFragment.Ability, AbilityFragment.Level);
        AbilitySpec.SourceObject = SourceObject;
        AbilitySpec.DynamicAbilityTags.AddTag(AbilityFragment.AbilityTag);
        OutArray.Add(AbilityComponent->GiveAbility(AbilitySpec));
    }


    //Apply the GameplayEffect to ability system component
    for (const TSubclassOf<UGameplayEffect> Effect : InitialEffects)
    {
        check(Effect);

        FGameplayEffectSpecHandle EffectSpecHandle = AbilityComponent->MakeOutgoingSpec(
            Effect, 1, FGameplayEffectContextHandle());
        AbilityComponent->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
    }
}

void USC_PawnData::BindAbilityInputAction(AController* InController)
{
    ASC_PlayerController* PC = Cast<ASC_PlayerController>(InController);
    check(PC);

    if (USC_InputComponent* SC_IC = Cast<USC_InputComponent>(PC->InputComponent))
    {
        //Bind ability input action
        SC_IC->BindAbilityAction(InputActions, PC,
                                 &ASC_PlayerController::Input_PressedAction,
                                 &ASC_PlayerController::Input_ReleasedAction);

        //Bind native input action. You must have the IMC action function. 
        SC_IC->BindNativeAction(InputMapping, ETriggerEvent::Triggered, PC);
    }
}

void USC_PawnData::BindInputMappingContext(const AController* InController) const
{
    const ASC_PlayerController* PC = Cast<ASC_PlayerController>(InController);
    check(PC);

    //Add InputMappingContext
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
        UEnhancedInputLocalPlayerSubsystem>(
        PC->GetLocalPlayer()))
    {
        Subsystem->AddMappingContext(InputMapping, 1);
    }
}

void USC_PawnData::ClearInputMappingContext(const AController* InController) const
{
    const ASC_PlayerController* PC = Cast<ASC_PlayerController>(InController);
    check(PC);

    //Add InputMappingContext
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
        UEnhancedInputLocalPlayerSubsystem>(
        PC->GetLocalPlayer()))
    {
        Subsystem->RemoveMappingContext(InputMapping);
    }
}