diff --git a/Source/ComboInput/Private/ComboInputAssets.cpp b/Source/ComboInput/Private/ComboInputAssets.cpp new file mode 100644 index 0000000..a57affd --- /dev/null +++ b/Source/ComboInput/Private/ComboInputAssets.cpp @@ -0,0 +1,6 @@ +// ©2022 Batty Bovine Productions, LLC. All Rights Reserved. + +#include "ComboInputAssets.h" + + + diff --git a/Source/ComboInput/Private/Components/ComboManagerComponent.cpp b/Source/ComboInput/Private/Components/ComboManagerComponent.cpp index 074c629..00ae124 100644 --- a/Source/ComboInput/Private/Components/ComboManagerComponent.cpp +++ b/Source/ComboInput/Private/Components/ComboManagerComponent.cpp @@ -2,8 +2,13 @@ #include "Components/ComboManagerComponent.h" -#include "Components/InputBufferComponent.h" -#include "Interfaces/ComboHandlerInterface.h" +#include "ComboInputAssets.h" +#include "EnhancedInputComponent.h" +#include "InputBufferLocalPlayerSubsystem.h" + +#include "Engine/LocalPlayer.h" +#include "GameFramework/Character.h" +#include "Kismet/GameplayStatics.h" DEFINE_LOG_CATEGORY(LogComboManagerComponent); @@ -18,20 +23,22 @@ UComboManagerComponent::UComboManagerComponent() void UComboManagerComponent::BeginPlay() { Super::BeginPlay(); - - const AActor *OwningActor = this->GetOwner(); - if (const IComboHandlerInterface *ComboHandler = Cast(OwningActor)) + + // If this component's owner is the player character, attach it to the subsystem. + ACharacter *PlayerCharacter = UGameplayStatics::GetPlayerCharacter(this, 0); + if (this->GetOwner() == PlayerCharacter) { - if (UInputBufferComponent *InputBuffer = IComboHandlerInterface::Execute_GetInputBuffer(OwningActor)) - { - this->AttachedInputBuffer = InputBuffer; - this->AttachedInputBuffer->NewComboInput.BindUObject(this, &UComboManagerComponent::ComboInputReceived); - } + APlayerController *Controller = UGameplayStatics::GetPlayerController(this, 0); + UEnhancedInputComponent *InputComponent = Cast(Controller->InputComponent); + checkf(Controller, TEXT("Discovered controller is not a %s type."), *UEnhancedInputComponent::StaticClass()->GetName()); + + UInputBufferLocalPlayerSubsystem *InputBufferSubsystem = Controller->GetLocalPlayer()->GetSubsystem(); + InputBufferSubsystem->AttachComboManager(this, InputComponent); } } -void UComboManagerComponent::ComboInputReceived(const UComboInputAsset *Input) +void UComboManagerComponent::ActivateComboInput(const UComboInputAsset *Input) { /************ DEBUG ************/ for (const TPair, float> &Pair : this->DEBUG__UnlockTimers) @@ -74,7 +81,9 @@ void UComboManagerComponent::ComboInputReceived(const UComboInputAsset *Input) } void UComboManagerComponent::DEBUG__UnlockAction(TObjectPtr Unlock) { - this->AttachedInputBuffer->UnlockComboInput(Unlock); + APlayerController *Controller = UGameplayStatics::GetPlayerController(this, 0); + UInputBufferLocalPlayerSubsystem *Subsystem = Controller->GetLocalPlayer()->GetSubsystem(); + Subsystem->UnlockComboInput(Unlock); } diff --git a/Source/ComboInput/Private/Components/InputBufferComponent.cpp b/Source/ComboInput/Private/Components/InputBufferComponent.cpp deleted file mode 100644 index 84e6c51..0000000 --- a/Source/ComboInput/Private/Components/InputBufferComponent.cpp +++ /dev/null @@ -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 InputActionsToBind; - for (const UComboInputAsset *ComboInput : this->ComboActions) - { - for (const UInputAction *InputAction : ComboInput->ActionGroup) - { - InputActionsToBind.Add(InputAction); - } - } - - APlayerController *Controller = Cast(this->GetOwner()); - checkf(Controller, TEXT("No player controller found as owner of %s"), *this->GetName()); - - if (this->EnhancedInputComponent = Cast(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 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 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(); - } -} diff --git a/Source/ComboInput/Private/GlobalSettings/InputBufferSubsystemGlobalSettings.cpp b/Source/ComboInput/Private/GlobalSettings/InputBufferSubsystemGlobalSettings.cpp new file mode 100644 index 0000000..b45493d --- /dev/null +++ b/Source/ComboInput/Private/GlobalSettings/InputBufferSubsystemGlobalSettings.cpp @@ -0,0 +1,6 @@ +// ©2023 Batty Bovine Productions, LLC. All Rights Reserved. + +#include "GlobalSettings/InputBufferSubsystemGlobalSettings.h" + + + diff --git a/Source/ComboInput/Private/InputBufferLocalPlayerSubsystem.cpp b/Source/ComboInput/Private/InputBufferLocalPlayerSubsystem.cpp new file mode 100644 index 0000000..dddc854 Binary files /dev/null and b/Source/ComboInput/Private/InputBufferLocalPlayerSubsystem.cpp differ diff --git a/Source/ComboInput/Private/Interfaces/ComboHandlerInterface.cpp b/Source/ComboInput/Private/Interfaces/ComboHandlerInterface.cpp deleted file mode 100644 index 7a66c33..0000000 --- a/Source/ComboInput/Private/Interfaces/ComboHandlerInterface.cpp +++ /dev/null @@ -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; -} diff --git a/Source/ComboInput/Public/ComboInputAssets.h b/Source/ComboInput/Public/ComboInputAssets.h new file mode 100644 index 0000000..cd4aa50 --- /dev/null +++ b/Source/ComboInput/Public/ComboInputAssets.h @@ -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 ComboAction; + + // Sequence node to switch to once this action is complete. + UPROPERTY(BlueprintReadOnly, EditAnywhere) + TObjectPtr 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 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 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> 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> LockedComboInputs; +}; diff --git a/Source/ComboInput/Public/Components/ComboManagerComponent.h b/Source/ComboInput/Public/Components/ComboManagerComponent.h index d364f79..b2d1df7 100644 --- a/Source/ComboInput/Public/Components/ComboManagerComponent.h +++ b/Source/ComboInput/Public/Components/ComboManagerComponent.h @@ -12,41 +12,6 @@ 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 ComboAction; - - // Sequence node to switch to once this action is complete. - UPROPERTY(BlueprintReadOnly, EditAnywhere) - TObjectPtr 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 ComboBranch; -}; - UCLASS(BlueprintType, ClassGroup=(Input), meta=(BlueprintSpawnableComponent)) class COMBOINPUT_API UComboManagerComponent : public UActorComponent { @@ -56,6 +21,9 @@ public: UComboManagerComponent(); virtual void BeginPlay() override; + UFUNCTION(BlueprintCallable) + void ActivateComboInput(const class UComboInputAsset *Input); + protected: UPROPERTY(BlueprintReadOnly, EditDefaultsOnly) TObjectPtr DefaultStartNode; @@ -70,8 +38,6 @@ protected: TMap, float> DEBUG__UnlockTimers; private: - void ComboInputReceived(const class UComboInputAsset *Input); - void BeginNodeTransition(const class UComboSequenceNode *NextNode); void FinishTransition(); diff --git a/Source/ComboInput/Public/Components/InputBufferComponent.h b/Source/ComboInput/Public/Components/InputBufferComponent.h deleted file mode 100644 index 9a809c3..0000000 --- a/Source/ComboInput/Public/Components/InputBufferComponent.h +++ /dev/null @@ -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 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> 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> 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 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 EnhancedInputComponent; - - // Currently active combo input. - TObjectPtr InputBufferActive; - - // Combo input held until the current input has expired. - TObjectPtr InputBufferHold; - - // Set of currently locked actions; will not be activated until an unlock signal is received. - TSet> LockedComboInputs; - - TSet MostRecentActions; - TSet ExpiringActions; - - FTimerHandle MultiPressTimerHandle; - FTimerHandle InputReleaseExpirationTimerHandle; - FTimerHandle ForceUnlockTimerHandle; -}; diff --git a/Source/ComboInput/Public/GlobalSettings/InputBufferSubsystemGlobalSettings.h b/Source/ComboInput/Public/GlobalSettings/InputBufferSubsystemGlobalSettings.h new file mode 100644 index 0000000..9078d5c --- /dev/null +++ b/Source/ComboInput/Public/GlobalSettings/InputBufferSubsystemGlobalSettings.h @@ -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> 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; +}; diff --git a/Source/ComboInput/Public/InputBufferLocalPlayerSubsystem.h b/Source/ComboInput/Public/InputBufferLocalPlayerSubsystem.h new file mode 100644 index 0000000..2632be5 Binary files /dev/null and b/Source/ComboInput/Public/InputBufferLocalPlayerSubsystem.h differ diff --git a/Source/ComboInput/Public/Interfaces/ComboHandlerInterface.h b/Source/ComboInput/Public/Interfaces/ComboHandlerInterface.h deleted file mode 100644 index ef07ed3..0000000 --- a/Source/ComboInput/Public/Interfaces/ComboHandlerInterface.h +++ /dev/null @@ -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; -};