251 lines
8.4 KiB
C++
251 lines
8.4 KiB
C++
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
|
|
|
|
#include "Components/InputBufferComponent.h"
|
|
|
|
#include "ComboInputAssets.h"
|
|
#include "EnhancedInputComponent.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
|
|
#include "Components/ComboManagerComponent.h"
|
|
#include "GlobalSettings/InputBufferGlobalSettings.h"
|
|
|
|
DEFINE_LOG_CATEGORY(LogInputBufferComponent);
|
|
|
|
|
|
UInputBufferComponent::UInputBufferComponent()
|
|
{
|
|
this->bWantsInitializeComponent = true;
|
|
}
|
|
|
|
void UInputBufferComponent::InitializeComponent()
|
|
{
|
|
Super::InitializeComponent();
|
|
|
|
if (APlayerController *PlayerController = UGameplayStatics::GetPlayerController(this, 0))
|
|
{
|
|
if (UComboManagerComponent *ComboManager = PlayerController->GetComponentByClass<UComboManagerComponent>())
|
|
{
|
|
// Get the player character and try to connect to its combo manager.
|
|
this->OnNewComboInput.BindUObject(ComboManager, &UComboManagerComponent::HandleComboInput);
|
|
|
|
const TSet<const UComboInputAsset *> &ComboInputs = ComboManager->FindAllUsedInputs();
|
|
|
|
// Get all unique EnhancedInput actions bound to combo input actions.
|
|
TSet<const UInputAction *> InputActionsToBind;
|
|
for (const UComboInputAsset *ComboInput : ComboInputs)
|
|
{
|
|
if (ComboInput)
|
|
{
|
|
this->ComboInputList.Emplace(ComboInput);
|
|
for (const UInputAction *InputAction : ComboInput->ActionGroup)
|
|
{
|
|
InputActionsToBind.Add(InputAction);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogInputBufferComponent, Verbose, TEXT("Invalid combo input found"));
|
|
}
|
|
}
|
|
for (const UInputAction *InputAction : InputActionsToBind)
|
|
{
|
|
this->BindAction(InputAction, ETriggerEvent::Started, this, &UInputBufferComponent::AddActionToBuffer, InputAction);
|
|
this->BindAction(InputAction, ETriggerEvent::Completed, this, &UInputBufferComponent::ExpireAction, InputAction);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void UInputBufferComponent::AddActionToBuffer(const FInputActionValue &Value, const class UInputAction *Action)
|
|
{
|
|
this->MultiPressActions.Add(Action);
|
|
this->ExpiringActions.Remove(Action);
|
|
|
|
// Find any combo input that matches this action, plus buffered actions.
|
|
bool bComboInputFound = false;
|
|
for (TObjectPtr<const UComboInputAsset> ComboInput : this->ComboInputList)
|
|
{
|
|
if (ComboInput->MatchesInputActions(this->MultiPressActions))
|
|
{
|
|
this->ActivateComboInput(ComboInput.Get());
|
|
bComboInputFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we haven't found any matching inputs, check if anything was somehow unhandled.
|
|
// This can happen if, for example, a jump input can combine with an attack input to
|
|
// create a combo action, but jump by itself has no action on its own.
|
|
if (!bComboInputFound)
|
|
{
|
|
if (!this->UnhandledActions.IsEmpty())
|
|
{
|
|
TSet<const UInputAction*> HandledActions = this->MultiPressActions;
|
|
for (const UInputAction *UnhandledAction : this->UnhandledActions)
|
|
{
|
|
HandledActions.Remove(UnhandledAction);
|
|
}
|
|
|
|
for (TObjectPtr<const UComboInputAsset> ComboInput : this->ComboInputList)
|
|
{
|
|
if (ComboInput->MatchesInputActions(HandledActions))
|
|
{
|
|
this->ActivateComboInput(ComboInput);
|
|
bComboInputFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this->UnhandledActions.Add(Action);
|
|
}
|
|
|
|
const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
|
|
this->GetWorld()->GetTimerManager().SetTimer(this->MultiPressTimerHandle, this, &UInputBufferComponent::ClearMultiPresses, Settings->MultiPressTimerLength);
|
|
}
|
|
void UInputBufferComponent::ClearMultiPresses()
|
|
{
|
|
#if WITH_EDITOR
|
|
TArray<FString> ActionNames;
|
|
for (const UInputAction *Action : this->MultiPressActions)
|
|
{
|
|
ActionNames.Add(Action->GetName());
|
|
}
|
|
UE_LOG(LogInputBufferComponent, Verbose, TEXT("Multi-press buffer cleared (%s)"), *FString::Join(ActionNames, TEXT(" | ")));
|
|
#endif
|
|
this->MultiPressActions.Empty();
|
|
this->UnhandledActions.Empty();
|
|
}
|
|
|
|
void UInputBufferComponent::ActivateComboInput(const UComboInputAsset *ComboInput)
|
|
{
|
|
checkf(ComboInput, TEXT("Invalid %s."), *UComboInputAsset::StaticClass()->GetName());
|
|
|
|
const bool bNewInputLocked = this->LockedComboInputs.Contains(ComboInput);
|
|
const bool bNewSupercedesActive = (this->InputBufferActive && this->InputBufferActive->ContainsOneOf(ComboInput->ActionGroup) && !bNewInputLocked);
|
|
|
|
// 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.
|
|
if (bNewSupercedesActive || !bNewInputLocked)
|
|
{
|
|
// Set the combo input as active.
|
|
this->InputBufferActive = ComboInput;
|
|
this->InputBufferHold = nullptr;
|
|
|
|
const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
|
|
for (const TObjectPtr<const UComboInputAsset> &LockedAsset : this->ComboInputList)
|
|
{
|
|
this->LockComboInput(LockedAsset);
|
|
}
|
|
|
|
this->OnNewComboInput.Execute(ComboInput, EComboActionTriggerEvent::Activated);
|
|
|
|
// If the incoming combo input contains an expiring action,
|
|
// we know it's been released, so send that signal too.
|
|
if (ComboInput->ContainsOneOf(this->ExpiringActions))
|
|
{
|
|
this->OnNewComboInput.Execute(ComboInput, EComboActionTriggerEvent::Released);
|
|
}
|
|
|
|
UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is active."), *ComboInput->ComboInputName.ToString());
|
|
}
|
|
else
|
|
{
|
|
this->HoldComboInput(ComboInput);
|
|
}
|
|
}
|
|
|
|
void UInputBufferComponent::HoldComboInput(const UComboInputAsset *ComboInput)
|
|
{
|
|
this->InputBufferHold = ComboInput;
|
|
UE_SUPPRESS(LogInputBufferComponent, Verbose,
|
|
{
|
|
if (this->LockedComboInputs.Contains(ComboInput))
|
|
{
|
|
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::LockComboInput(const UComboInputAsset *Input)
|
|
{
|
|
this->LockedComboInputs.Emplace(Input);
|
|
}
|
|
|
|
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)
|
|
{
|
|
// Only send a release event if we haven't already, i.e. if the combo input associated
|
|
// with the action is not already buffered for another future activation.
|
|
if (this->InputBufferActive && this->InputBufferActive != this->InputBufferHold && this->InputBufferActive->MatchesInputAction(Action))
|
|
{
|
|
this->OnNewComboInput.Execute(this->InputBufferActive, EComboActionTriggerEvent::Released);
|
|
}
|
|
|
|
// Prepare to expire any buffered combo inputs
|
|
this->ExpiringActions.Add(Action);
|
|
const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
|
|
this->GetWorld()->GetTimerManager().SetTimer(this->InputReleaseExpirationTimerHandle, this, &UInputBufferComponent::ExpireBufferedActions, Settings->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();
|
|
}
|
|
}
|