diff --git a/Content/BPML_StateMacros.uasset b/Content/BPML_StateMacros.uasset new file mode 100644 index 0000000..35fbad5 Binary files /dev/null and b/Content/BPML_StateMacros.uasset differ diff --git a/README.md b/README.md index 601c0b4..1daa9ed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# UnrealFightingEngine +# UnrealFightingFramework This library provides actors, components, and other general data structures that are useful for developing character action or fighting games. diff --git a/Source/UnrealFightingEngine/Private/UnrealFightingEngineBPLibrary.cpp b/Source/UnrealFightingEngine/Private/UnrealFightingEngineBPLibrary.cpp deleted file mode 100644 index cca6b9c..0000000 --- a/Source/UnrealFightingEngine/Private/UnrealFightingEngineBPLibrary.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "UnrealFightingEngineBPLibrary.h" -#include "UnrealFightingEngine.h" - -UUnrealFightingEngineBPLibrary::UUnrealFightingEngineBPLibrary(const FObjectInitializer& ObjectInitializer) -: Super(ObjectInitializer) -{ - -} - -float UUnrealFightingEngineBPLibrary::UnrealFightingEngineSampleFunction(float Param) -{ - return -1; -} - diff --git a/Source/UnrealFightingFramework/GameplayFramework/FFGameState.cpp b/Source/UnrealFightingFramework/GameplayFramework/FFGameState.cpp new file mode 100644 index 0000000..ebb0e9a --- /dev/null +++ b/Source/UnrealFightingFramework/GameplayFramework/FFGameState.cpp @@ -0,0 +1,25 @@ +// Project Sword & Gun Copyright Kevin Poretti + +#include "GameplayFramework/FFGameState.h" + +void AFFGameState::OnFixedTick(float OneTick) +{ +} + + +void AFFGameState::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + // 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 + AccumulatedTime += DeltaSeconds; + while(AccumulatedTime > ONE_TICK) + { + OnFixedTick(ONE_TICK); + + AccumulatedTime -= ONE_TICK; + + CurrentTick++; + } +} diff --git a/Source/UnrealFightingFramework/GameplayFramework/FFGameState.h b/Source/UnrealFightingFramework/GameplayFramework/FFGameState.h new file mode 100644 index 0000000..7e89f24 --- /dev/null +++ b/Source/UnrealFightingFramework/GameplayFramework/FFGameState.h @@ -0,0 +1,41 @@ +// Project Sword & Gun Copyright Kevin Poretti + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameState.h" +#include "FFGameState.generated.h" + +constexpr float ONE_TICK = 0.0083333333; +constexpr int64 TICKS_PER_SECOND = 120; + +/** + * + */ +UCLASS() +class UNREALFIGHTINGFRAMEWORK_API AFFGameState : public AGameState +{ + GENERATED_BODY() + +public: + + UFUNCTION(BlueprintPure) + int64 GetCurrentTick() const { return CurrentTick; } + + virtual void OnFixedTick(float OneTick); + + // Begin AActor interface + virtual void Tick(float DeltaSeconds) override; + // End AActor interface +private: + /** + * Amount of time accumulated from ticks. When Tick is called the delta time since the + * last Tick will be added to this variable. + * + * Once enough time has accumulated to simulate at least one frame (defined by the value ONE_TICK) then the + * game logic will update/tick. ONE_TICK's worth of time will be subtracted from the this variable + */ + float AccumulatedTime = 0; + + int64 CurrentTick = 0; +}; diff --git a/Source/UnrealFightingFramework/IFFSystemInterface.h b/Source/UnrealFightingFramework/IFFSystemInterface.h new file mode 100644 index 0000000..f08cef1 --- /dev/null +++ b/Source/UnrealFightingFramework/IFFSystemInterface.h @@ -0,0 +1,35 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// UE includes +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "IFFSystemInterface.generated.h" + +// This class does not need to be modified. +UINTERFACE(MinimalAPI) +class UFFSystemInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * This interface defines functions that need to be implemented for any objects that are "gameplay affecting". + * + * This ensures all gameplay effecting objects can be assumed to have certain properties that enable serialization, networking and + * some form of determinism. + */ +class UNREALFIGHTINGFRAMEWORK_API IFFSystemInterface +{ + GENERATED_BODY() + +public: + /** + * Function to be called at a fixed interval/frame rate rather than using Unreal's variable Tick function + * + * @param OneFrame the time that elapses during one fixed tick + */ + virtual void FixedTick(float OneFrame) = 0; +}; diff --git a/Source/UnrealFightingFramework/Input/FFInputBufferComponent.cpp b/Source/UnrealFightingFramework/Input/FFInputBufferComponent.cpp new file mode 100644 index 0000000..f34fc0d --- /dev/null +++ b/Source/UnrealFightingFramework/Input/FFInputBufferComponent.cpp @@ -0,0 +1,86 @@ +// Unreal Fighting Framework by Kevin Poretti + +#include "FFInputBufferComponent.h" + +UFFInputBufferComponent::UFFInputBufferComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + + +void UFFInputBufferComponent::AddInput(const FFFInputState& InputState) +{ + InputBuffer.ForcePush(InputState); +} + + +bool UFFInputBufferComponent::CheckInputSequence(const FFFInputSequence& InputSequence) +{ + if(InputSequence.Sequence.IsEmpty()) + { + UE_LOG(LogTemp, Error, + TEXT("FFInputBufferComponent :: CheckInputSequence - tried to check input sequence but it was empty")); + return false; + } + + int CondIdx = InputSequence.Sequence.Num() - 1; + int ElapsedFrames = 0; + for(int InpIdx = InputBuffer.Num() - 1; InpIdx > 1; InpIdx--) + { + int32 RequiredButtons = InputSequence.Sequence[CondIdx].RequiredButtons; + EFFButtonState RequiredButtonState = InputSequence.Sequence[CondIdx].RequiredButtonState; + int32 PrevInput = InputBuffer[InpIdx - 1].Buttons; + int32 CurrInput = InputBuffer[InpIdx].Buttons; + int32 PrevDisable = InputBuffer[InpIdx - 1].DisabledButtons; + int32 CurrDisable = InputBuffer[InpIdx].DisabledButtons; + switch (RequiredButtonState) + { + // TODO: should it be (PrevInput & RequiredButtons) == RequiredButtons or what we have now? + case EFFButtonState::BTNS_Pressed: + if(!(PrevInput & RequiredButtons | PrevDisable) && + CurrInput & RequiredButtons & ~CurrDisable) + { + CondIdx--; + } + break; + case EFFButtonState::BTNS_Released: + if(PrevInput & RequiredButtons & ~PrevDisable && + !(CurrInput & RequiredButtons | CurrDisable)) + { + CondIdx--; + } + break; + case EFFButtonState::BTNS_Down: + if(CurrInput & RequiredButtons & ~CurrDisable) + { + CondIdx--; + } + break; + // TODO: implement button held condition + /* + case EFFButtonState::BTNS_Held: + break; + */ + } + + // All conditions were met + if(CondIdx == -1) + { + return true; + } + + ElapsedFrames++; + if(ElapsedFrames > InputSequence.MaxDuration) + { + return false; + } + } + + return false; +} + +void UFFInputBufferComponent::DisableMostRecentInput() +{ + InputBuffer[InputBuffer.Num() - 1].DisabledButtons |= InputBuffer[InputBuffer.Num() - 1].Buttons; + InputBuffer[InputBuffer.Num() - 2].DisabledButtons |= InputBuffer[InputBuffer.Num() - 2].Buttons; +} diff --git a/Source/UnrealFightingFramework/Input/FFInputBufferComponent.h b/Source/UnrealFightingFramework/Input/FFInputBufferComponent.h new file mode 100644 index 0000000..698e603 --- /dev/null +++ b/Source/UnrealFightingFramework/Input/FFInputBufferComponent.h @@ -0,0 +1,102 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// FF includes +#include "Utils/TCircleBuffer.h" + +// UE includes +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" + +#include "FFInputBufferComponent.generated.h" + +UENUM(BlueprintType) +enum class EFFButtonState : uint8 +{ + BTNS_Pressed UMETA(DisplayName="Pressed"), + BTNS_Released UMETA(DisplayName="Released"), + BTNS_Down UMETA(DisplayName="Down"), + //BTNS_Held UMETA(DisplayName="Held"), + BTNS_MAX UMETA(Hidden) +}; + +/** + * Struct representing the state of a player's inputs for one frame + */ +USTRUCT() +struct FFFInputState +{ + GENERATED_BODY() + + FVector2D MoveAxes; + FVector2D LookAxes; + + UPROPERTY(EditAnywhere, Meta = (Bitmask)) + int32 Buttons; + int32 DisabledButtons; + + FFFInputState() + : MoveAxes(FVector2D::ZeroVector) + , LookAxes(FVector2D::ZeroVector) + , Buttons(0) + , DisabledButtons(0) + { + } +}; + +USTRUCT(BlueprintType) +struct FFFInputCondition +{ + GENERATED_BODY() + + // Buttons required for this specific condition to be valid + UPROPERTY(EditAnywhere, BlueprintReadWrite) + int32 RequiredButtons; + + // The button state required for condition to be valid i.e. pressed or released + UPROPERTY(EditAnywhere, BlueprintReadWrite) + EFFButtonState RequiredButtonState; + + FFFInputCondition() + : RequiredButtons(0) + , RequiredButtonState(EFFButtonState::BTNS_Pressed) + { + } +}; + +USTRUCT(BlueprintType) +struct FFFInputSequence +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TArray Sequence; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + int32 MaxDuration; + + FFFInputSequence() + : MaxDuration(0) + { + } +}; + +UCLASS( ClassGroup=(UnrealFightingFramework), meta=(BlueprintSpawnableComponent) ) +class UNREALFIGHTINGFRAMEWORK_API UFFInputBufferComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UFFInputBufferComponent(); + + void AddInput(const FFFInputState& InputState); + + bool CheckInputSequence(const FFFInputSequence& InputSequence); + + void DisableMostRecentInput(); + +protected: + /** The underlying buffer data structure for holding past input states */ + TCircleBuffer InputBuffer; +}; diff --git a/Source/UnrealFightingFramework/Input/FFPlayerController.cpp b/Source/UnrealFightingFramework/Input/FFPlayerController.cpp new file mode 100644 index 0000000..c8fea47 --- /dev/null +++ b/Source/UnrealFightingFramework/Input/FFPlayerController.cpp @@ -0,0 +1,79 @@ +// Unreal Fighting Framework by Kevin Poretti + +#include "FFPlayerController.h" + +// FF includes +#include "FFInputBufferComponent.h" + +// UE includes +#include "EnhancedInputSubsystems.h" + +AFFPlayerController::AFFPlayerController() +{ + InputBuffer = CreateDefaultSubobject(TEXT("InputBuffer")); +} + + +void AFFPlayerController::SendInputsToRemote() const +{ +} + + +void AFFPlayerController::ModifyRawInput() +{ + ModifiedInput = RawInput; +} + + +void AFFPlayerController::FixedTick(float OneFrame) +{ + //UnacknowledgedInputs.Push(RawInput); + InputBuffer->AddInput(ModifiedInput); + //SendInputsToRemote(); +} + +bool AFFPlayerController::CheckInputSequence(const FFFInputSequence& InInputSequence) +{ + return InputBuffer->CheckInputSequence(InInputSequence); +} + + +bool AFFPlayerController::CheckInputSequences(const TArray& InputSequences) +{ + // no input conditions to check + if(InputSequences.IsEmpty()) + { + return true; + } + + for(const FFFInputSequence& ThisInputSequence : InputSequences) + { + if(InputBuffer->CheckInputSequence(ThisInputSequence)) + { + return true; + } + } + return false; +} + +void AFFPlayerController::DisableMostRecentInput() +{ + InputBuffer->DisableMostRecentInput(); +} + + +void AFFPlayerController::SetupInputComponent() +{ + Super::SetupInputComponent(); + + if (ULocalPlayer* LocalPlayer = Cast(Player)) + { + if (UEnhancedInputLocalPlayerSubsystem* InputSystem = LocalPlayer->GetSubsystem()) + { + if (!DefaultInputMapping.IsNull()) + { + InputSystem->AddMappingContext(DefaultInputMapping.LoadSynchronous(), 0); + } + } + } +} diff --git a/Source/UnrealFightingFramework/Input/FFPlayerController.h b/Source/UnrealFightingFramework/Input/FFPlayerController.h new file mode 100644 index 0000000..e1fcc05 --- /dev/null +++ b/Source/UnrealFightingFramework/Input/FFPlayerController.h @@ -0,0 +1,92 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// FF includes +#include "FFInputBufferComponent.h" +#include "IFFSystemInterface.h" +#include "Utils/TCircleBuffer.h" +#include "State/IFFStateOwnerInterface.h" + +// UE includes +#include "CoreMinimal.h" +#include "GameFramework/PlayerController.h" +#include "InputMappingContext.h" + +#include "FFPlayerController.generated.h" + +/** + * A class that collects player inputs, stores them in an input buffer, and sends a rolling window of + * unacknowledged inputs to a remote client or server for processing. + */ +UCLASS() +class UNREALFIGHTINGFRAMEWORK_API AFFPlayerController : public APlayerController, public IFFSystemInterface, public IFFStateOwnerInterface +{ + GENERATED_BODY() + +public: + AFFPlayerController(); + + /** + * Sends all unacknowledged inputs to the remote + */ + virtual void SendInputsToRemote() const; + + /** + * @brief Gets the current input after cleaning/modifying the raw input state + * @return the current input + */ + virtual FFFInputState GetModifiedInput() const { return ModifiedInput; }; + + /** + * @brief Function called before inputs are passed to the game logic to update the game state. + * This is a chance for the application to mutate inputs before the game uses them to update game state. + * Examples would include treating opposite directional inputs being held cancelling each other out or setting + * a "neutral input" flag when no directional inputs are being held. + * For stick/axis values this can be clamping those values or normalizing the Move and Look direction vectors. + */ + virtual void ModifyRawInput(); + + // IFFSystemInterface interface + virtual void FixedTick(float OneFrame) override; + // End of IFFSystemInterface + + // IFFStateOwnerInterface + UFUNCTION() + virtual bool CheckInputSequence(const FFFInputSequence& InInputSequence) override; + + UFUNCTION() + virtual bool CheckInputSequences(const TArray& InputSequences) override; + + virtual void DisableMostRecentInput() override; + // End of IFFStateOwnerInterface + + // APlayerController interface + virtual void SetupInputComponent() override; + // End of APlayerController interface + +protected: + /** MappingContext */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UFF|Input", meta = (AllowPrivateAccess = "true")) + TSoftObjectPtr DefaultInputMapping; + + /** Input Buffer component */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UFF|Input", meta = (AllowPrivateAccess = "true")) + UFFInputBufferComponent* InputBuffer; + + /** Current state of the player's inputs */ + FFFInputState RawInput; + + /** Current state of the player's inputs after performing cleaning and modifications */ + FFFInputState ModifiedInput; + + /** + * Rolling window of the player's past inputs that have yet to be + * acknowledged and simulated by the remote machine + * + * This ring buffer should be initialized to be the size of the past X frames + * you want the remote machine to re-simulate, where X is the oldest input you want to + * allow to be re-simulated. + */ + TCircleBuffer UnacknowledgedInputs; +}; diff --git a/Source/UnrealFightingFramework/State/FFState.cpp b/Source/UnrealFightingFramework/State/FFState.cpp new file mode 100644 index 0000000..2693143 --- /dev/null +++ b/Source/UnrealFightingFramework/State/FFState.cpp @@ -0,0 +1,240 @@ +// Unreal Fighting Framework by Kevin Poretti + +#include "FFState.h" + +// FF includes +#include "FFStateMachineComponent.h" +#include "IFFStateAvatarInterface.h" +#include "IFFStateOwnerInterface.h" + +// UE includes +#include "Components/SkeletalMeshComponent.h" + +void UFFState::Init(const FFFStateContext& InStateContext) +{ + OnInit(InStateContext); +} + +bool UFFState::CanTransition(const FFFStateContext& InStateContext) +{ + /** + * Check to see if avatar implements StateAvatarInterface + * if so then + * Check if the avatar is in the correct stance to perform this action + * Check if the state is enabled + * + * Check to see if the owner implements the StateOwnerInterface + * Check input conditions if there are any + * + * If at least one input condition is good then we can transition + * so return true otherwise return false + */ + IFFStateAvatarInterface* SAI = Cast(InStateContext.Avatar); + if(SAI) + { + if(!(SAI->CheckStance(StanceRequired) && SAI->CheckStateEnabled(StateType))) + { + return false; + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("CanTransition :: Avatar of FFFStateContext does not implement IFFStateAvatarInterface")); + return false; + } + + IFFStateOwnerInterface* SOI = Cast(InStateContext.Owner); + if(SOI) + { + if(!SOI->CheckInputSequences(InputSequences)) + { + return false; + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("CanTransition :: Owner of FFFStateContext does not implement IFFStateOwnerInterface")); + return false; + } + + return OnCanTransition(InStateContext); +} + + +void UFFState::Enter(const FFFStateContext& InStateContext) +{ + PlayMontage(InStateContext); + + OnEnter(InStateContext); +} + + +void UFFState::Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason) +{ + + + if(InStateContext.Avatar && + (bStopMontageOnStateEnd && StateFinishReason == EFFStateFinishReason::SFT_DurationMetOrExceeded || StateFinishReason == EFFStateFinishReason::SFT_Interrupted) || + (bStopMontageOnMovementModeChange && StateFinishReason == EFFStateFinishReason::SFT_NotInReqMovementMode)) + { + StopCurrentAnimMontage(InStateContext); + } + + OnExit(InStateContext); +} + + +void UFFState::Update(float OneFrame, const FFFStateContext& InStateContext) +{ + IFFStateOwnerInterface* SOI = Cast(InStateContext.Owner); + // TODO: maybe a check/asset is better because I can't think of a reason that not having the Owner implement + // IFFStateOwnerInterface is valid. Although there could be objects that have state that don't necessarily directly + // respond to input and don't have an Owner/controller responsible for them. Or they do have an owner but are associated + // with another Avatar like a character who is owned by a player/player controller. + if(!SOI) + { + UE_LOG(LogTemp, Error, TEXT("Owner of FFFStateContext does not implement IFFStateOwnerInterface")); + return; + } + + for(FFFInputEventHandler InputHandler : InputHandlers) + { + if(SOI->CheckInputSequence(InputHandler.RequiredSequence)) + { + if(InputHandler.Delegate.ExecuteIfBound(InStateContext)) + { + UE_LOG(LogTemp, Error, + TEXT("Trying to execute an input handler delegate on %s but it is not bound"), + *Name.ToString()); + } + } + } + + OnUpdate(OneFrame, InStateContext); + + if(bStateHasDuration && InStateContext.Parent->GetTicksInState() >= StateDuration) + { + Finish(InStateContext, EFFStateFinishReason::SFT_DurationMetOrExceeded); + } +} + + +void UFFState::Landed(const FHitResult& Hit, const FFFStateContext& InStateContext) +{ + OnLanded(Hit, InStateContext); + + // TODO: might want to also finish the state here by default and have the subclasses overwrite + // that behavior in the few cases we don't want to finish the state when we land +} + + +void UFFState::MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, + EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext) +{ + OnMovementModeChanged(PrevMovementMode, PreviousCustomMode, + 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) +{ + OnHit(InStateContext); +} + + +void UFFState::Block(const FFFStateContext& InStateContext) +{ + OnBlock(InStateContext); +} + + +void UFFState::Finish(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason) +{ + // TODO: I really don't like having to pass this state finish reason into this GoToEntryState + // function, which then passes it to GoToState, which then passes it back to the state through + // it's "Exit" function all so we can stop the current anim montage when we exit from a state if + // 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 + // what + InStateContext.Parent->GoToEntryState(StateFinishReason); +} + + +void UFFState::RegisterInputHandler(const FFFInputSequence& InRequiredSequence, FFFInputEventDelegate InDelegate) +{ + FFFInputEventHandler TempHandler; + TempHandler.RequiredSequence = InRequiredSequence; + TempHandler.Delegate = InDelegate; + InputHandlers.Add(TempHandler); +} + +void UFFState::PlayMontage(const FFFStateContext& InStateContext) +{ + // TODO: think of a better way to handle optionally playing montages other than the one set as MontageToPlay + OnPlayMontage(InStateContext); +} + + +void UFFState::OnInit_Implementation(const FFFStateContext& InStateContext) +{ +} + + +bool UFFState::OnCanTransition_Implementation(const FFFStateContext& InStateContext) +{ + return true; +} + +// TODO: think of a better way to handle optionally playing montages other than the one set as MontageToPlay +void UFFState::OnPlayMontage_Implementation(const FFFStateContext& InStateContext) +{ + if(InStateContext.Avatar) + { + USkeletalMeshComponent* SMC = InStateContext.Avatar->GetComponentByClass(); + if(SMC && MontageToPlay) + { + SMC->GetAnimInstance()->Montage_Play(MontageToPlay); + } + } +} + + +UWorld* UFFState::GetWorld() const +{ + UFFStateMachineComponent* SMC = Cast(GetOuter()); + if(SMC) + { + return SMC->GetWorld(); + } + + return nullptr; +} + + +void UFFState::StopCurrentAnimMontage(const FFFStateContext& InStateContext) +{ + USkeletalMeshComponent* SMC = InStateContext.Avatar->GetComponentByClass(); + if(SMC) + { + // TODO: Need a system for keeping track what anim montages were played during a state + // so we can stop only those when the state ends (and if the bStopMontageOnStateEnd flag is set) + // ALSO, this will be important for when we need to fast forward an animation when the game rollsback + // and we have to resimulate to the current local tick + // For now just stop all anim montages + UAnimMontage* CurrAnim = SMC->GetAnimInstance()->GetCurrentActiveMontage(); + if(CurrAnim) + { + SMC->GetAnimInstance()->Montage_Stop(CurrAnim->BlendOut.GetBlendTime()); + } + } +} diff --git a/Source/UnrealFightingFramework/State/FFState.h b/Source/UnrealFightingFramework/State/FFState.h new file mode 100644 index 0000000..8a80d93 --- /dev/null +++ b/Source/UnrealFightingFramework/State/FFState.h @@ -0,0 +1,285 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// FF includes +#include "Input/FFInputBufferComponent.h" + +// UE includes +#include "CoreMinimal.h" + +#include "FFState.generated.h" + +USTRUCT(BlueprintType) +struct FFFStateContext +{ + GENERATED_BODY() + + /** Actor that owns the avatar. Typically a player controller. */ + UPROPERTY(BlueprintReadOnly) + AActor* Owner; + + /** + * Actor that represents the player's avatar or an object associated with the player's avatar. + * This is typically a character or a weapon. + */ + UPROPERTY(BlueprintReadOnly) + AActor* Avatar; + + /** + * Parent state machine that controls this state + */ + UPROPERTY(BlueprintReadOnly) + class UFFStateMachineComponent* Parent; +}; + +DECLARE_DYNAMIC_DELEGATE_OneParam(FFFInputEventDelegate, const FFFStateContext&, InStateContext); + +USTRUCT(BlueprintType) +struct FFFInputEventHandler +{ + GENERATED_BODY() + + // TODO: document + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + FFFInputSequence RequiredSequence; + + // TODO: document + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + FFFInputEventDelegate Delegate; +}; + +UENUM(BlueprintType) +enum class EFFStateFinishReason : uint8 +{ + // TODO: document + SFT_Interrupted UMETA(DisplayName="Interrupted"), + SFT_DurationMetOrExceeded UMETA(DisplayName="Duration Reached"), + SFT_NotInReqMovementMode UMETA(DisplayName="Not In Required Movement Mode"), + SFT_MAX UMETA(Hidden) +}; + + +/** + * A state is an object that provides rules and conditions for when a state can be transitioned into + * and logic to run when the state is entered, exited, and active. + */ +UCLASS(Blueprintable) +class UNREALFIGHTINGFRAMEWORK_API UFFState : public UObject +{ + GENERATED_BODY() + +public: + // State parameters (should be read-only) + /** Name of this state behavior */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + FName Name; + + /** + * True if this state has some duration where it should return to the entry state + * if this state is not cancelled out of by other means. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + bool bStateHasDuration; + + // TODO: this should only be editable if bStateHasDuration is true + /** + * How long this state will be active before finishing if this state is not cancelled out of + * by other means. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="UFF State Properties") + int64 StateDuration; + + /** + * What stance the object this state represents must be in. + * For example this is usually an enumerated value that signifies if a character needs to be + * crouching, standing, or airborne for this state to be eligible for transitioning. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + int32 StanceRequired; + + /** + * What is this state's category. + * Used for determining what types of state can prematurely cancel this one. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + int32 StateType; + + /** + * Input sequences that needs to be present in the controlling player's input buffer for this + * state to be eligible for transitioning. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + TArray InputSequences; + + /** + * If true the state can transition from itself into itself without having to go through + * another state like Idle + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + bool bCanTransitionToSelf = false; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + TEnumAsByte ReqMovementMode; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + uint8 RequiredCustomMode; + + /** + * Animation to begin playing when this state is entered + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + UAnimMontage* MontageToPlay; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + bool bStopMontageOnStateEnd; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="UFF State Properties") + bool bStopMontageOnMovementModeChange; + + /** + * Event delegates to call when a certain input condition is detected in the Owner's input buffer + */ + UPROPERTY(BlueprintReadOnly, Category="UFF State Events") + TArray InputHandlers; + + /** + * Called when state is first created. + */ + virtual void Init(const FFFStateContext& InStateContext); + + /** + * Returns true if Avatar's is in the correct stance AND + * the state type is enabled (or the state can be hit or whiff cancelled from the current state) AND + * all state entry conditions were met AND + * at least one input sequence is present in the Owner's input buffer + */ + virtual bool CanTransition(const FFFStateContext& InStateContext); + + /** + * Called whenever this state is transitioned into. + * + * Calls appropriate Blueprint hooks. + */ + virtual void Enter(const FFFStateContext& InStateContext); + + /** + * Called whenever this state is transitioned out of into a new state. + * + * Calls appropriate Blueprint hooks. + * + * TODO: document StateFinishReason + */ + virtual void Exit(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason); + + /** + * Called whenever this state is active and the game logic ticks. + * + * Calls appropriate Blueprint hooks. + * + * @param OneFrame the time that elapses during one fixed tick + */ + virtual void Update(float OneFrame, const FFFStateContext& InStateContext); + + // TODO: document + virtual void Landed(const FHitResult& Hit, const FFFStateContext& InStateContext); + + // TODO: document + virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, + EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext); + + // TODO: document + // TODO: pass in hitdata struct as well + virtual void Hit(const FFFStateContext& InStateContext); + + // TODO: document + // TODO: pass in hitdata struct as well + virtual void Block(const FFFStateContext& InStateContext); + + /** + * Called when you want to exit from this state but no eligible transitions exist to other states, + * usually due to a lack of player input. + * + * By default this will transition to the owning state machine's entry state, which is usually + * an idle state. + * + * This function will get called automatically when the ticks in this state reaches the + * threshold set by the StateDuration member, but you can call it whenever you wish to return + * to the entry state. + * + * @param InStateContext + */ + UFUNCTION(BlueprintCallable) + virtual void Finish(const FFFStateContext& InStateContext, EFFStateFinishReason StateFinishReason); + + // TODO: document + UFUNCTION(BlueprintCallable) + virtual void RegisterInputHandler( + const FFFInputSequence& InRequiredSequence, FFFInputEventDelegate InDelegate); + + // TODO: document + UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events") + void OnInit(const FFFStateContext& InStateContext); + + /** + * Blueprint hook for overriding the CanTransition logic + */ + UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events") + bool OnCanTransition(const FFFStateContext& InStateContext); + + /** + * Blueprint hook for whenever this state is transitioned into + */ + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnEnter(const FFFStateContext& InStateContext); + + /** + * Blueprint hook for whenever this state is transitioned out of into a new state + */ + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnExit(const FFFStateContext& InStateContext); + + /** + * Blueprint hook for whenever this state is active and the game logic ticks. + * + * @param OneFrame the time that elapses during one fixed tick + */ + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnUpdate(float OneFrame, const FFFStateContext& InStateContext); + + UFUNCTION(BlueprintCallable, Category="UFF|State|Animations") + void PlayMontage(const FFFStateContext& InStateContext); + + /** + * Blueprint hook for overriding the logic for when a anim montage plays at the start of a state + * @param InStateContext + */ + UFUNCTION(BlueprintNativeEvent, Category="UFF|State|Events") + void OnPlayMontage(const FFFStateContext& InStateContext); + + // TODO: document + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnLanded(const FHitResult& Hit, const FFFStateContext& InStateContext); + + // TODO: document + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, + EMovementMode NewMovementMode, uint8 NewCustomMode, const FFFStateContext& InStateContext); + + // TODO: document + // TODO: pass in hitdata struct as well + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnHit(const FFFStateContext& InStateContext); + + // TODO: document + // TODO: pass in hitdata struct as well + UFUNCTION(BlueprintImplementableEvent, Category="UFF|State|Events") + void OnBlock(const FFFStateContext& InStateContext); + + // UObject interface + virtual UWorld* GetWorld() const override; + // End of UObject interface + +private: + void StopCurrentAnimMontage(const FFFStateContext& InStateContext); +}; diff --git a/Source/UnrealFightingFramework/State/FFStateData.h b/Source/UnrealFightingFramework/State/FFStateData.h new file mode 100644 index 0000000..d2dcbc1 --- /dev/null +++ b/Source/UnrealFightingFramework/State/FFStateData.h @@ -0,0 +1,28 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// FF includes +#include "State/FFState.h" + +// UE includes +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" + +#include "FFStateData.generated.h" + +/** + * + */ +UCLASS(BlueprintType) +class UNREALFIGHTINGFRAMEWORK_API UFFStateData : public UDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + TSubclassOf EntryState; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + TArray> OtherStates; +}; diff --git a/Source/UnrealFightingFramework/State/FFStateMachineComponent.cpp b/Source/UnrealFightingFramework/State/FFStateMachineComponent.cpp new file mode 100644 index 0000000..120c8cf --- /dev/null +++ b/Source/UnrealFightingFramework/State/FFStateMachineComponent.cpp @@ -0,0 +1,264 @@ +// Unreal Fighting Framework by Kevin Poretti + +#include "FFStateMachineComponent.h" + +// FF includes +#include "FFState.h" +#include "GameplayFramework/FFGameState.h" +#include "Input/FFPlayerController.h" + +#if !UE_BUILD_SHIPPING +class ASNGGameState; +static int32 StateMachineDebug = 0; +FAutoConsoleVariableRef CVARStateMachineDebug(TEXT("ff.StateMachine.ShowDebug"), + StateMachineDebug, + TEXT("Print state machine information for character"), + ECVF_Cheat); +#endif + +UFFStateMachineComponent::UFFStateMachineComponent() +{ + // Don't use Unreal's tick instead use a fixed tick +#if !UE_BUILD_SHIPPING + PrimaryComponentTick.bCanEverTick = true; +#else + PrimaryComponentTick.bCanEverTick = false; +#endif +} + + +void UFFStateMachineComponent::Initialize(TSubclassOf EntryState, const TArray>& InitialStates) +{ + UFFState* EntryStateInstance = AddState(EntryState); + + for(const TSubclassOf& CurrState : InitialStates) + { + AddState(CurrState); + } + + // need an entry state or something went seriously wrong + check(EntryStateInstance); + + CurrentState = EntryStateInstance; + CurrentState->Enter(GetCurrentStateContext()); +} + + +void UFFStateMachineComponent::InitActorInfo(AActor* InOwner, AActor* InAvatar) +{ + Owner = InOwner; + Avatar = InAvatar; +} + + +UFFState* UFFStateMachineComponent::AddState(TSubclassOf StateClassToAdd) +{ + UFFState* TempState = NewObject(this, StateClassToAdd); + if(TempState) + { + if(!FindStateWithName(TempState->Name)) + { + States.Add(TempState); + TempState->Init(GetCurrentStateContext()); + return TempState; + } + + UE_LOG(LogTemp, Error, TEXT("State with name %s already exists in state machine for %s"), + *TempState->Name.ToString(), *GetOwner()->GetFName().ToString()); + return nullptr; + } + + UE_LOG(LogTemp, Error, TEXT("Could not create instance of state class %s"), + *StateClassToAdd.GetDefaultObject()->Name.ToString()); + + return nullptr; +} + + +void UFFStateMachineComponent::AddStates(const TArray>& StateClassesToAdd) +{ + for(const TSubclassOf& CurrState : StateClassesToAdd) + { + AddState(CurrState); + } +} + + +void UFFStateMachineComponent::RemoveState(FName StateToRemove) +{ + UE_LOG(LogTemp, Error, TEXT("UFFStateMachineComponent::RemoveState is not yet implemented")); +} + +void UFFStateMachineComponent::GoToState(FName NewStateName, EFFStateFinishReason StateFinishReason) +{ + UFFState* NewState = FindStateWithName(NewStateName); + if(NewState) + { + GoToState(NewState, StateFinishReason); + } +} + +void UFFStateMachineComponent::GoToState(UFFState* NewState, EFFStateFinishReason StateFinishReason) +{ + check(CurrentState); + 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(Owner); + if(PC) + { + PC->DisableMostRecentInput(); + } + } + + CurrentState->Exit(GetCurrentStateContext(), StateFinishReason); + TicksInState = 0; + TickStateWasEntered = GetWorld()->GetGameState()->GetCurrentTick(); + CurrentSubStateLabel = NAME_None; + NewState->Enter(GetCurrentStateContext()); + OnStateTransition.Broadcast(CurrentState, NewState); + CurrentState = NewState; +} + +void UFFStateMachineComponent::GoToEntryState(EFFStateFinishReason StateFinishReason) +{ + // can't have an entry state if there are no states + check(States.Num() > 0); + + GoToState(States[0], StateFinishReason); +} + + +FName UFFStateMachineComponent::GetCurrentStateName() const +{ + check(CurrentState) + + return CurrentState->Name; +} + + +void UFFStateMachineComponent::SetSubStateLabel(FName InSubStateLabel) +{ + CurrentSubStateLabel = InSubStateLabel; +} + + +FFFStateContext UFFStateMachineComponent::GetCurrentStateContext() +{ + FFFStateContext CurrStateContext; + CurrStateContext.Owner = Owner; + CurrStateContext.Avatar = Avatar; + CurrStateContext.Parent = this; + return CurrStateContext; +} + + +void UFFStateMachineComponent::Landed(const FHitResult& Hit) +{ + check(CurrentState) + + CurrentState->Landed(Hit, GetCurrentStateContext()); +} + + +void UFFStateMachineComponent::MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, + EMovementMode NewMovementMode, uint8 NewCustomMode) +{ + // TODO: this check should be valid but isn't right now. + // Movement mode changed seems to be called whenever the character is first created but before + // any states are created and entered. + // For now just check if CurrentState is null and don't call the MovementModeChanged callback if + // so + //check(CurrentState) + if(!CurrentState) + { + return; + } + + CurrentState->MovementModeChanged(PrevMovementMode, PreviousCustomMode, + NewMovementMode, NewCustomMode, GetCurrentStateContext()); +} + + +void UFFStateMachineComponent::FixedTick(float OneFrame) +{ + // CurrentState should never be null + // TODO: Should probably assert or whatever UE's equivalent is + //check(CurrentState); + // TODO: yet another reason I want FULL CONTROL over when my game objects are created and initialized + if(!CurrentState) + { + return; + } + + // Should we switch states? + UFFState* StateToTransitionTo = nullptr; + for(UFFState* ThisState : States) + { + // found a state + if(ThisState->CanTransition(GetCurrentStateContext()) && + (CurrentState->Name != ThisState->Name || ThisState->bCanTransitionToSelf)) + { + StateToTransitionTo = ThisState; + break; + } + } + + // Lastly just check if the state we're about to transition into isn't the current state. + // It is OK to transition if state's "CanTransitionToSelf" is true + if(StateToTransitionTo) + { + GoToState(StateToTransitionTo, EFFStateFinishReason::SFT_Interrupted); + } + else + { + // Tick current state + TicksInState++; + CurrentState->Update(OneFrame, GetCurrentStateContext()); + } +} + + +UFFState* UFFStateMachineComponent::FindStateWithName(FName StateName) +{ + for (UFFState* CurrState : States) + { + if(CurrState->Name == StateName) + { + return CurrState; + } + } + + UE_LOG(LogTemp, Warning, + TEXT("Could not find state in state machine with name %s"), *StateName.ToString()); + + return nullptr; +} + +#if !UE_BUILD_SHIPPING +void UFFStateMachineComponent::TickComponent(float DeltaTime, ELevelTick TickType, + FActorComponentTickFunction* ThisTickFunction) +{ + Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + + // Debug + if(StateMachineDebug && CurrentState) + { + FString SMDebugString = "---State Machine Info---\n"; + SMDebugString.Append(FString::Printf(TEXT("Current State: %s\n"), *CurrentState->Name.ToString())); + SMDebugString.Append(FString::Printf(TEXT("Current SubState Label: %s\n"), *CurrentSubStateLabel.ToString())); + SMDebugString.Append(FString::Printf(TEXT("Ticks In State: %lld\n"), TicksInState)); + SMDebugString.Append(FString::Printf(TEXT("-- States added --\n"))); + for (auto TempState : States) + { + SMDebugString.Append(FString::Printf(TEXT("%s\n"), *TempState->Name.ToString())); + } + + GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, SMDebugString); + } +} +#endif diff --git a/Source/UnrealFightingFramework/State/FFStateMachineComponent.h b/Source/UnrealFightingFramework/State/FFStateMachineComponent.h new file mode 100644 index 0000000..2c0c84d --- /dev/null +++ b/Source/UnrealFightingFramework/State/FFStateMachineComponent.h @@ -0,0 +1,188 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +// FF includes +#include "IFFSystemInterface.h" +#include "FFState.h" + +// UE includes +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" + +#include "FFStateMachineComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnStateTransitionSignature, + const UFFState*, PrevState, const UFFState*, NextState); + +/** + * A state machine is a component that evaluates and controls the transitions for state objects that + * are a part of this state machine. + * + * This component also calls the appropriate state logic when a state is changed or the component ticks. + */ +UCLASS( ClassGroup=(UnrealFightingFramework), meta=(BlueprintSpawnableComponent) ) +class UNREALFIGHTINGFRAMEWORK_API UFFStateMachineComponent : public UActorComponent, public IFFSystemInterface +{ + GENERATED_BODY() + +public: + UFFStateMachineComponent(); + + /** + * Creates and adds default states and enters the first state + */ + void Initialize(TSubclassOf EntryState, const TArray>& InitialStates); + + /** + * Initializes pointers to what owns this state machine and what avatar this state represents. + * + * These pointers will also be passed to the states owned by this state machine. + * + * @param InOwner Actor that owns this state machine. + * @param InAvatar Actor that this state machine represents. + */ + virtual void InitActorInfo(AActor* InOwner, AActor* InAvatar); + + /** + * Creates an instance of the state class and adds newly created state to this state machine. + * + * @param StateClassToAdd State class type to be added to this state machine + * + * @return A pointer to the state that was added or nullptr if there was an issue adding or creating the state + */ + // TODO: return should probably be a const pointer if this function is public. I don't want + // anything outside of the state machine modifying the state. Really, I want states in general to + // be read only objects that are only modified at edit time when creating state blueprints. + UFFState* AddState(TSubclassOf StateClassToAdd); + + /** + * Creates an instance of the state classes and adds newly created states to this state machine. + * + * @param StateClassesToAdd Array of state class types to be added to this state machine + */ + void AddStates(const TArray>& StateClassesToAdd); + + /** + * Destroys the state with corresponding name and removes it from this state machine. + */ + void RemoveState(FName StateToRemove); + + /** + * Transitions from CurrentState to the new state with the name passed to this function + * + * Triggers the Exit callback on the CurrentState and the Enter callback on the new state + * + * TODO: document StateFinishReason + */ + void GoToState(FName NewStateName, EFFStateFinishReason StateFinishReason); + + /** + * Transitions from CurrentState to the new state passed to this function + * + * Triggers the Exit callback on the CurrentState and the Enter callback on the new state + * + * TODO: document StateFinishReason + */ + void GoToState(UFFState* NewState, EFFStateFinishReason StateFinishReason); + + /** + * Transitions from CurrentState to the default entry state + * + * TODO: document StateFinishReason + */ + void GoToEntryState(EFFStateFinishReason StateFinishReason); + + UFUNCTION(BlueprintPure) + FORCEINLINE int64 GetTicksInState() const { return TicksInState; } + + /** + * Returns the tick number which corresponds to when this state was entered into. + * + * The tick number represents the ticks that have elapsed since the game began. + */ + UFUNCTION(BlueprintPure) + FORCEINLINE int64 GetTickStateWasEntered() const { return TickStateWasEntered; } + + UFUNCTION(BlueprintPure) + const UFFState* GetCurrentState() const { return const_cast(CurrentState); } + + /** + * Returns the name of the current state + */ + UFUNCTION(BlueprintPure) + FORCEINLINE FName GetCurrentStateName() const; + + /** + * Returns the name of the current sub state label + */ + UFUNCTION(BlueprintPure) + FORCEINLINE FName GetCurrentSubStateLabel() const { return CurrentSubStateLabel; } + + UFUNCTION(BlueprintCallable) + void SetSubStateLabel(FName InSubStateLabel); + + /** + * + */ + FFFStateContext GetCurrentStateContext(); + + // Events + virtual void Landed(const FHitResult& Hit); + + virtual void MovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode, + EMovementMode NewMovementMode, uint8 NewCustomMode); + + FOnStateTransitionSignature OnStateTransition; + + // IFFSystemInterface interface + virtual void FixedTick(float OneFrame) override; + // End of IFFSystemInterface interface + +protected: + /** + * Actor that owns this state machine. + * This will typically be a player controller that possesses the avatar. + */ + UPROPERTY() + AActor* Owner; + + /** + * The avatar is an actor that this state machine represents. + * This will typically be a pawn or character the state machine is attached to. + */ + UPROPERTY() + AActor* Avatar; + + /** How many ticks have elapsed since the currently active state was entered */ + int64 TicksInState; + + /** + * The tick number which corresponds to when this state was entered into. + * + * The tick number represents the ticks that have elapsed since the game began. + */ + int64 TickStateWasEntered; + + /** Current active state for this state machine */ + UPROPERTY() + UFFState* CurrentState; + + /** Current SubState label or NAME_None if there is no SubState label set for the current state*/ + FName CurrentSubStateLabel; + + // States that have been added + UPROPERTY() + TArray States; + + /** + * Returns the state with corresponding name + */ + UFFState* FindStateWithName(FName StateName); + +#if !UE_BUILD_SHIPPING + // UActorComponent interface + virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + // End of UActorComponent interface +#endif +}; diff --git a/Source/UnrealFightingFramework/State/IFFStateAvatarInterface.h b/Source/UnrealFightingFramework/State/IFFStateAvatarInterface.h new file mode 100644 index 0000000..94609f6 --- /dev/null +++ b/Source/UnrealFightingFramework/State/IFFStateAvatarInterface.h @@ -0,0 +1,27 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "IFFStateAvatarInterface.generated.h" + +UINTERFACE(MinimalAPI) +class UFFStateAvatarInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class UNREALFIGHTINGFRAMEWORK_API IFFStateAvatarInterface +{ + GENERATED_BODY() + +public: + virtual bool CheckStance(int32 Stance) = 0; + + virtual bool CheckStateEnabled(int32 StateType) = 0; +}; diff --git a/Source/UnrealFightingFramework/State/IFFStateOwnerInterface.h b/Source/UnrealFightingFramework/State/IFFStateOwnerInterface.h new file mode 100644 index 0000000..5aeb2e4 --- /dev/null +++ b/Source/UnrealFightingFramework/State/IFFStateOwnerInterface.h @@ -0,0 +1,31 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "IFFStateOwnerInterface.generated.h" + +UINTERFACE(MinimalAPI, NotBlueprintable) +class UFFStateOwnerInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * + */ +class UNREALFIGHTINGFRAMEWORK_API IFFStateOwnerInterface +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface") + virtual bool CheckInputSequence(const FFFInputSequence& InInputSequence) = 0; + + UFUNCTION(BlueprintCallable, Category="UFF State Owner Interface") + virtual bool CheckInputSequences(const TArray& InInputSequences) = 0; + + virtual void DisableMostRecentInput() = 0; +}; diff --git a/Source/UnrealFightingEngine/UnrealFightingEngine.Build.cs b/Source/UnrealFightingFramework/UnrealFightingFramework.Build.cs similarity index 70% rename from Source/UnrealFightingEngine/UnrealFightingEngine.Build.cs rename to Source/UnrealFightingFramework/UnrealFightingFramework.Build.cs index 5015cba..b6a13a9 100644 --- a/Source/UnrealFightingEngine/UnrealFightingEngine.Build.cs +++ b/Source/UnrealFightingFramework/UnrealFightingFramework.Build.cs @@ -1,23 +1,22 @@ -// Some copyright should be here... - +// Unreal Fighting Framework by Kevin Poretti +using System.IO; using UnrealBuildTool; -public class UnrealFightingEngine : ModuleRules +public class UnrealFightingFramework : ModuleRules { - public UnrealFightingEngine(ReadOnlyTargetRules Target) : base(Target) + public UnrealFightingFramework(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicIncludePaths.AddRange( new string[] { - // ... add public include paths required here ... + Path.Combine(PluginDirectory,"Source/UnrealFightingFramework"), } ); PrivateIncludePaths.AddRange( new string[] { - // ... add other private include paths required here ... } ); @@ -26,6 +25,7 @@ public class UnrealFightingEngine : ModuleRules new string[] { "Core", + "EnhancedInput", // ... add other public dependencies that you statically link with here ... } ); @@ -36,8 +36,6 @@ public class UnrealFightingEngine : ModuleRules { "CoreUObject", "Engine", - "Slate", - "SlateCore", // ... add private dependencies that you statically link with here ... } ); diff --git a/Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.cpp b/Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.cpp new file mode 100644 index 0000000..df9ca6d --- /dev/null +++ b/Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.cpp @@ -0,0 +1,16 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "UnrealFightingFrameworkBPLibrary.h" +#include "UnrealFightingFrameworkModule.h" + +UUnrealFightingFrameworkBPLibrary::UUnrealFightingFrameworkBPLibrary(const FObjectInitializer& ObjectInitializer) +: Super(ObjectInitializer) +{ + +} + +float UUnrealFightingFrameworkBPLibrary::UnrealFightingFrameworkSampleFunction(float Param) +{ + return -1; +} + diff --git a/Source/UnrealFightingEngine/Public/UnrealFightingEngineBPLibrary.h b/Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.h similarity index 81% rename from Source/UnrealFightingEngine/Public/UnrealFightingEngineBPLibrary.h rename to Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.h index f524d8b..7d54fae 100644 --- a/Source/UnrealFightingEngine/Public/UnrealFightingEngineBPLibrary.h +++ b/Source/UnrealFightingFramework/UnrealFightingFrameworkBPLibrary.h @@ -3,7 +3,7 @@ #pragma once #include "Kismet/BlueprintFunctionLibrary.h" -#include "UnrealFightingEngineBPLibrary.generated.h" +#include "UnrealFightingFrameworkBPLibrary.generated.h" /* * Function library class. @@ -23,10 +23,10 @@ * https://wiki.unrealengine.com/Custom_Blueprint_Node_Creation */ UCLASS() -class UUnrealFightingEngineBPLibrary : public UBlueprintFunctionLibrary +class UUnrealFightingFrameworkBPLibrary : public UBlueprintFunctionLibrary { GENERATED_UCLASS_BODY() - UFUNCTION(BlueprintCallable, meta = (DisplayName = "Execute Sample function", Keywords = "UnrealFightingEngine sample test testing"), Category = "UnrealFightingEngineTesting") - static float UnrealFightingEngineSampleFunction(float Param); + UFUNCTION(BlueprintCallable, meta = (DisplayName = "Execute Sample function", Keywords = "UnrealFightingFramework sample test testing"), Category = "UnrealFightingFrameworkTesting") + static float UnrealFightingFrameworkSampleFunction(float Param); }; diff --git a/Source/UnrealFightingEngine/Private/UnrealFightingEngine.cpp b/Source/UnrealFightingFramework/UnrealFightingFrameworkModule.cpp similarity index 58% rename from Source/UnrealFightingEngine/Private/UnrealFightingEngine.cpp rename to Source/UnrealFightingFramework/UnrealFightingFrameworkModule.cpp index 86106a8..c74be02 100644 --- a/Source/UnrealFightingEngine/Private/UnrealFightingEngine.cpp +++ b/Source/UnrealFightingFramework/UnrealFightingFrameworkModule.cpp @@ -1,16 +1,16 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "UnrealFightingEngine.h" +#include "UnrealFightingFrameworkModule.h" -#define LOCTEXT_NAMESPACE "FUnrealFightingEngineModule" +#define LOCTEXT_NAMESPACE "FUnrealFightingFrameworkModule" -void FUnrealFightingEngineModule::StartupModule() +void FUnrealFightingFrameworkModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module } -void FUnrealFightingEngineModule::ShutdownModule() +void FUnrealFightingFrameworkModule::ShutdownModule() { // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, // we call this function before unloading the module. @@ -19,4 +19,4 @@ void FUnrealFightingEngineModule::ShutdownModule() #undef LOCTEXT_NAMESPACE -IMPLEMENT_MODULE(FUnrealFightingEngineModule, UnrealFightingEngine) \ No newline at end of file +IMPLEMENT_MODULE(FUnrealFightingFrameworkModule, UnrealFightingFramework) \ No newline at end of file diff --git a/Source/UnrealFightingEngine/Public/UnrealFightingEngine.h b/Source/UnrealFightingFramework/UnrealFightingFrameworkModule.h similarity index 79% rename from Source/UnrealFightingEngine/Public/UnrealFightingEngine.h rename to Source/UnrealFightingFramework/UnrealFightingFrameworkModule.h index c763149..d65f51e 100644 --- a/Source/UnrealFightingEngine/Public/UnrealFightingEngine.h +++ b/Source/UnrealFightingFramework/UnrealFightingFrameworkModule.h @@ -4,7 +4,7 @@ #include "Modules/ModuleManager.h" -class FUnrealFightingEngineModule : public IModuleInterface +class FUnrealFightingFrameworkModule : public IModuleInterface { public: diff --git a/Source/UnrealFightingFramework/Utils/TCircleBuffer.h b/Source/UnrealFightingFramework/Utils/TCircleBuffer.h new file mode 100644 index 0000000..3d82b1a --- /dev/null +++ b/Source/UnrealFightingFramework/Utils/TCircleBuffer.h @@ -0,0 +1,151 @@ +// Unreal Fighting Framework by Kevin Poretti + +#pragma once + +#include "CoreMinimal.h" + +/** + * CircleBuffer is a templated array data structure allowing for FIFO, O(1) insertion and removal, + * and wraparound when iterating/accessing elements of the buffer. + * + * The underlying container type is a statically allocated array. You can use the second + * template parameter to specify the static size of the buffer. Defaults to 64. + */ +template +class UNREALFIGHTINGFRAMEWORK_API TCircleBuffer +{ +public: + TCircleBuffer() + : WriteIdx(0) + , ReadIdx(0) + , _Num(0) + { + } + + /** + * @brief Adds a new element to the buffer only if there is room. + * @param Element to add to the buffer + * @return true if there was room in the buffer and the element was added, false otherwise + */ + bool Push(const T& Element) + { + if(_Num == L) + { + // buffer full so can't add element + return false; + } + Buffer[WriteIdx] = Element; + WriteIdx = (WriteIdx + 1) % L; + _Num++; + return true; + } + + /** + * @brief Adds a new element to the buffer. + * This function always adds an element. If the buffer is full the oldest element will be overwritten. + * @param Element to add to the buffer + */ + void ForcePush(const T& Element) + { + if(_Num == L) + { + // buffer is full so we need to overwrite the oldest element + // and make the read index point to the next oldest element + ReadIdx = (ReadIdx + 1) % L; + } + Buffer[WriteIdx] = Element; + WriteIdx = (WriteIdx + 1) % L; + _Num = FMath::Min(_Num + 1, L); + } + + /** + * @brief Returns the oldest element in the buffer and removes that element from the buffer. + * @param Result out parameter to be filled in with the popped element + * @return true if there was an element in the buffer to pop, false otherwise + */ + bool Pop(T* Result) + { + if(IsEmpty()) + { + return false; + } + + *Result = Buffer[ReadIdx]; + ReadIdx = (ReadIdx + 1) % L; + _Num--; + return true; + } + + /** + * @brief Returns the newest element that was added to the buffer + * @param Result out parameter to be filled in with the most recently added element + * @return true if there was an element in the buffer to peak, false otherwise + */ + bool PeakLast(T* Result) + { + if(IsEmpty()) + { + return false; + } + + *Result = Buffer[((SSIZE_T)WriteIdx - 1) <= 0 ? L - 1 : (WriteIdx - 1)]; + return true; + } + + /** + * @brief Returns the oldest element that was added to the buffer + * @param Result out parameter to be filled in with the oldest element + * @return true if there was an element in the buffer to peak, false otherwise + */ + bool PeakFirst(T* Result) + { + if(IsEmpty()) + { + return false; + } + + *Result = Buffer[ReadIdx]; + return true; + } + + /** + * @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. + * i.e. Index 0 will be the oldest added element while index Num - 1 will be the newest added element + * @param Index of the element to return + * @return the element at the index supplied + */ + T& operator[](SIZE_T Index) + { + return Buffer[(ReadIdx + Index) % L]; + } + + FORCEINLINE bool IsEmpty() const + { + return _Num == 0; + } + + /** + * @brief Returns the max size of the underlying buffer + * @return Max size of the underlying buffer + */ + FORCEINLINE SIZE_T Max() const + { + return L; + } + + /** + * @brief Returns the current number of elements of the underlying buffer + * @return Max size of the underlying buffer + */ + FORCEINLINE SIZE_T Num() const + { + return _Num; + } + +private: + T Buffer[L]; + SIZE_T WriteIdx; + SIZE_T ReadIdx; + SIZE_T _Num; +}; \ No newline at end of file diff --git a/UnrealFightingEngine.uplugin b/UnrealFightingFramework.uplugin similarity index 78% rename from UnrealFightingEngine.uplugin rename to UnrealFightingFramework.uplugin index 0b3ea6d..6bb148b 100644 --- a/UnrealFightingEngine.uplugin +++ b/UnrealFightingFramework.uplugin @@ -2,7 +2,7 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "UnrealFightingEngine", + "FriendlyName": "UnrealFightingFramework", "Description": "This library provides actors, components, and other general data structures that are useful for developing character action or fighting games.", "Category": "Other", "CreatedBy": "Kevin Poretti", @@ -16,9 +16,15 @@ "Installed": false, "Modules": [ { - "Name": "UnrealFightingEngine", + "Name": "UnrealFightingFramework", "Type": "Runtime", "LoadingPhase": "PreLoadingScreen" } + ], + "Plugins": [ + { + "Name": "EnhancedInput", + "Enabled": true + } ] } \ No newline at end of file