- Buffered inputs are no longer handled by a component on the PlayerController, but by a LocalPlayerSubsystem instead.

- Combo manager connects to this subsystem entirely on its own when its owner is the player character.
This commit is contained in:
Jamie Greunbaum 2023-09-10 01:47:21 -04:00
parent 0c3b6fcf71
commit cd4708449f
12 changed files with 158 additions and 405 deletions

View File

@ -0,0 +1,6 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#include "ComboInputAssets.h"

View File

@ -2,8 +2,13 @@
#include "Components/ComboManagerComponent.h" #include "Components/ComboManagerComponent.h"
#include "Components/InputBufferComponent.h" #include "ComboInputAssets.h"
#include "Interfaces/ComboHandlerInterface.h" #include "EnhancedInputComponent.h"
#include "InputBufferLocalPlayerSubsystem.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
DEFINE_LOG_CATEGORY(LogComboManagerComponent); DEFINE_LOG_CATEGORY(LogComboManagerComponent);
@ -19,19 +24,21 @@ void UComboManagerComponent::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
const AActor *OwningActor = this->GetOwner(); // If this component's owner is the player character, attach it to the subsystem.
if (const IComboHandlerInterface *ComboHandler = Cast<IComboHandlerInterface>(OwningActor)) ACharacter *PlayerCharacter = UGameplayStatics::GetPlayerCharacter(this, 0);
if (this->GetOwner() == PlayerCharacter)
{ {
if (UInputBufferComponent *InputBuffer = IComboHandlerInterface::Execute_GetInputBuffer(OwningActor)) APlayerController *Controller = UGameplayStatics::GetPlayerController(this, 0);
{ UEnhancedInputComponent *InputComponent = Cast<UEnhancedInputComponent>(Controller->InputComponent);
this->AttachedInputBuffer = InputBuffer; checkf(Controller, TEXT("Discovered controller is not a %s type."), *UEnhancedInputComponent::StaticClass()->GetName());
this->AttachedInputBuffer->NewComboInput.BindUObject(this, &UComboManagerComponent::ComboInputReceived);
} UInputBufferLocalPlayerSubsystem *InputBufferSubsystem = Controller->GetLocalPlayer()->GetSubsystem<UInputBufferLocalPlayerSubsystem>();
InputBufferSubsystem->AttachComboManager(this, InputComponent);
} }
} }
void UComboManagerComponent::ComboInputReceived(const UComboInputAsset *Input) void UComboManagerComponent::ActivateComboInput(const UComboInputAsset *Input)
{ {
/************ DEBUG ************/ /************ DEBUG ************/
for (const TPair<TObjectPtr<const UComboInputAsset>, float> &Pair : this->DEBUG__UnlockTimers) for (const TPair<TObjectPtr<const UComboInputAsset>, float> &Pair : this->DEBUG__UnlockTimers)
@ -74,7 +81,9 @@ void UComboManagerComponent::ComboInputReceived(const UComboInputAsset *Input)
} }
void UComboManagerComponent::DEBUG__UnlockAction(TObjectPtr<const UComboInputAsset> Unlock) void UComboManagerComponent::DEBUG__UnlockAction(TObjectPtr<const UComboInputAsset> Unlock)
{ {
this->AttachedInputBuffer->UnlockComboInput(Unlock); APlayerController *Controller = UGameplayStatics::GetPlayerController(this, 0);
UInputBufferLocalPlayerSubsystem *Subsystem = Controller->GetLocalPlayer()->GetSubsystem<UInputBufferLocalPlayerSubsystem>();
Subsystem->UnlockComboInput(Unlock);
} }

View File

@ -1,189 +0,0 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#include "Components/InputBufferComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/PlayerController.h"
DEFINE_LOG_CATEGORY(LogInputBufferComponent);
UInputBufferComponent::UInputBufferComponent()
{
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bTickEvenWhenPaused = false;
PrimaryComponentTick.bCanEverTick = false;
}
void UInputBufferComponent::BeginPlay()
{
Super::BeginPlay();
// Get all unique EnhancedInput actions bound to combo input actions.
TSet<const UInputAction*> InputActionsToBind;
for (const UComboInputAsset *ComboInput : this->ComboActions)
{
for (const UInputAction *InputAction : ComboInput->ActionGroup)
{
InputActionsToBind.Add(InputAction);
}
}
APlayerController *Controller = Cast<APlayerController>(this->GetOwner());
checkf(Controller, TEXT("No player controller found as owner of %s"), *this->GetName());
if (this->EnhancedInputComponent = Cast<UEnhancedInputComponent>(Controller->InputComponent))
{
// Bind the input actions we found to the buffer management functions.
for (const UInputAction *InputAction : InputActionsToBind)
{
this->EnhancedInputComponent->BindAction(InputAction, ETriggerEvent::Started, this, &UInputBufferComponent::AddActionToBuffer, InputAction);
this->EnhancedInputComponent->BindAction(InputAction, ETriggerEvent::Completed, this, &UInputBufferComponent::ExpireAction, InputAction);
}
}
else
{
UE_LOG(LogInputBufferComponent, Error, TEXT("Parent of %s is not a UEnhancedInputComponent type."), *this->GetName());
}
}
void UInputBufferComponent::AddActionToBuffer(const FInputActionValue &Value, const class UInputAction *Action)
{
this->MostRecentActions.Add(Action);
this->ExpiringActions.Remove(Action);
// Find any combo input that matches this action, plus buffered actions.
for (const UComboInputAsset *Combo : this->ComboActions)
{
if (Combo->MatchesInputActions(this->MostRecentActions))
{
this->ActivateComboInput(Combo);
break;
}
}
this->GetWorld()->GetTimerManager().SetTimer(this->MultiPressTimerHandle, this, &UInputBufferComponent::ClearMultiPresses, this->MultiPressTimerLength);
}
void UInputBufferComponent::ClearMultiPresses()
{
#if WITH_EDITOR
TArray<FString> ActionNames;
for (const UInputAction *Action : this->MostRecentActions)
{
ActionNames.Add(Action->GetName());
}
UE_LOG(LogInputBufferComponent, Verbose, TEXT("Multi-press buffer cleared (%s)"), *FString::Join(ActionNames, TEXT(" | ")));
#endif
this->MostRecentActions.Empty();
}
void UInputBufferComponent::ActivateComboInput(const UComboInputAsset *ComboInput)
{
checkf(ComboInput, TEXT("Invalid UComboInputAsset"));
// Make this combo input active if it isn't being locked, or if we are
// overwriting a previous combo input with a multi-press combo input.
const bool bMultiPressTimerActive = this->GetWorld()->GetTimerManager().IsTimerActive(this->MultiPressTimerHandle);
const bool bComboInputLocked = this->LockedComboInputs.Contains(ComboInput);
if (bMultiPressTimerActive || !bComboInputLocked)
{
if (!ComboInput->LockedComboInputs.IsEmpty())
{
// Set the combo input as active, and copy its lock data.
this->InputBufferActive = ComboInput;
this->LockedComboInputs = ComboInput->LockedComboInputs;
// Make sure the hold is clear if we're coming off of a multi-press action.
if (bMultiPressTimerActive)
{
this->InputBufferHold = nullptr;
}
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is active."), *ComboInput->ComboInputName.ToString());
}
else
{
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is active and won't lock inputs."), *ComboInput->ComboInputName.ToString());
}
this->NewComboInput.Execute(ComboInput);
}
else
{
this->InputBufferHold = ComboInput;
if (bComboInputLocked)
{
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is locked and won't be activated yet."), *ComboInput->ComboInputName.ToString());
}
else
{
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s added to buffer."), *ComboInput->ComboInputName.ToString());
}
}
}
void UInputBufferComponent::UnlockComboInput(const UComboInputAsset *Unlocked)
{
// Remove the newly-unlocked asset from the locked combo inputs.
UE_CLOG(this->LockedComboInputs.Contains(Unlocked), LogInputBufferComponent, Verbose, TEXT("%s has unlocked."), *Unlocked->ComboInputName.ToString());
this->LockedComboInputs.Remove(Unlocked);
// Check if the newly unlocked combo input is in the hold.
if (Unlocked == this->InputBufferHold)
{
const UComboInputAsset *OriginalActive = this->InputBufferActive;
// Activate the held combo input.
const UComboInputAsset *HeldAsset = this->InputBufferHold;
this->InputBufferHold = nullptr;
if (HeldAsset)
{
this->ActivateComboInput(HeldAsset);
}
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s has expired."), *OriginalActive->ComboInputName.ToString());
}
}
void UInputBufferComponent::ExpireAction(const FInputActionValue &Value, const class UInputAction *Action)
{
this->ExpiringActions.Add(Action);
this->GetWorld()->GetTimerManager().SetTimer(this->InputReleaseExpirationTimerHandle, this, &UInputBufferComponent::ExpireBufferedActions, this->InputReleaseExpirationTimerLength);
}
void UInputBufferComponent::ExpireBufferedActions()
{
// Only bother dealing with this if there's something to deal with in the first place.
if (!this->ExpiringActions.IsEmpty())
{
UE_SUPPRESS(LogInputBufferComponent, Verbose,
{
TArray<FString> ActionNames;
for (const UInputAction *Action : this->ExpiringActions)
{
ActionNames.Add(Action->GetName());
}
UE_LOG(LogInputBufferComponent, Verbose, TEXT("Released actions expired (%s)"), *FString::Join(ActionNames, TEXT(" | ")));
}
);
// If there is an action in the hold, check if it's related
// to our current released buttons, and if so cancel it.
if (this->InputBufferHold)
{
for (const UInputAction *Action : this->ExpiringActions)
{
if (this->InputBufferHold->MatchesInputAction(Action))
{
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s has been cancelled."), *this->InputBufferHold->ComboInputName.ToString());
this->InputBufferHold = nullptr;
break;
}
}
}
this->ExpiringActions.Empty();
}
}

View File

@ -0,0 +1,6 @@
// ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
#include "GlobalSettings/InputBufferSubsystemGlobalSettings.h"

View File

@ -1,17 +0,0 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#include "Interfaces/ComboHandlerInterface.h"
#include "Components/ComboManagerComponent.h"
#include "Components/InputBufferComponent.h"
UInputBufferComponent *IComboHandlerInterface::GetInputBuffer_Implementation() const
{
return nullptr;
}
UComboManagerComponent *IComboHandlerInterface::GetComboManager_Implementation() const
{
return nullptr;
}

View File

@ -0,0 +1,90 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "ComboInputAssets.generated.h"
USTRUCT(BlueprintType)
struct COMBOINPUT_API FComboSequenceAction
{
GENERATED_BODY()
public:
// Action to perform when the associated combo sequence node is activated.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TObjectPtr<const class UComboAction> ComboAction;
// Sequence node to switch to once this action is complete.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TObjectPtr<const class UComboSequenceNode> NextNode;
};
UCLASS(BlueprintType)
class COMBOINPUT_API UComboAction : public UDataAsset
{
GENERATED_BODY()
public:
// Human-readable name of this combo action.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FName ActionName;
};
UCLASS(BlueprintType)
class COMBOINPUT_API UComboSequenceNode : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TMap<const class UComboInputAsset *, struct FComboSequenceAction> ComboBranch;
};
UCLASS(BlueprintType)
class COMBOINPUT_API UComboInputAsset : public UDataAsset
{
GENERATED_BODY()
public:
bool MatchesInputAction(const class UInputAction* Action) const
{
if (this->ActionGroup.Num() == 1 && this->ActionGroup.Contains(Action))
{
return true;
}
return false;
}
bool MatchesInputActions(TSet<const class UInputAction *> Actions) const
{
if (this->ActionGroup.Num() == Actions.Num())
{
for (const UInputAction *Action : Actions)
{
if (!this->ActionGroup.Contains(Action))
{
return false;
}
}
return true;
}
return false;
}
// Human-readable name of this combo input.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FName ComboInputName;
// Combined actions that add up to this combo input when activated
// within a short time of one another. If only one is present, then
// this combo input asset will simply represent that action.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TSet<TObjectPtr<const class UInputAction>> ActionGroup;
// Combo inputs that should be prevented from occurring during this
// action. These will be locked when the action is broadcast, and
// should be unlocked by the receiving actor by sending an unlock
// event.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TSet<TObjectPtr<const class UComboInputAsset>> LockedComboInputs;
};

View File

@ -12,41 +12,6 @@
DECLARE_LOG_CATEGORY_EXTERN(LogComboManagerComponent, Log, All); DECLARE_LOG_CATEGORY_EXTERN(LogComboManagerComponent, Log, All);
USTRUCT(BlueprintType)
struct COMBOINPUT_API FComboSequenceAction
{
GENERATED_BODY()
public:
// Action to perform when the associated combo sequence node is activated.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TObjectPtr<const class UComboAction> ComboAction;
// Sequence node to switch to once this action is complete.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TObjectPtr<const class UComboSequenceNode> NextNode;
};
UCLASS(BlueprintType)
class COMBOINPUT_API UComboAction : public UDataAsset
{
GENERATED_BODY()
public:
// Human-readable name of this combo action.
UPROPERTY(BlueprintReadOnly, EditAnywhere)
FName ActionName;
};
UCLASS(BlueprintType)
class COMBOINPUT_API UComboSequenceNode : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, EditAnywhere)
TMap<const class UComboInputAsset*, struct FComboSequenceAction> ComboBranch;
};
UCLASS(BlueprintType, ClassGroup=(Input), meta=(BlueprintSpawnableComponent)) UCLASS(BlueprintType, ClassGroup=(Input), meta=(BlueprintSpawnableComponent))
class COMBOINPUT_API UComboManagerComponent : public UActorComponent class COMBOINPUT_API UComboManagerComponent : public UActorComponent
{ {
@ -56,6 +21,9 @@ public:
UComboManagerComponent(); UComboManagerComponent();
virtual void BeginPlay() override; virtual void BeginPlay() override;
UFUNCTION(BlueprintCallable)
void ActivateComboInput(const class UComboInputAsset *Input);
protected: protected:
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly) UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)
TObjectPtr<const class UComboSequenceNode> DefaultStartNode; TObjectPtr<const class UComboSequenceNode> DefaultStartNode;
@ -70,8 +38,6 @@ protected:
TMap<TObjectPtr<const class UComboInputAsset>, float> DEBUG__UnlockTimers; TMap<TObjectPtr<const class UComboInputAsset>, float> DEBUG__UnlockTimers;
private: private:
void ComboInputReceived(const class UComboInputAsset *Input);
void BeginNodeTransition(const class UComboSequenceNode *NextNode); void BeginNodeTransition(const class UComboSequenceNode *NextNode);
void FinishTransition(); void FinishTransition();

View File

@ -1,118 +0,0 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "Components/ActorComponent.h"
#include "InputBufferComponent.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogInputBufferComponent, Log, All);
UCLASS(BlueprintType)
class COMBOINPUT_API UComboInputAsset : public UDataAsset
{
GENERATED_BODY()
public:
bool MatchesInputAction(const class UInputAction* Action) const
{
if (this->ActionGroup.Num() == 1 && this->ActionGroup.Contains(Action))
{
return true;
}
return false;
}
bool MatchesInputActions(TSet<const class UInputAction *> Actions) const
{
if (this->ActionGroup.Num() == Actions.Num())
{
for (const UInputAction *Action : Actions)
{
if (!this->ActionGroup.Contains(Action))
{
return false;
}
}
return true;
}
return false;
}
// Human-readable name of this combo input.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FName ComboInputName;
// Combined actions that add up to this combo input when activated
// within a short time of one another. If only one is present, then
// this combo input asset will simply represent that action.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TSet<TObjectPtr<const class UInputAction>> ActionGroup;
// Combo inputs that should be prevented from occurring during this
// action. These will be locked when the action is broadcast, and
// should be unlocked by the receiving actor by sending an unlock
// event.
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TSet<TObjectPtr<const class UComboInputAsset>> LockedComboInputs;
};
UCLASS(BlueprintType, ClassGroup=(Input), meta=(BlueprintSpawnableComponent))
class COMBOINPUT_API UInputBufferComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInputBufferComponent();
virtual void BeginPlay() override;
UFUNCTION(BlueprintCallable)
void UnlockComboInput(const class UComboInputAsset *Unlocked);
// List of possible combo inputs that can be taken. A combo input is selected from this list
// either if an action is made while the current combo is inactive, or when the previous
// action expires.
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)
TSet<const class UComboInputAsset*> ComboActions;
// Length of time after releasing an input to keep the associated combo action buffered before clearing it.
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, meta=(UIMin="0.0", UIMax="0.5"))
float InputReleaseExpirationTimerLength = 0.0666666666666666667f;
// Length of time within which we can recognise multiple button presses as one input.
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, meta=(UIMin="0.02", UIMax="0.25"))
float MultiPressTimerLength = 0.025f;
DECLARE_DELEGATE_OneParam(FNewComboInput, const class UComboInputAsset*);
FNewComboInput NewComboInput;
private:
void AddActionToBuffer(const FInputActionValue &Value, const class UInputAction *Action);
void ExpireAction(const FInputActionValue &Value, const class UInputAction *Action);
void ActivateComboInput(const class UComboInputAsset *ComboInput);
void ClearMultiPresses();
void ExpireBufferedActions();
TObjectPtr<class UEnhancedInputComponent> EnhancedInputComponent;
// Currently active combo input.
TObjectPtr<const class UComboInputAsset> InputBufferActive;
// Combo input held until the current input has expired.
TObjectPtr<const class UComboInputAsset> InputBufferHold;
// Set of currently locked actions; will not be activated until an unlock signal is received.
TSet<TObjectPtr<const class UComboInputAsset>> LockedComboInputs;
TSet<const class UInputAction*> MostRecentActions;
TSet<const class UInputAction*> ExpiringActions;
FTimerHandle MultiPressTimerHandle;
FTimerHandle InputReleaseExpirationTimerHandle;
FTimerHandle ForceUnlockTimerHandle;
};

View File

@ -0,0 +1,32 @@
// ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
#pragma once
#include "Engine/DeveloperSettingsBackedByCVars.h"
#include "InputBufferSubsystemGlobalSettings.generated.h"
/**
* Global settings for the input buffer subsystem
*/
UCLASS(Config=Game, defaultconfig, meta=(DisplayName="Input Buffer Subsystem"))
class COMBOINPUT_API UInputBufferSubsystemGlobalSettings : public UDeveloperSettingsBackedByCVars
{
GENERATED_BODY()
public:
// List of possible combo inputs that can be taken. A combo input is selected from this list
// either if an action is made while the current combo is inactive, or when the previous
// action expires.
UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly)
TSet<TSoftObjectPtr<const class UComboInputAsset>> ComboActions;
// Length of time after releasing an input to keep the associated combo action buffered before clearing it.
UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly, meta = (UIMin = "0.0", UIMax = "0.5"))
float InputReleaseExpirationTimerLength = 0.0666666666666666667f;
// Length of time within which we can recognise multiple button presses as one input.
UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly, meta = (UIMin = "0.02", UIMax = "0.25"))
float MultiPressTimerLength = 0.025f;
};

View File

@ -1,32 +0,0 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ComboHandlerInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI, meta=(Blueprintable))
class UComboHandlerInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for anything that handles combo inputs and contains a
* UComboManagerComponent.
*/
class COMBOINPUT_API IComboHandlerInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
class UInputBufferComponent *GetInputBuffer() const;
virtual class UInputBufferComponent *GetInputBuffer_Implementation() const;
UFUNCTION(BlueprintCallable, BlueprintNativeEvent)
class UComboManagerComponent *GetComboManager() const;
virtual class UComboManagerComponent *GetComboManager_Implementation() const;
};