Compare commits

...

21 Commits

Author SHA1 Message Date
dcd0a283fe Even more documentation 2024-09-18 20:49:41 -04:00
bc2cd13cc1 Good ol' documentation 2024-09-17 20:55:40 -04:00
108bcb67dd Update README 2024-05-11 22:54:56 -04:00
127d382b29 Different events for taking a hit and a hit from your attack landing 2024-02-10 00:55:08 -05:00
b66a93d108 Pass state context to GetFollowupState 2024-02-04 19:08:47 -05:00
b8ffa48b47 IsFollowupState flag and replace FollowupState with GetFollowupState 2024-01-31 21:59:28 -05:00
50a3bd8a4b Automatically go to a followup state when curr state ends 2024-01-30 21:20:23 -05:00
ea8e26e77d Expose GoTo State to blueprints 2024-01-29 20:37:42 -05:00
52736236d3 Fix Is Frame in Range function 2024-01-11 20:28:54 -05:00
5a967109fa Actually initialize the input buffer on the AI controller 2023-12-05 22:23:16 -05:00
a074478f81 Fix naming 2023-11-27 21:46:52 -05:00
3d4c56fcd9 Write some unit tests for the input buffer 2023-11-25 21:03:31 -05:00
ee8dff88ea Implement separate timeout durations for each input condition 2023-11-25 19:21:31 -05:00
afc6781f5c Define whether most recent is disabled per state 2023-11-24 22:44:37 -05:00
d7920802cf Keep track of tick on landed was called 2023-11-20 22:17:55 -05:00
da392c03fb Stub in AI controller 2023-11-15 20:42:40 -05:00
14cc6304c4 Fix copyright 2023-11-12 20:43:32 -05:00
c3a0d1f481 Reset current input when losing focus of game viewport 2023-11-10 14:27:55 -05:00
c09094e186 Fix include order 2023-11-10 10:34:35 -05:00
2f22223776 Poll inputs outside of game logic tick 2023-11-10 10:05:16 -05:00
b62a8cebb6 Collect move axis and look rot input 2023-11-06 21:34:16 -05:00
18 changed files with 606 additions and 94 deletions

Binary file not shown.

View File

@ -4,8 +4,12 @@ This library provides actors, components, and other general data structures that
### To-Do ### To-Do
- [ ] State Machine - [X] State Machine
- [ ] Input Buffer - [X] Input Buffer
- [ ] Unit tests
- [ ] Generic "Actor" class for any game affecting actors that need to run at a fixed tick & rollback
- [ ] Interfaces/virtual functions for rolling back and resimulating objects
- [ ] Methods for queueing animations/VFX/SFX when re-simulating
### Setup ### Setup

View File

@ -0,0 +1,31 @@
// Project Sword & Gun Copyright Kevin Poretti
#include "GameplayFramework/FFAIController.h"
AFFAIController::AFFAIController()
{
InputBuffer = CreateDefaultSubobject<UFFInputBufferComponent>(TEXT("InputBuffer"));
bWantsPlayerState = true;
}
bool AFFAIController::CheckInputSequence(const FFFInputSequence& InInputSequence)
{
return false;
}
bool AFFAIController::CheckInputSequences(const TArray<FFFInputSequence>& InInputSequences)
{
return false;
}
FVector AFFAIController::GetMoveInputAsWorldDirection() const
{
return FVector::ZeroVector;
}
void AFFAIController::DisableMostRecentInput()
{
check(InputBuffer);
InputBuffer->DisableMostRecentInput();
}

View File

@ -0,0 +1,42 @@
// Project Sword & Gun Copyright Kevin Poretti
#pragma once
// FF includes
#include "State/IFFStateOwnerInterface.h"
#include "Input/FFInputBufferComponent.h"
// UE includes
#include "CoreMinimal.h"
#include "AIController.h"
#include "FFAIController.generated.h"
/**
*
*/
UCLASS()
class UNREALFIGHTINGFRAMEWORK_API AFFAIController : public AAIController, public IFFStateOwnerInterface
{
GENERATED_BODY()
public:
AFFAIController();
// IFFStateOwnerInterface
UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface")
virtual bool CheckInputSequence(const FFFInputSequence& InInputSequence) override;
UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface")
virtual bool CheckInputSequences(const TArray<FFFInputSequence>& InInputSequences) override;
virtual FVector GetMoveInputAsWorldDirection() const override;
virtual void DisableMostRecentInput() override;
// End of IFFStateOwnerInterface
protected:
/** Input Buffer component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UFF|Input", meta = (AllowPrivateAccess = "true"))
UFFInputBufferComponent* InputBuffer;
};

View File

@ -1,7 +1,13 @@
// Project Sword & Gun Copyright Kevin Poretti // Unreal Fighting Framework by Kevin Poretti
#include "GameplayFramework/FFGameState.h" #include "GameplayFramework/FFGameState.h"
// FF includes
#include "Input/FFPlayerController.h"
// UE includes
#include "Kismet/GameplayStatics.h"
void AFFGameState::OnFixedTick(float OneTick) void AFFGameState::OnFixedTick(float OneTick)
{ {
} }
@ -11,15 +17,50 @@ void AFFGameState::Tick(float DeltaSeconds)
{ {
Super::Tick(DeltaSeconds); Super::Tick(DeltaSeconds);
/*
* TODO: I want to reinvestigate how we're collecting and applying inputs. I seem to keep getting
* weird timing issues due to how Unreal collects and informs the game of inputs through function
* callbacks rather than some mechanism where we can poll the state of various actions/axes when
* it comes time to update. The investigation should be two-fold:
*
* 1 - is there any alternative ways of using the EnhancedInputComponent but polling for inputs
* rather than this whole callback system, so when it is time to tick the game state we can just
* do something like a bool bIsJumpPressed = GetActionValue("Jump") for all inputs and axes
* we need to collect?
*
* 2 - if there isn't any polling mechanism in the EnhancedInputComponent, how can we work around
* that and what are some of the things we have to keep in mind? What happens when the Unreal tick
* rate is slower than the game tick rate? Do we just apply the current input to multiple game ticks?
* What about when Unreal's tick rate is faster than the game? Would that make the game feel
* unresponsive i.e. feel like inputs are getting dropped?
*
* 3 - would like to know more about EnhancedInputComponent and inputs in Unreal in general as well.
* Are inputs collected in a separate thread? How does unreal determine when to call any input callbacks
* you've registered with the input component?
*/
// Get inputs
AFFPlayerController* PC = Cast<AFFPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
if(!PC)
{
UE_LOG(LogTemp, Warning, TEXT("Could not get a local player controller so no inputs could be collected"));
return;
}
PC->ModifyRawInput();
// Run anything game logic related at a fixed tick rate // Run anything game logic related at a fixed tick rate
// TODO: Interpolate between prev and current game state if there is time left in accumulator // TODO: Interpolate between prev and current game state if there is time left in accumulator
AccumulatedTime += DeltaSeconds; AccumulatedTime += DeltaSeconds;
while(AccumulatedTime > ONE_TICK) while(AccumulatedTime > ONE_TICK)
{ {
PC->AddCurrentInputToBuffer();
OnFixedTick(ONE_TICK); OnFixedTick(ONE_TICK);
AccumulatedTime -= ONE_TICK; AccumulatedTime -= ONE_TICK;
CurrentTick++; CurrentTick++;
} }
PC->ConsumeMoveInput();
} }

View File

@ -1,4 +1,4 @@
// Project Sword & Gun Copyright Kevin Poretti // Unreal Fighting Framework by Kevin Poretti
#pragma once #pragma once

View File

@ -18,29 +18,37 @@ bool UFFInputBufferComponent::CheckInputSequence(const FFFInputSequence& InputSe
{ {
if(InputSequence.Sequence.IsEmpty()) if(InputSequence.Sequence.IsEmpty())
{ {
UE_LOG(LogTemp, Error, UE_LOG(LogTemp, Warning,
TEXT("FFInputBufferComponent :: CheckInputSequence - tried to check input sequence but it was empty")); TEXT("FFInputBufferComponent :: CheckInputSequence - tried to check input sequence but it was empty"));
return false; return false;
} }
int CondIdx = InputSequence.Sequence.Num() - 1; int CondIdx = InputSequence.Sequence.Num() - 1;
int ElapsedFrames = 0; int ElapsedFrames = 0;
for(int InpIdx = InputBuffer.Num() - 1; InpIdx > 1; InpIdx--) for(int InpIdx = InputBuffer.Num() - 1; InpIdx > 0; InpIdx--)
{ {
int32 RequiredButtons = InputSequence.Sequence[CondIdx].RequiredButtons; int32 RequiredButtons = InputSequence.Sequence[CondIdx].RequiredButtons;
EFFButtonState RequiredButtonState = InputSequence.Sequence[CondIdx].RequiredButtonState; EFFButtonState RequiredButtonState = InputSequence.Sequence[CondIdx].RequiredButtonState;
int32 TimeoutDuration = InputSequence.Sequence[CondIdx].TimeoutDuration;
int32 PrevInput = InputBuffer[InpIdx - 1].Buttons; int32 PrevInput = InputBuffer[InpIdx - 1].Buttons;
int32 CurrInput = InputBuffer[InpIdx].Buttons; int32 CurrInput = InputBuffer[InpIdx].Buttons;
int32 PrevDisable = InputBuffer[InpIdx - 1].DisabledButtons; int32 PrevDisable = InputBuffer[InpIdx - 1].DisabledButtons;
int32 CurrDisable = InputBuffer[InpIdx].DisabledButtons; int32 CurrDisable = InputBuffer[InpIdx].DisabledButtons;
bool bCondFoundThisIteration = false;
switch (RequiredButtonState) switch (RequiredButtonState)
{ {
// TODO: should it be (PrevInput & RequiredButtons) == RequiredButtons or what we have now? // TODO: should it be (PrevInput & RequiredButtons) == RequiredButtons or what we have now?
// TODO: If you have a sequence that is defined as something like button 1 pressed + button 2 pressed
// and the buttons are pressed on the same exact frame, then this implementation will not
// account for that. We should keep checking the sequences against the current frame's
// inputs until no sequences are detected, then continue on checking through the input buffer.
case EFFButtonState::BTNS_Pressed: case EFFButtonState::BTNS_Pressed:
if(!(PrevInput & RequiredButtons | PrevDisable) && if(!(PrevInput & RequiredButtons | PrevDisable) &&
CurrInput & RequiredButtons & ~CurrDisable) CurrInput & RequiredButtons & ~CurrDisable)
{ {
CondIdx--; CondIdx--;
ElapsedFrames = 0;
bCondFoundThisIteration = true;
} }
break; break;
case EFFButtonState::BTNS_Released: case EFFButtonState::BTNS_Released:
@ -48,12 +56,16 @@ bool UFFInputBufferComponent::CheckInputSequence(const FFFInputSequence& InputSe
!(CurrInput & RequiredButtons | CurrDisable)) !(CurrInput & RequiredButtons | CurrDisable))
{ {
CondIdx--; CondIdx--;
ElapsedFrames = 0;
bCondFoundThisIteration = true;
} }
break; break;
case EFFButtonState::BTNS_Down: case EFFButtonState::BTNS_Down:
if(CurrInput & RequiredButtons & ~CurrDisable) if(CurrInput & RequiredButtons & ~CurrDisable)
{ {
CondIdx--; CondIdx--;
ElapsedFrames = 0;
bCondFoundThisIteration = true;
} }
break; break;
// TODO: implement button held condition // TODO: implement button held condition
@ -69,18 +81,32 @@ bool UFFInputBufferComponent::CheckInputSequence(const FFFInputSequence& InputSe
return true; return true;
} }
// No condition found this iteration, so increment elapse frames and see if current condition timed out
if(!bCondFoundThisIteration)
{
ElapsedFrames++; ElapsedFrames++;
if(ElapsedFrames > InputSequence.MaxDuration) if(ElapsedFrames > TimeoutDuration)
{ {
return false; return false;
} }
} }
}
return false; return false;
} }
void UFFInputBufferComponent::DisableMostRecentInput() void UFFInputBufferComponent::DisableMostRecentInput()
{ {
if(InputBuffer.Num() < 2)
{
return;
}
InputBuffer[InputBuffer.Num() - 1].DisabledButtons |= InputBuffer[InputBuffer.Num() - 1].Buttons; InputBuffer[InputBuffer.Num() - 1].DisabledButtons |= InputBuffer[InputBuffer.Num() - 1].Buttons;
InputBuffer[InputBuffer.Num() - 2].DisabledButtons |= InputBuffer[InputBuffer.Num() - 2].Buttons; InputBuffer[InputBuffer.Num() - 2].DisabledButtons |= InputBuffer[InputBuffer.Num() - 2].Buttons;
} }
void UFFInputBufferComponent::FlushBuffer()
{
InputBuffer.Flush();
}

View File

@ -17,6 +17,7 @@ enum class EFFButtonState : uint8
BTNS_Pressed UMETA(DisplayName="Pressed"), BTNS_Pressed UMETA(DisplayName="Pressed"),
BTNS_Released UMETA(DisplayName="Released"), BTNS_Released UMETA(DisplayName="Released"),
BTNS_Down UMETA(DisplayName="Down"), BTNS_Down UMETA(DisplayName="Down"),
//BTNS_Up UMETA(DisplayName="Up),
//BTNS_Held UMETA(DisplayName="Held"), //BTNS_Held UMETA(DisplayName="Held"),
BTNS_MAX UMETA(Hidden) BTNS_MAX UMETA(Hidden)
}; };
@ -30,7 +31,7 @@ struct FFFInputState
GENERATED_BODY() GENERATED_BODY()
FVector2D MoveAxes; FVector2D MoveAxes;
FVector2D LookAxes; FRotator LookRot;
UPROPERTY(EditAnywhere, Meta = (Bitmask)) UPROPERTY(EditAnywhere, Meta = (Bitmask))
int32 Buttons; int32 Buttons;
@ -38,7 +39,7 @@ struct FFFInputState
FFFInputState() FFFInputState()
: MoveAxes(FVector2D::ZeroVector) : MoveAxes(FVector2D::ZeroVector)
, LookAxes(FVector2D::ZeroVector) , LookRot(FRotator::ZeroRotator)
, Buttons(0) , Buttons(0)
, DisabledButtons(0) , DisabledButtons(0)
{ {
@ -50,17 +51,25 @@ struct FFFInputCondition
{ {
GENERATED_BODY() GENERATED_BODY()
// Buttons required for this specific condition to be valid /** Buttons required for this specific condition to be valid */
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 RequiredButtons; int32 RequiredButtons;
// The button state required for condition to be valid i.e. pressed or released /** The button state required for condition to be valid i.e. pressed or released */
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
EFFButtonState RequiredButtonState; EFFButtonState RequiredButtonState;
/*
* How many ticks that can be checked since the last valid input condition was found before
* this condition is considered not valid/found in the input buffer
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 TimeoutDuration;
FFFInputCondition() FFFInputCondition()
: RequiredButtons(0) : RequiredButtons(0)
, RequiredButtonState(EFFButtonState::BTNS_Pressed) , RequiredButtonState(EFFButtonState::BTNS_Pressed)
, TimeoutDuration(0)
{ {
} }
}; };
@ -73,11 +82,7 @@ struct FFFInputSequence
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FFFInputCondition> Sequence; TArray<FFFInputCondition> Sequence;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 MaxDuration;
FFFInputSequence() FFFInputSequence()
: MaxDuration(0)
{ {
} }
}; };
@ -96,6 +101,8 @@ public:
void DisableMostRecentInput(); void DisableMostRecentInput();
void FlushBuffer();
protected: protected:
/** The underlying buffer data structure for holding past input states */ /** The underlying buffer data structure for holding past input states */
TCircleBuffer<FFFInputState, 120> InputBuffer; TCircleBuffer<FFFInputState, 120> InputBuffer;

View File

@ -7,6 +7,7 @@
// UE includes // UE includes
#include "EnhancedInputSubsystems.h" #include "EnhancedInputSubsystems.h"
#include "Kismet/KismetMathLibrary.h"
AFFPlayerController::AFFPlayerController() AFFPlayerController::AFFPlayerController()
{ {
@ -25,13 +26,37 @@ void AFFPlayerController::ModifyRawInput()
} }
void AFFPlayerController::FixedTick(float OneFrame) bool AFFPlayerController::HasMoveInput()
{
// TODO: configurable deadzone
return !ModifiedInput.MoveAxes.IsNearlyZero();
}
void AFFPlayerController::ConsumeMoveInput()
{
RawInput.MoveAxes = FVector2D::ZeroVector;
}
FVector AFFPlayerController::GetMoveInputAsWorldDirection() const
{
FRotator ViewRotationYaw = FRotator(0.0f, ModifiedInput.LookRot.Yaw, 0.0f);
FVector ForwardInput = UKismetMathLibrary::GetForwardVector(ViewRotationYaw) * ModifiedInput.MoveAxes.Y;
FVector RightInput = UKismetMathLibrary::GetRightVector(ViewRotationYaw) * ModifiedInput.MoveAxes.X;
return ForwardInput + RightInput;
}
void AFFPlayerController::AddCurrentInputToBuffer()
{ {
//UnacknowledgedInputs.Push(RawInput); //UnacknowledgedInputs.Push(RawInput);
InputBuffer->AddInput(ModifiedInput); InputBuffer->AddInput(ModifiedInput);
//SendInputsToRemote(); //SendInputsToRemote();
} }
bool AFFPlayerController::CheckInputSequence(const FFFInputSequence& InInputSequence) bool AFFPlayerController::CheckInputSequence(const FFFInputSequence& InInputSequence)
{ {
return InputBuffer->CheckInputSequence(InInputSequence); return InputBuffer->CheckInputSequence(InInputSequence);
@ -56,6 +81,7 @@ bool AFFPlayerController::CheckInputSequences(const TArray<FFFInputSequence>& In
return false; return false;
} }
void AFFPlayerController::DisableMostRecentInput() void AFFPlayerController::DisableMostRecentInput()
{ {
InputBuffer->DisableMostRecentInput(); InputBuffer->DisableMostRecentInput();
@ -77,3 +103,12 @@ void AFFPlayerController::SetupInputComponent()
} }
} }
} }
void AFFPlayerController::FlushPressedKeys()
{
Super::FlushPressedKeys();
RawInput.Buttons = 0;
RawInput.MoveAxes = FVector2D::ZeroVector;
}

View File

@ -20,7 +20,7 @@
* unacknowledged inputs to a remote client or server for processing. * unacknowledged inputs to a remote client or server for processing.
*/ */
UCLASS() UCLASS()
class UNREALFIGHTINGFRAMEWORK_API AFFPlayerController : public APlayerController, public IFFSystemInterface, public IFFStateOwnerInterface class UNREALFIGHTINGFRAMEWORK_API AFFPlayerController : public APlayerController, public IFFStateOwnerInterface
{ {
GENERATED_BODY() GENERATED_BODY()
@ -36,7 +36,7 @@ public:
* @brief Gets the current input after cleaning/modifying the raw input state * @brief Gets the current input after cleaning/modifying the raw input state
* @return the current input * @return the current input
*/ */
virtual FFFInputState GetModifiedInput() const { return ModifiedInput; }; virtual FFFInputState GetModifiedInput() const { return ModifiedInput; }
/** /**
* @brief Function called before inputs are passed to the game logic to update the game state. * @brief Function called before inputs are passed to the game logic to update the game state.
@ -47,9 +47,23 @@ public:
*/ */
virtual void ModifyRawInput(); virtual void ModifyRawInput();
// IFFSystemInterface interface /**
virtual void FixedTick(float OneFrame) override; * @brief Checks if the current input state has movement input being applied
// End of IFFSystemInterface * @return True if move axes is non-zero, false otherwise
*/
UFUNCTION(BlueprintPure)
bool HasMoveInput();
/**
* @brief Zeroes out the raw movement axes after input has been processed
*/
void ConsumeMoveInput();
/**
* @brief Adds the current state of the player input to the input buffer for processing and
* interpretation after input has been cleaned
*/
void AddCurrentInputToBuffer();
// IFFStateOwnerInterface // IFFStateOwnerInterface
UFUNCTION() UFUNCTION()
@ -58,11 +72,15 @@ public:
UFUNCTION() UFUNCTION()
virtual bool CheckInputSequences(const TArray<FFFInputSequence>& InputSequences) override; virtual bool CheckInputSequences(const TArray<FFFInputSequence>& InputSequences) override;
virtual FVector GetMoveInputAsWorldDirection() const override;
virtual void DisableMostRecentInput() override; virtual void DisableMostRecentInput() override;
// End of IFFStateOwnerInterface // End of IFFStateOwnerInterface
// APlayerController interface // APlayerController interface
virtual void SetupInputComponent() override; virtual void SetupInputComponent() override;
virtual void FlushPressedKeys() override;
// End of APlayerController interface // End of APlayerController interface
protected: protected:

View File

@ -57,7 +57,7 @@ bool UFFState::CanTransition(const FFFStateContext& InStateContext)
return false; return false;
} }
return OnCanTransition(InStateContext); return OnCanTransition(InStateContext) && !bIsFollowupState;
} }
@ -65,14 +65,21 @@ void UFFState::Enter(const FFFStateContext& InStateContext)
{ {
PlayMontage(InStateContext); PlayMontage(InStateContext);
if(bDisableMostRecentInputOnEntry)
{
IFFStateOwnerInterface* PC = Cast<IFFStateOwnerInterface>(InStateContext.Owner);
if(PC)
{
PC->DisableMostRecentInput();
}
}
OnEnter(InStateContext); OnEnter(InStateContext);
} }
void UFFState::Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason) void UFFState::Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason)
{ {
if(InStateContext.Avatar && if(InStateContext.Avatar &&
(bStopMontageOnStateEnd && StateFinishReason == EFFStateFinishReason::SFT_DurationMetOrExceeded || StateFinishReason == EFFStateFinishReason::SFT_Interrupted) || (bStopMontageOnStateEnd && StateFinishReason == EFFStateFinishReason::SFT_DurationMetOrExceeded || StateFinishReason == EFFStateFinishReason::SFT_Interrupted) ||
(bStopMontageOnMovementModeChange && StateFinishReason == EFFStateFinishReason::SFT_NotInReqMovementMode)) (bStopMontageOnMovementModeChange && StateFinishReason == EFFStateFinishReason::SFT_NotInReqMovementMode))
@ -133,28 +140,24 @@ void UFFState::MovementModeChanged(EMovementMode PrevMovementMode, uint8 Previou
{ {
OnMovementModeChanged(PrevMovementMode, PreviousCustomMode, OnMovementModeChanged(PrevMovementMode, PreviousCustomMode,
NewMovementMode, NewCustomMode, InStateContext); NewMovementMode, NewCustomMode, InStateContext);
// TODO: Movement mode MOVE_None means movement is disabled but in this context it means no movement
// mode is specifically required to stay in this state i.e. changing from falling to walking
// will not exit out of this state. I think I want to use my own movement mode enum just so I
// can explicitly document this is what is meant by none
if((ReqMovementMode != EMovementMode::MOVE_None && NewMovementMode != ReqMovementMode) ||
((ReqMovementMode == MOVE_Custom) && NewCustomMode != RequiredCustomMode))
{
Finish(InStateContext, EFFStateFinishReason::SFT_NotInReqMovementMode);
}
} }
void UFFState::Hit(const FFFStateContext& InStateContext) void UFFState::AttackHit(const FHitResult& HitResult, const FFFStateContext& InStateContext)
{ {
OnHit(InStateContext); OnAttackHit(HitResult, InStateContext);
} }
void UFFState::Block(const FFFStateContext& InStateContext) void UFFState::HitTaken(const FFFStateContext& InStateContext)
{ {
OnBlock(InStateContext); OnHitTaken(InStateContext);
}
void UFFState::BlockTaken(const FFFStateContext& InStateContext)
{
OnBlockTaken(InStateContext);
} }
@ -166,18 +169,24 @@ void UFFState::Finish(const FFFStateContext& InStateContext, EFFStateFinishReaso
// the appropriate flags are set. I think having this state finish reason is good but I may want // the appropriate flags are set. I think having this state finish reason is good but I may want
// to rethink the way we handle logic for ending a state and which class is in charge of handling // to rethink the way we handle logic for ending a state and which class is in charge of handling
// what // what
if(GetFollowupState(InStateContext) != NAME_None)
{
InStateContext.Parent->GoToState(GetFollowupState(InStateContext), StateFinishReason);
}
else
{
InStateContext.Parent->GoToEntryState(StateFinishReason); InStateContext.Parent->GoToEntryState(StateFinishReason);
} }
void UFFState::RegisterInputHandler(const FFFInputSequence& InRequiredSequence, FFFInputEventDelegate InDelegate)
{
FFFInputEventHandler TempHandler;
TempHandler.RequiredSequence = InRequiredSequence;
TempHandler.Delegate = InDelegate;
InputHandlers.Add(TempHandler);
} }
FName UFFState::GetFollowupState_Implementation(const FFFStateContext& InStateContext)
{
return FollowupState;
}
void UFFState::PlayMontage(const FFFStateContext& InStateContext) void UFFState::PlayMontage(const FFFStateContext& InStateContext)
{ {
// TODO: think of a better way to handle optionally playing montages other than the one set as MontageToPlay // TODO: think of a better way to handle optionally playing montages other than the one set as MontageToPlay

View File

@ -35,6 +35,11 @@ struct FFFStateContext
DECLARE_DYNAMIC_DELEGATE_OneParam(FFFInputEventDelegate, const FFFStateContext&, InStateContext); DECLARE_DYNAMIC_DELEGATE_OneParam(FFFInputEventDelegate, const FFFStateContext&, InStateContext);
/*
* TODO: check if this is even used anymore and if not delete? I recall using this in a few
* states/actions but it was a pain to use and I switched to checking an input sequence directly in
* a state's update function
*/
USTRUCT(BlueprintType) USTRUCT(BlueprintType)
struct FFFInputEventHandler struct FFFInputEventHandler
{ {
@ -52,10 +57,15 @@ struct FFFInputEventHandler
UENUM(BlueprintType) UENUM(BlueprintType)
enum class EFFStateFinishReason : uint8 enum class EFFStateFinishReason : uint8
{ {
// TODO: document /** State ended due to another state's entry condition being satisfied */
SFT_Interrupted UMETA(DisplayName="Interrupted"), SFT_Interrupted UMETA(DisplayName="Interrupted"),
/** State ended because the number of ticks in this state exceeded state's duration property */
SFT_DurationMetOrExceeded UMETA(DisplayName="Duration Reached"), SFT_DurationMetOrExceeded UMETA(DisplayName="Duration Reached"),
/** State ended because the owner is no longer in the movement mode required by the state */
SFT_NotInReqMovementMode UMETA(DisplayName="Not In Required Movement Mode"), SFT_NotInReqMovementMode UMETA(DisplayName="Not In Required Movement Mode"),
SFT_MAX UMETA(Hidden) SFT_MAX UMETA(Hidden)
}; };
@ -112,6 +122,13 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties")
TArray<FFFInputSequence> InputSequences; TArray<FFFInputSequence> InputSequences;
/**
* Should the most recent input in the controlling player's input buffer be disabled when
* entering this state?
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties")
bool bDisableMostRecentInputOnEntry = true;
/** /**
* If true the state can transition from itself into itself without having to go through * If true the state can transition from itself into itself without having to go through
* another state like Idle * another state like Idle
@ -119,11 +136,23 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties")
bool bCanTransitionToSelf = false; bool bCanTransitionToSelf = false;
/**
* Specifies whether a state is a followup state or not.
*
* Followup states are states that should only be transitioned into from another specific state
* but otherwise have no other specific conditions that need to be met to transition.
* Setting this flag to true when there are no other conditions prevents this state from
* constantly being transitioned into by the state machine.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties")
TEnumAsByte<EMovementMode> ReqMovementMode; bool bIsFollowupState = false;
/**
* State to transition to when this state is finished. If left blank then the entry state
* of the state machine will be transitioned into.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties")
uint8 RequiredCustomMode; FName FollowupState = NAME_None;
/** /**
* Animation to begin playing when this state is entered * Animation to begin playing when this state is entered
@ -168,7 +197,7 @@ public:
* *
* Calls appropriate Blueprint hooks. * Calls appropriate Blueprint hooks.
* *
* TODO: document StateFinishReason * @param StateFinishReason the reason the state is ending i.e. the state's duration elapse
*/ */
virtual void Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason); virtual void Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason);
@ -181,20 +210,40 @@ public:
*/ */
virtual void Update(float OneFrame, const FFFStateContext& InStateContext); virtual void Update(float OneFrame, const FFFStateContext& InStateContext);
// TODO: document /**
* Called whenever state owners lands on a walkable surface after they were previously falling.
*
* Calls appropriate Blueprint hooks.
*
* @param Hit Hit result for the surface that was landed on
*/
virtual void Landed(const FHitResult& Hit, const FFFStateContext& InStateContext); virtual void Landed(const FHitResult& Hit, const FFFStateContext& InStateContext);
// TODO: document /**
* @brief Called whenever the state's owner current movement mode changes
*
* Calls appropriate Blueprint hooks.
*
* @param PrevMovementMode previous movement mode.
* @param PreviousCustomMode previous movement mode if PrevMovementMode is MOVE_Custom
* @param NewMovementMode new movement mode
* @param NewCustomMode new movement mode if NewMovementMode is MOVE_Custom
*/
virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode,
EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext); EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext);
// TODO: document // TODO: document
// TODO: pass in hitdata struct as well virtual void AttackHit(const FHitResult& HitResult, const FFFStateContext& InStateContext);
virtual void Hit(const FFFStateContext& InStateContext);
// TODO: document // TODO: document
// TODO: call this callback when the avatar is hit
// TODO: pass in hitdata struct as well // TODO: pass in hitdata struct as well
virtual void Block(const FFFStateContext& InStateContext); virtual void HitTaken(/*const FHitParams& HitParams,*/ const FFFStateContext& InStateContext);
// TODO: document
// TODO: call this callback when the avatar blocks a hit
// TODO: pass in hitdata struct as well
virtual void BlockTaken(const FFFStateContext& InStateContext);
/** /**
* Called when you want to exit from this state but no eligible transitions exist to other states, * Called when you want to exit from this state but no eligible transitions exist to other states,
@ -212,12 +261,7 @@ public:
UFUNCTION(BlueprintCallable) UFUNCTION(BlueprintCallable)
virtual void Finish(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason); virtual void Finish(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason);
// TODO: document /** @brief Blueprint hook for when the state is first created. */
UFUNCTION(BlueprintCallable)
virtual void RegisterInputHandler(
const FFFInputSequence& InRequiredSequence, FFFInputEventDelegate InDelegate);
// TODO: document
UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events") UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events")
void OnInit(const FFFStateContext& InStateContext); void OnInit(const FFFStateContext& InStateContext);
@ -247,9 +291,23 @@ public:
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnUpdate(float OneFrame, const FFFStateContext& InStateContext); void OnUpdate(float OneFrame, const FFFStateContext& InStateContext);
/**
* Returns the next state to transition into when this state finishes.
*
* This is called during the Finish function, either when the state duration is reached or the
* state is manually finished due to some other logic.
*
* By default this returns the name of the state specified by the FollowupState property or
* NAME_None if the FollowupState is not specified.
*/
UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events")
FName GetFollowupState(const FFFStateContext& InStateContext);
// TODO: Don't like this at all and needs to be refactored or redesigned
UFUNCTION(BlueprintCallable, Category="UFF|State|Animations") UFUNCTION(BlueprintCallable, Category="UFF|State|Animations")
void PlayMontage(const FFFStateContext& InStateContext); void PlayMontage(const FFFStateContext& InStateContext);
// TODO: Don't like this at all and needs to be refactored or redesigned
/** /**
* Blueprint hook for overriding the logic for when a anim montage plays at the start of a state * Blueprint hook for overriding the logic for when a anim montage plays at the start of a state
* @param InStateContext * @param InStateContext
@ -257,11 +315,23 @@ public:
UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events") UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events")
void OnPlayMontage(const FFFStateContext& InStateContext); void OnPlayMontage(const FFFStateContext& InStateContext);
// TODO: document /**
* @brief Blueprint hook for whenever state owners lands on a walkable surface after they were
* previously falling.
*
* @param Hit Hit result for the surface that was landed on
*/
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnLanded(const FHitResult& Hit, const FFFStateContext& InStateContext); void OnLanded(const FHitResult& Hit, const FFFStateContext& InStateContext);
// TODO: document /**
* @brief Blueprint hook whenever the state's owner current movement mode changes
*
* @param PrevMovementMode previous movement mode.
* @param PreviousCustomMode previous movement mode if PrevMovementMode is MOVE_Custom
* @param NewMovementMode new movement mode
* @param NewCustomMode new movement mode if NewMovementMode is MOVE_Custom
*/
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode,
EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext); EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext);
@ -269,12 +339,16 @@ public:
// TODO: document // TODO: document
// TODO: pass in hitdata struct as well // TODO: pass in hitdata struct as well
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnHit(const FFFStateContext& InStateContext); void OnAttackHit(const FHitResult& Hit, const FFFStateContext& InStateContext);
// TODO: document
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnHitTaken(/*const FHitParams,*/ const FFFStateContext& InStateContext);
// TODO: document // TODO: document
// TODO: pass in hitdata struct as well // TODO: pass in hitdata struct as well
UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events")
void OnBlock(const FFFStateContext& InStateContext); void OnBlockTaken(/*const FHitParams,*/ const FFFStateContext& InStateContext);
// UObject interface // UObject interface
virtual UWorld* GetWorld() const override; virtual UWorld* GetWorld() const override;

View File

@ -103,20 +103,9 @@ void UFFStateMachineComponent::GoToState(UFFState* NewState, EFFStateFinishReaso
check(CurrentState); check(CurrentState);
check(NewState); check(NewState);
// if state being transitioned into requires an input sequence we need to disable the most
// recent input in the player controller input buffer to prevent unwanted "double firing" of
// actions that have short durations but very lenient required input sequences
if(NewState->InputSequences.Num() > 0)
{
IFFStateOwnerInterface* PC = Cast<IFFStateOwnerInterface>(Owner);
if(PC)
{
PC->DisableMostRecentInput();
}
}
CurrentState->Exit(GetCurrentStateContext(), StateFinishReason); CurrentState->Exit(GetCurrentStateContext(), StateFinishReason);
TicksInState = 0; TicksInState = 0;
OnLandedTick = 0;
TickStateWasEntered = GetWorld()->GetGameState<AFFGameState>()->GetCurrentTick(); TickStateWasEntered = GetWorld()->GetGameState<AFFGameState>()->GetCurrentTick();
CurrentSubStateLabel = NAME_None; CurrentSubStateLabel = NAME_None;
NewState->Enter(GetCurrentStateContext()); NewState->Enter(GetCurrentStateContext());
@ -161,6 +150,8 @@ void UFFStateMachineComponent::Landed(const FHitResult& Hit)
{ {
check(CurrentState) check(CurrentState)
OnLandedTick = GetWorld()->GetGameState<AFFGameState>()->GetCurrentTick();
CurrentState->Landed(Hit, GetCurrentStateContext()); CurrentState->Landed(Hit, GetCurrentStateContext());
} }
@ -183,6 +174,16 @@ void UFFStateMachineComponent::MovementModeChanged(EMovementMode PrevMovementMod
NewMovementMode, NewCustomMode, GetCurrentStateContext()); NewMovementMode, NewCustomMode, GetCurrentStateContext());
} }
void UFFStateMachineComponent::AttackHit(const FHitResult& HitResult)
{
check(CurrentState)
CurrentState->AttackHit(HitResult, GetCurrentStateContext());
}
void UFFStateMachineComponent::TakeHit()
{
CurrentState->HitTaken(GetCurrentStateContext());
}
void UFFStateMachineComponent::FixedTick(float OneFrame) void UFFStateMachineComponent::FixedTick(float OneFrame)
{ {
@ -233,7 +234,7 @@ UFFState* UFFStateMachineComponent::FindStateWithName(FName StateName)
} }
} }
UE_LOG(LogTemp, Warning, UE_LOG(LogTemp, Error,
TEXT("Could not find state in state machine with name %s"), *StateName.ToString()); TEXT("Could not find state in state machine with name %s"), *StateName.ToString());
return nullptr; return nullptr;

View File

@ -73,8 +73,10 @@ public:
* *
* Triggers the Exit callback on the CurrentState and the Enter callback on the new state * Triggers the Exit callback on the CurrentState and the Enter callback on the new state
* *
* TODO: document StateFinishReason * @param StateFinishReason the reason the current state is ending i.e. the current state's
* duration elapsed
*/ */
UFUNCTION(BlueprintCallable)
void GoToState(FName NewStateName, EFFStateFinishReason StateFinishReason); void GoToState(FName NewStateName, EFFStateFinishReason StateFinishReason);
/** /**
@ -82,14 +84,16 @@ public:
* *
* Triggers the Exit callback on the CurrentState and the Enter callback on the new state * Triggers the Exit callback on the CurrentState and the Enter callback on the new state
* *
* TODO: document StateFinishReason * @param StateFinishReason the reason the current state is ending i.e. the current state's
* duration elapsed
*/ */
void GoToState(UFFState* NewState, EFFStateFinishReason StateFinishReason); void GoToState(UFFState* NewState, EFFStateFinishReason StateFinishReason);
/** /**
* Transitions from CurrentState to the default entry state * Transitions from CurrentState to the default entry state
* *
* TODO: document StateFinishReason * @param StateFinishReason the reason the current state is ending i.e. the current state's
* duration elapsed
*/ */
void GoToEntryState(EFFStateFinishReason StateFinishReason); void GoToEntryState(EFFStateFinishReason StateFinishReason);
@ -104,6 +108,14 @@ public:
UFUNCTION(BlueprintPure) UFUNCTION(BlueprintPure)
FORCEINLINE int64 GetTickStateWasEntered() const { return TickStateWasEntered; } FORCEINLINE int64 GetTickStateWasEntered() const { return TickStateWasEntered; }
/**
* Returns the tick number which corresponds to when the On Landed event fired
*
* The tick number represents the ticks that have elapsed since the game began.
*/
UFUNCTION(BlueprintPure)
FORCEINLINE int64 GetOnLandedTick() const { return OnLandedTick; }
UFUNCTION(BlueprintPure) UFUNCTION(BlueprintPure)
const UFFState* GetCurrentState() const { return const_cast<UFFState*>(CurrentState); } const UFFState* GetCurrentState() const { return const_cast<UFFState*>(CurrentState); }
@ -123,16 +135,38 @@ public:
void SetSubStateLabel(FName InSubStateLabel); void SetSubStateLabel(FName InSubStateLabel);
/** /**
* * TODO: document
*/ */
FFFStateContext GetCurrentStateContext(); FFFStateContext GetCurrentStateContext();
// Events // Events
/**
* Called whenever state machine's owner lands on a walkable surface after they were previously falling.
*
* @param Hit Hit result for the surface that was landed on
*/
virtual void Landed(const FHitResult& Hit); virtual void Landed(const FHitResult& Hit);
/**
* @brief Called whenever the state machine's owner current movement mode changes
*
* Calls appropriate Blueprint hooks.
*
* @param PrevMovementMode previous movement mode.
* @param PreviousCustomMode previous movement mode if PrevMovementMode is MOVE_Custom
* @param NewMovementMode new movement mode
* @param NewCustomMode new movement mode if NewMovementMode is MOVE_Custom
*/
virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode,
EMovementMode NewMovementMode, uint8 NewCustomMode); EMovementMode NewMovementMode, uint8 NewCustomMode);
// TODO: document
virtual void AttackHit(const FHitResult& HitResult);
// TODO: document
// TODO: bring HitParams struct into FightingFramework
virtual void TakeHit(/* const FHitParams& HitParams*/);
FOnStateTransitionSignature OnStateTransition; FOnStateTransitionSignature OnStateTransition;
// IFFSystemInterface interface // IFFSystemInterface interface
@ -158,12 +192,19 @@ protected:
int64 TicksInState; int64 TicksInState;
/** /**
* The tick number which corresponds to when this state was entered into. * The tick number which corresponds to when the current state was entered.
* *
* The tick number represents the ticks that have elapsed since the game began. * The tick number represents the ticks that have elapsed since the game began.
*/ */
int64 TickStateWasEntered; int64 TickStateWasEntered;
/**
* The tick number which corresponds to when the On Landed event was called for the current tick.
*
* The tick number represents the ticks that have elapsed since the game began.
*/
int64 OnLandedTick;
/** Current active state for this state machine */ /** Current active state for this state machine */
UPROPERTY() UPROPERTY()
UFFState* CurrentState; UFFState* CurrentState;
@ -171,6 +212,7 @@ protected:
/** Current SubState label or NAME_None if there is no SubState label set for the current state */ /** Current SubState label or NAME_None if there is no SubState label set for the current state */
FName CurrentSubStateLabel; FName CurrentSubStateLabel;
// TODO: should be a TMap
// States that have been added // States that have been added
UPROPERTY() UPROPERTY()
TArray<UFFState*> States; TArray<UFFState*> States;

View File

@ -2,6 +2,10 @@
#pragma once #pragma once
// FF includes
#include "Input/FFInputBufferComponent.h"
// UE includes
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "UObject/Interface.h" #include "UObject/Interface.h"
@ -14,18 +18,46 @@ class UFFStateOwnerInterface : public UInterface
}; };
/** /**
* * TODO: document
*/ */
// TODO: why is this an interface again?
class UNREALFIGHTINGFRAMEWORK_API IFFStateOwnerInterface class UNREALFIGHTINGFRAMEWORK_API IFFStateOwnerInterface
{ {
GENERATED_BODY() GENERATED_BODY()
public: public:
/**
* @brief Checks to see if the provided input sequence has a valid matching series of inputs in
* the input buffer
*
* @param InInputSequence sequence of inputs to check in the input buffer
* @return true if the input sequence has a valid matching series of inputs in the input buffer
*/
UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface") UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface")
virtual bool CheckInputSequence(const FFFInputSequence& InInputSequence) = 0; virtual bool CheckInputSequence(const FFFInputSequence& InInputSequence) = 0;
/**
* @brief Checks to see if at least one of the provided input sequences has a valid matching
* series of inputs in the input buffer
*
* @param InInputSequences array of sequences of inputs to check in the input buffer
* @return true if at least one input sequence has a valid matching series of inputs in the
* input buffer
*/
UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface") UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface")
virtual bool CheckInputSequences(const TArray<FFFInputSequence>& InInputSequences) = 0; virtual bool CheckInputSequences(const TArray<FFFInputSequence>& InInputSequences) = 0;
/**
* @brief Convert 2D movement input a 3D world direction relative to the view orientation
*
* @return direction of player's 2D movement input in world space
*/
virtual FVector GetMoveInputAsWorldDirection() const = 0;
/**
* @brief Sets the current tick's input in the input buffer as disabled, preventing it from
* being considered as valid input for input sequence interpretation
*/
virtual void DisableMostRecentInput() = 0; virtual void DisableMostRecentInput() = 0;
}; };

View File

@ -0,0 +1,139 @@
// Unreal Fighting Framework by Kevin Poretti
// FF includes
#include "Input/FFInputBufferComponent.h"
// UE includes
#include "CoreMinimal.h"
#include "Misc/AutomationTest.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FInputBufferTest, "FightingFramework.Input.InputBuffer",
EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)
bool FInputBufferTest::RunTest(const FString& Parameters)
{
TStrongObjectPtr<UWorld> World = TStrongObjectPtr(UWorld::CreateWorld(EWorldType::Game, false));
// Create a dummy actor
TStrongObjectPtr<AActor> InputBufferOwner = TStrongObjectPtr<AActor>(World->SpawnActor<AActor>());
TestTrue(TEXT("Create InputBufferOwner"), InputBufferOwner.IsValid());
UFFInputBufferComponent* InputBuffer =
NewObject<UFFInputBufferComponent>(InputBufferOwner.Get(),
UFFInputBufferComponent::StaticClass(), TEXT("InputBuffer"));
InputBuffer->RegisterComponent();
// InputBuffer must be able to be created as a subobject of an actor
{
TestNotNull(TEXT("Create new InputBuffer component"), InputBuffer);
}
// InputBuffer should not attempt to evaluate an input sequence if there is only one input in
// the buffer and should return false
{
FFFInputState InputDown;
InputDown.Buttons = 0x00000001;
InputBuffer->AddInput(InputDown);
FFFInputCondition PressedCondition;
PressedCondition.RequiredButtons = 0x00000001;
PressedCondition.RequiredButtonState = EFFButtonState::BTNS_Pressed;
PressedCondition.TimeoutDuration = 0;
FFFInputSequence PressedSequence;
PressedSequence.Sequence.Add(PressedCondition);
TestFalse(TEXT("Don't try to detect a sequence if only one input is present in the buffer"),
InputBuffer->CheckInputSequence(PressedSequence));
}
InputBuffer->FlushBuffer();
// InputBuffer should not attempt to evaluate an input sequence if the sequence has no input conditions
{
FFFInputState InputUp;
InputUp.Buttons = 0x00000000;
FFFInputState InputDown;
InputDown.Buttons = 0x00000001;
InputBuffer->AddInput(InputUp);
InputBuffer->AddInput(InputDown);
FFFInputSequence EmptySequence;
TestFalse(TEXT("Don't try to detect a sequence if the sequence has no input conditions"),
InputBuffer->CheckInputSequence(EmptySequence));
}
InputBuffer->FlushBuffer();
// InputBuffer must detect simple button pressed condition
{
FFFInputState InputUp;
InputUp.Buttons = 0x00000000;
FFFInputState InputDown;
InputDown.Buttons = 0x00000001;
InputBuffer->AddInput(InputUp);
InputBuffer->AddInput(InputDown);
FFFInputCondition PressedCondition;
PressedCondition.RequiredButtons = 0x00000001;
PressedCondition.RequiredButtonState = EFFButtonState::BTNS_Pressed;
PressedCondition.TimeoutDuration = 0;
FFFInputSequence PressedSequence;
PressedSequence.Sequence.Add(PressedCondition);
TestTrue(TEXT("Detect a single button press in the buffer"),
InputBuffer->CheckInputSequence(PressedSequence));
}
InputBuffer->FlushBuffer();
// InputBuffer must detect simple button released condition
{
FFFInputState InputUp;
InputUp.Buttons = 0x0000000;
FFFInputState InputDown;
InputDown.Buttons = 0x00000001;
InputBuffer->AddInput(InputDown);
InputBuffer->AddInput(InputUp);
FFFInputCondition ReleasedCondition;
ReleasedCondition.RequiredButtons = 0x00000001;
ReleasedCondition.RequiredButtonState = EFFButtonState::BTNS_Released;
ReleasedCondition.TimeoutDuration = 0;
FFFInputSequence ReleasedSequence;
ReleasedSequence.Sequence.Add(ReleasedCondition);
TestTrue(TEXT("Detect a single button release in the buffer"),
InputBuffer->CheckInputSequence(ReleasedSequence));
}
InputBuffer->FlushBuffer();
// InputBuffer must detect simple button down condition
{
FFFInputState InputUp;
InputUp.Buttons = 0x0000000;
FFFInputState InputDown;
InputDown.Buttons = 0x00000001;
InputBuffer->AddInput(InputUp);
InputBuffer->AddInput(InputDown);
FFFInputCondition DownCondition;
DownCondition.RequiredButtons = 0x00000001;
DownCondition.RequiredButtonState = EFFButtonState::BTNS_Down;
DownCondition.TimeoutDuration = 0;
FFFInputSequence DownSequence;
DownSequence.Sequence.Add(DownCondition);
TestTrue(TEXT("Detect a single button press in the buffer"),
InputBuffer->CheckInputSequence(DownSequence));
}
World->DestroyWorld(false);
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS

View File

@ -26,6 +26,7 @@ public class UnrealFightingFramework : ModuleRules
{ {
"Core", "Core",
"EnhancedInput", "EnhancedInput",
"AIModule",
// ... add other public dependencies that you statically link with here ... // ... add other public dependencies that you statically link with here ...
} }
); );

View File

@ -108,6 +108,16 @@ public:
return true; return true;
} }
/**
* @brief Flushes the buffer of all contents
*/
void Flush()
{
WriteIdx = 0;
ReadIdx = 0;
_Num = 0;
}
/** /**
* @brief Returns the element at an index supplied to the function. * @brief Returns the element at an index supplied to the function.
* This function will account for write index offset and wraparound of the supplied index. * This function will account for write index offset and wraparound of the supplied index.