TFCOE: Devlog #2
Conor Kirkby
13/11/25
Click to Enlarge
TLDR
- Setup Multiple frameworks for the gameplay systems
- Created a foundation for a player inventory
- Created the board piece base for the combat section
- Created a system that will switch between free control towards the turn based combat system
- Setup the foundation for a combat manager
What Happened
Hey everyone!. This time, I spent my effort crafting the foundation for the gameplay systems. To start off and get me in the mood I started with something simple, that being a simple player inventory system. Using dictionaries, I created storage for both general items and key items and the relevant functions to add to them. I made this into an actor component so it can be added to any character in the future including the functionality for drops on enemies.
Following this I created another actor component to be attached to the game mode which will manage the combat. Including the current state of the game, and though it isnt there at this moment, the queue order and anything combat related. Frustratingly this is actually what caused the most trouble for me as for some reason when I tried to create an enum for the combat state, any blueprint usages would completely corrupt upon reload of the engine which didnt make sense as other enums exist and work perfectly fine. I struggled with this for nearly 2 days before I finally gave up and decided to subsitute an int variable. 0 meaning combat is not currently in progress and 1 for it is.
Now with a way to control the combat state of the game, I could begin to build the player controls for this. The player would need to switch between free movement to only being able to move with each click. I know also that player controllers and AI Controllers do not typically mesh well on Unreal and any AI input would most likely be ignored from a player character. Thus I divised a sneaky way around this. When combat is initiated, a dummy player which is a seperate actor mimickng the player but is an AI will be spawned in the exact location and rotation as the player. The normal player will then be hidden and their controls locked off. The new AI could now be controlled with mouse clicks and then when combat is over it will be destroyed and the player restored control and visuals in exact the same location as where the AI dummy ended. As of right now for testing it was allowed free roam with clicks, this of course will not be the case in the future, only allowing movement on board pieces. The camera also is among the next steps as it needs to turn from a locked perspective into a free moving camera seperate from the player.
To finish off I quickly created a base template for the board pieces, they are in no way close to what I want them to act as however, they are happily working now and I can click on them successfully registering input with my mouse
Code
Player Character Script
// Created by Snow Paw Games
#pragma once
#include "CoreMinimal.h"
#include "PaperZDCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "PlayerCharacter.generated.h"
class UCharacter_Inventory;
/**
*
*/
UCLASS()
class TFCOE_API APlayerCharacter : public APaperZDCharacter
{
GENERATED_BODY()
APlayerCharacter();
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBoardPieceClicked, AActor*, BoardPiece);
UPROPERTY(BlueprintAssignable, Category="Events")
FOnBoardPieceClicked OnBoardPieceClicked;
protected:
// Input Mapping & Actions //
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|MappingContext")
UInputMappingContext* MappingContext = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Actions")
UInputAction* MoveAction = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Actions")
UInputAction* SprintAction = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Actions")
UInputAction* InteractAction = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Actions")
UInputAction* CombatClickAction = nullptr;
// Settings
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Movement")
bool MovementEnabled = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Movement")
bool CameraMovementEnabled = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Movement")
float WalkSpeed = 400.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Movement")
float SprintSpeed = 700.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Combat")
TSoftClassPtr CombatPlayer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings|Combat")
bool CombatModeActivated = false;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Settings|Combat")
AActor* AIPlayerDummy = nullptr;
// Components
UPROPERTY()
UCharacterMovementComponent* MovementComponent = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCharacter_Inventory* CharacterInventory = nullptr;
// Functions
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
void InitialiseMovementComponent();
UFUNCTION(BlueprintCallable, Category = "Settings")
void UpdatePlayerCombatState(bool CombatEnabled);
// Input Functions
void MoveTrigger(const FInputActionValue& Value);
void ResetMovement();
void SprintTrigger();
void SprintEnd();
void InteractTrigger();
void CombatClickTrigger();
// Getter & Setter
UCharacter_Inventory* GetInventory() const
{
return CharacterInventory;
}
UFUNCTION(BlueprintCallable, Category = "Settings")
void SetLocomotion(const bool EntityMovement, const bool CameraMovement)
{
MovementEnabled = EntityMovement;
CameraMovementEnabled = CameraMovement;
}
UFUNCTION(BlueprintCallable, Category = "Settings")
AActor* GetPlayerAIDummy() const
{
return AIPlayerDummy;
}
};
// Created by Snow Paw Games
#include "PlayerCharacter.h"
#include "Character_Inventory.h"
#include "EnhancedInputComponent.h"
#include "Engine/AssetManager.h"
#include "GameFramework/CharacterMovementComponent.h"
APlayerCharacter::APlayerCharacter()
{
CharacterInventory = CreateDefaultSubobject(TEXT("Character Inventory"));
}
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
// Gets the movement component and stores it for future use.
InitialiseMovementComponent();
}
// Initialises the player input system
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (const APlayerController* PlayerController = Cast(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem(PlayerController->GetLocalPlayer()))
{
if (MappingContext)
{
Subsystem->AddMappingContext(MappingContext, 0);
}
}
}
// Initialises the action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast(PlayerInputComponent))
{
// Insert Action bindings here
// Binds the movement action to the input variables
if (MoveAction)
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerCharacter::MoveTrigger);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Canceled, this, &APlayerCharacter::ResetMovement);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &APlayerCharacter::ResetMovement);
}
// Binds the interact action to the triggered function
if (InteractAction)
{
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &APlayerCharacter::InteractTrigger);
}
// Binds the sprint action to the trigger for starting, and the completed to the end function
if (SprintAction)
{
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Started, this, &APlayerCharacter::SprintTrigger);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &APlayerCharacter::SprintEnd);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Canceled, this, &APlayerCharacter::SprintEnd);
}
if (CombatClickAction)
{
EnhancedInputComponent->BindAction(CombatClickAction, ETriggerEvent::Started, this, &APlayerCharacter::CombatClickTrigger);
}
}
}
void APlayerCharacter::InitialiseMovementComponent()
{
// Gets and caches the movement component for use
if (UCharacterMovementComponent* CharacterMovementComp = GetCharacterMovement())
{
MovementComponent = CharacterMovementComp;
}
}
void APlayerCharacter::UpdatePlayerCombatState(const bool CombatEnabled)
{
if (APlayerController* PlayerController = Cast(GetController()))
{
if (CombatEnabled)
{
// The combat mode is enabled, the player will no longer be visible and the dummy player will activate
CombatModeActivated = true;
SetLocomotion(false, true);
PlayerController->bShowMouseCursor = true;
// Async loads the dummy player character for use with the combat systems.
UAssetManager::GetStreamableManager().RequestAsyncLoad(CombatPlayer.ToSoftObjectPath(), FStreamableDelegate::CreateLambda([this]
{
if (UClass* LoadedClass = Cast(CombatPlayer.Get()))
{
AIPlayerDummy = GetWorld()->SpawnActor(LoadedClass, GetActorLocation(), GetActorRotation());
}
else
{
UE_LOG(LogTemp, Error, TEXT("Player Character: Attempt to load AI player, Loaded Class failure"))
}
}));
// Sets the main player to be hidden in the game so that it cannot interfere
SetActorHiddenInGame(true);
}
else
{
// Combat is over or not activated, the player will return to visibility and control restored.
CombatModeActivated = false;
SetLocomotion(true, false);
PlayerController->bShowMouseCursor = false;
// Places the hidden player in the exact spot as the AI dummy so that it can resume control seemlessly
if (AIPlayerDummy)
{
SetActorLocation(AIPlayerDummy->GetActorLocation());
SetActorRotation(AIPlayerDummy->GetActorRotation());
AIPlayerDummy->Destroy();
}
SetActorHiddenInGame(false);
}
}
}
void APlayerCharacter::MoveTrigger(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get();
if (!MovementEnabled)
{
ResetMovement();
return;
}
// Movement for X vector
AddMovementInput(FVector(1, 0, 0), MovementVector.X);
// Movement for Y vector
AddMovementInput(FVector(0, 1, 0), MovementVector.Y);
}
void APlayerCharacter::ResetMovement()
{
MovementComponent->StopMovementImmediately();
}
void APlayerCharacter::SprintTrigger()
{
if (MovementComponent)
{
// Sets the movement speed to be the sprint speed on sprint start
MovementComponent->MaxWalkSpeed = SprintSpeed;
}
}
void APlayerCharacter::SprintEnd()
{
if (MovementComponent)
{
// Sets the movement speed to be the walk speed on the sprint end
MovementComponent->MaxWalkSpeed = WalkSpeed;
}
}
void APlayerCharacter::InteractTrigger()
{
UE_LOG(LogTemp, Warning, TEXT("The player has interacted with something"))
CharacterInventory->AddToGeneralItems("Scrap", FMath::RandRange(1, 4));
const int* Amount = CharacterInventory->GetGeneralItemList().Find("Scrap");
UE_LOG(LogTemp, Error, TEXT("You now have %i Scrap metal"), *Amount)
}
void APlayerCharacter::CombatClickTrigger()
{
if (const APlayerController* PlayerController = Cast(GetController()))
{
// Creates an array of types to search for. Only want world dynamic to be triggered
TArray> ObjectTypes;
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic));
// Checks what the mouse is clicking on, the aim is to detect board pieces only
FHitResult HitResult;
PlayerController->GetHitResultUnderCursorForObjects(ObjectTypes, false, HitResult);
if (HitResult.bBlockingHit)
{
// Broadcasts back to blueprints which was hit, where then I can do checks etc...
OnBoardPieceClicked.Broadcast(HitResult.GetActor());
}
}
}
Player Character Script
Combat Manager Script
// Created by Snow Paw Games
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CombatManager.generated.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class TFCOE_API UCombatManager : public UActorComponent
{
GENERATED_BODY()
public:
UCombatManager();
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCombatBegin);
UPROPERTY(BlueprintAssignable, Category="Events")
FOnCombatBegin OnCombatBegin;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCombatEnd);
UPROPERTY(BlueprintAssignable, Category="Events")
FOnCombatEnd OnCombatEnd;
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Settings")
int CurrentCombatState = 0;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/**
* 0 -> Disengaged
* 1 -> Engaged
* @param CombatState The state to set the combat into.
*/
UFUNCTION(BlueprintCallable, Category="Combat")
void SetCombatState(int CombatState);
UFUNCTION(BlueprintCallable, Category="Combat")
int GetCombatState() const
{
return CurrentCombatState;
}
};
// Created by Snow Paw Games
#include "CombatManager.h"
UCombatManager::UCombatManager()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UCombatManager::BeginPlay()
{
Super::BeginPlay();
}
void UCombatManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
//Tick turned off
}
void UCombatManager::SetCombatState(const int CombatState)
{
// Tracks the current state of the game
CurrentCombatState = CombatState;
// 0 -> Disengaged
// 1 -> Engaged
switch (CombatState)
{
case 0:
OnCombatEnd.Broadcast();
break;
case 1:
OnCombatBegin.Broadcast();
break;
default:
OnCombatEnd.Broadcast();
}
}
Combat Manager Script
Player Inventory Script
// Created by Snow Paw Games
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Character_Inventory.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class TFCOE_API UCharacter_Inventory : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UCharacter_Inventory();
protected:
// A map that contains the regular items and their amount.
UPROPERTY(BlueprintReadOnly, Category = "Inventory")
TMap GeneralItems = {};
// A map that contains any key items.
UPROPERTY(BlueprintReadOnly, Category = "Inventory")
TMap KeyItems = {};
public:
// Adds to the regular inventory
UFUNCTION(BlueprintCallable, Category = "Inventory")
void AddToGeneralItems(const FName KeyID, const int ItemCount);
// Adds to the key items inventory
UFUNCTION(BlueprintCallable, Category = "Inventory")
void AddToKeyItems(const FName KeyID, const int ItemCount);
// Getters and Setters //
UFUNCTION(BlueprintCallable, Category = "Inventory")
TMap GetGeneralItemList() const
{
return GeneralItems;
}
UFUNCTION(BlueprintCallable, Category = "Inventory")
TMap GetKeyItemList() const
{
return KeyItems;
}
};
// Created by Snow Paw Games
#include "Character_Inventory.h"
// Sets default values for this component's properties
UCharacter_Inventory::UCharacter_Inventory()
{
}
void UCharacter_Inventory::AddToGeneralItems(const FName KeyID, const int ItemCount)
{
if (GeneralItems.Contains(KeyID))
{
if (const int* CurrentItemCount = GeneralItems.Find(KeyID))
{
const int NewItemCount = *CurrentItemCount + ItemCount;
GeneralItems[KeyID] = NewItemCount;
}
}
else
{
GeneralItems.Add(KeyID, ItemCount);
}
}
void UCharacter_Inventory::AddToKeyItems(const FName KeyID, const int ItemCount)
{
if (KeyItems.Contains(KeyID))
{
if (const int* CurrentItemCount = KeyItems.Find(KeyID))
{
const int NewItemCount = *CurrentItemCount + ItemCount;
KeyItems[KeyID] = NewItemCount;
}
}
else
{
KeyItems.Add(KeyID, ItemCount);
}
}
Player Inventory Script
Board Piece Script
// Created by Snow Paw Games
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "BoardPiece.generated.h"
class UBoxComponent;
UENUM(BlueprintType)
enum EPieceState
{
Enabled UMETA(DisplayName = "Enabled"),
Disabled UMETA(DisplayName = "Disabled"),
Occupied UMETA(DisplayName = "Occupied"),
};
UCLASS()
class TFCOE_API ABoardPiece : public AActor
{
GENERATED_BODY()
public:
ABoardPiece();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings")
TEnumAsByte CurrentPieceState = Enabled;
virtual void BeginPlay() override;
public:
// Getter and Setter
UFUNCTION(BlueprintCallable, Category = "Board Piece")
void SetPieceState(const EPieceState NewPieceState)
{
CurrentPieceState = NewPieceState;
}
};
// Created by Snow Paw Games
#include "BoardPiece.h"
ABoardPiece::ABoardPiece()
{
}
void ABoardPiece::BeginPlay()
{
Super::BeginPlay();
}