Remove unused Unreal starter variants

This commit is contained in:
2026-05-14 14:52:18 -07:00
parent 12ae66d76a
commit e25b3d2cdd
585 changed files with 40 additions and 7526 deletions
+1 -16
View File
@@ -15,8 +15,6 @@ public class AgrarianGame : ModuleRules
"InputCore",
"EnhancedInput",
"AIModule",
"StateTreeModule",
"GameplayStateTreeModule",
"UMG",
"Landscape",
"Slate",
@@ -33,20 +31,7 @@ public class AgrarianGame : ModuleRules
}
PublicIncludePaths.AddRange(new string[] {
"AgrarianGame",
"AgrarianGame/Variant_Platforming",
"AgrarianGame/Variant_Platforming/Animation",
"AgrarianGame/Variant_Combat",
"AgrarianGame/Variant_Combat/AI",
"AgrarianGame/Variant_Combat/Animation",
"AgrarianGame/Variant_Combat/Gameplay",
"AgrarianGame/Variant_Combat/Interfaces",
"AgrarianGame/Variant_Combat/UI",
"AgrarianGame/Variant_SideScrolling",
"AgrarianGame/Variant_SideScrolling/AI",
"AgrarianGame/Variant_SideScrolling/Gameplay",
"AgrarianGame/Variant_SideScrolling/Interfaces",
"AgrarianGame/Variant_SideScrolling/UI"
"AgrarianGame"
});
// Uncomment if you are using Slate UI
@@ -1,19 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatAIController.h"
#include "Components/StateTreeAIComponent.h"
ACombatAIController::ACombatAIController()
{
// create the StateTree AI Component
StateTreeAI = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
check(StateTreeAI);
// ensure we start the StateTree when we possess the pawn
bStartAILogicOnPossess = true;
// ensure we're attached to the possessed character.
// this is necessary for EnvQueries to work correctly
bAttachToPawn = true;
}
@@ -1,27 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "CombatAIController.generated.h"
class UStateTreeAIComponent;
/**
* A basic AI Controller capable of running StateTree
*/
UCLASS(abstract)
class ACombatAIController : public AAIController
{
GENERATED_BODY()
/** StateTree Component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStateTreeAIComponent* StateTreeAI;
public:
/** Constructor */
ACombatAIController();
};
@@ -1,343 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatEnemy.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "CombatAIController.h"
#include "Components/WidgetComponent.h"
#include "Engine/DamageEvents.h"
#include "CombatLifeBar.h"
#include "TimerManager.h"
#include "Components/SkeletalMeshComponent.h"
#include "Animation/AnimInstance.h"
ACombatEnemy::ACombatEnemy()
{
PrimaryActorTick.bCanEverTick = true;
// bind the attack montage ended delegate
OnAttackMontageEnded.BindUObject(this, &ACombatEnemy::AttackMontageEnded);
// set the AI Controller class by default
AIControllerClass = ACombatAIController::StaticClass();
// use an AI Controller regardless of whether we're placed or spawned
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
// ignore the controller's yaw rotation
bUseControllerRotationYaw = false;
// create the life bar
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
LifeBar->SetupAttachment(RootComponent);
// set the collision capsule size
GetCapsuleComponent()->SetCapsuleSize(35.0f, 90.0f);
// set the character movement properties
GetCharacterMovement()->bUseControllerDesiredRotation = true;
// reset HP to maximum
CurrentHP = MaxHP;
}
void ACombatEnemy::DoAIComboAttack()
{
// ignore if we're already playing an attack animation
if (bIsAttacking)
{
return;
}
// raise the attacking flag
bIsAttacking = true;
// choose how many times we're going to attack
TargetComboCount = FMath::RandRange(1, ComboSectionNames.Num() - 1);
// reset the attack counter
CurrentComboAttack = 0;
// play the attack montage
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
// subscribe to montage completed and interrupted events
if (MontageLength > 0.0f)
{
// set the end delegate for the montage
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
}
}
}
void ACombatEnemy::DoAIChargedAttack()
{
// ignore if we're already playing an attack animation
if (bIsAttacking)
{
return;
}
// raise the attacking flag
bIsAttacking = true;
// choose how many loops are we going to charge for
TargetChargeLoops = FMath::RandRange(MinChargeLoops, MaxChargeLoops);
// reset the charge loop counter
CurrentChargeLoop = 0;
// play the attack montage
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
// subscribe to montage completed and interrupted events
if (MontageLength > 0.0f)
{
// set the end delegate for the montage
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
}
}
}
void ACombatEnemy::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
// reset the attacking flag
bIsAttacking = false;
// call the attack completed delegate so the StateTree can continue execution
OnAttackCompleted.ExecuteIfBound();
}
const FVector& ACombatEnemy::GetLastDangerLocation() const
{
return LastDangerLocation;
}
float ACombatEnemy::GetLastDangerTime() const
{
return LastDangerTime;
}
void ACombatEnemy::DoAttackTrace(FName DamageSourceBone)
{
// sweep for objects in front of the character to be hit by the attack
TArray<FHitResult> OutHits;
// start at the provided socket location, sweep forward
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
// enemies only affect Pawn collision objects; they don't knock back boxes
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
// use a sphere shape for the sweep
FCollisionShape CollisionShape;
CollisionShape.SetSphere(MeleeTraceRadius);
// ignore self
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
{
// iterate over each object hit
for (const FHitResult& CurrentHit : OutHits)
{
/** does the actor have the player tag? */
if (CurrentHit.GetActor()->ActorHasTag(FName("Player")))
{
// check if the actor is damageable
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
if (Damageable)
{
// knock upwards and away from the impact normal
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
// pass the damage event to the actor
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
}
}
}
}
}
void ACombatEnemy::CheckCombo()
{
// increase the combo counter
++CurrentComboAttack;
// do we still have attacks to play in this string?
if (CurrentComboAttack < TargetComboCount)
{
// jump to the next attack section
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
AnimInstance->Montage_JumpToSection(ComboSectionNames[CurrentComboAttack], ComboAttackMontage);
}
}
}
void ACombatEnemy::CheckChargedAttack()
{
// increase the charge loop counter
++CurrentChargeLoop;
// jump to either the loop or attack section of the montage depending on whether we hit the loop target
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
AnimInstance->Montage_JumpToSection(CurrentChargeLoop >= TargetChargeLoops ? ChargeAttackSection : ChargeLoopSection, ChargedAttackMontage);
}
}
void ACombatEnemy::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
{
// pass the damage event to the actor
FDamageEvent DamageEvent;
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
// only process knockback and effects if we received nonzero damage
if (ActualDamage > 0.0f)
{
// apply the knockback impulse
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
// is the character ragdolling?
if (GetMesh()->IsSimulatingPhysics())
{
// apply an impulse to the ragdoll
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
}
// stop the attack montages to interrupt the attack
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
AnimInstance->Montage_Stop(0.1f, ComboAttackMontage);
AnimInstance->Montage_Stop(0.1f, ChargedAttackMontage);
}
// pass control to BP to play effects, etc.
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
}
}
void ACombatEnemy::HandleDeath()
{
// hide the life bar
LifeBar->SetHiddenInGame(true);
// disable the collision capsule to avoid being hit again while dead
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// disable character movement
GetCharacterMovement()->DisableMovement();
// enable full ragdoll physics
GetMesh()->SetSimulatePhysics(true);
// call the died delegate to notify any subscribers
OnEnemyDied.Broadcast();
// set up the death timer
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &ACombatEnemy::RemoveFromLevel, DeathRemovalTime);
}
void ACombatEnemy::ApplyHealing(float Healing, AActor* Healer)
{
// stub
}
void ACombatEnemy::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
{
// ensure we're being attacked by the player
if (DangerSource && DangerSource->ActorHasTag(FName("Player")))
{
// save the danger location and game time
LastDangerLocation = DangerLocation;
LastDangerTime = GetWorld()->GetTimeSeconds();
}
}
void ACombatEnemy::RemoveFromLevel()
{
// destroy this actor
Destroy();
}
float ACombatEnemy::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// only process damage if the character is still alive
if (CurrentHP <= 0.0f)
{
return 0.0f;
}
// reduce the current HP
CurrentHP -= Damage;
// have we run out of HP?
if (CurrentHP <= 0.0f)
{
// die
HandleDeath();
}
else
{
// update the life bar
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
// enable partial ragdoll physics, but keep the pelvis vertical
GetMesh()->SetPhysicsBlendWeight(0.5f);
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
}
// return the received damage amount
return Damage;
}
void ACombatEnemy::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
// is the character still alive?
if (CurrentHP >= 0.0f)
{
// disable ragdoll physics
GetMesh()->SetPhysicsBlendWeight(0.0f);
}
// call the landed Delegate for StateTree
OnEnemyLanded.ExecuteIfBound();
}
void ACombatEnemy::BeginPlay()
{
// reset HP to maximum
CurrentHP = MaxHP;
// we top the HP before BeginPlay so StateTree picks it up at the right value
Super::BeginPlay();
// get the life bar widget from the widget comp
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
check(LifeBarWidget);
// fill the life bar
LifeBarWidget->SetLifePercentage(1.0f);
}
void ACombatEnemy::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the death timer
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
}
@@ -1,232 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CombatAttacker.h"
#include "CombatDamageable.h"
#include "Animation/AnimMontage.h"
#include "Engine/TimerHandle.h"
#include "CombatEnemy.generated.h"
class UWidgetComponent;
class UCombatLifeBar;
class UAnimMontage;
/** Completed attack animation delegate for StateTree */
DECLARE_DELEGATE(FOnEnemyAttackCompleted);
/** Landed delegate for StateTree */
DECLARE_DELEGATE(FOnEnemyLanded);
/** Enemy died delegate */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEnemyDied);
/**
* An AI-controlled character with combat capabilities.
* Its bundled AI Controller runs logic through StateTree
*/
UCLASS(abstract)
class ACombatEnemy : public ACharacter, public ICombatAttacker, public ICombatDamageable
{
GENERATED_BODY()
/** Life bar widget component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UWidgetComponent* LifeBar;
public:
/** Constructor */
ACombatEnemy();
protected:
/** Max amount of HP the character will have on respawn */
UPROPERTY(EditAnywhere, Category="Damage")
float MaxHP = 3.0f;
public:
/** Current amount of HP the character has */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Damage", meta = (ClampMin = 0, ClampMax = 100))
float CurrentHP = 0.0f;
protected:
/** Name of the pelvis bone, for damage ragdoll physics */
UPROPERTY(EditAnywhere, Category="Damage")
FName PelvisBoneName;
/** Pointer to the life bar widget */
UPROPERTY(EditAnywhere, Category="Damage")
UCombatLifeBar* LifeBarWidget;
/** If true, the character is currently playing an attack animation */
bool bIsAttacking = false;
/** Distance ahead of the character that melee attack sphere collision traces will extend */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units = "cm"))
float MeleeTraceDistance = 75.0f;
/** Radius of the sphere trace for melee attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units = "cm"))
float MeleeTraceRadius = 50.0f;
/** Amount of damage a melee attack will deal */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 100))
float MeleeDamage = 1.0f;
/** Amount of knockback impulse a melee attack will apply */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
float MeleeKnockbackImpulse = 150.0f;
/** Amount of upwards impulse a melee attack will apply */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
float MeleeLaunchImpulse = 350.0f;
/** AnimMontage that will play for combo attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
UAnimMontage* ComboAttackMontage;
/** Names of the AnimMontage sections that correspond to each stage of the combo attack */
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
TArray<FName> ComboSectionNames;
/** Target number of attacks in the combo attack string we're playing */
int32 TargetComboCount = 0;
/** Index of the current stage of the melee attack combo */
int32 CurrentComboAttack = 0;
/** AnimMontage that will play for charged attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
UAnimMontage* ChargedAttackMontage;
/** Name of the AnimMontage section that corresponds to the charge loop */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
FName ChargeLoopSection;
/** Name of the AnimMontage section that corresponds to the attack */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
FName ChargeAttackSection;
/** Minimum number of charge animation loops that will be played by the AI */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged", meta = (ClampMin = 1, ClampMax = 20))
int32 MinChargeLoops = 2;
/** Maximum number of charge animation loops that will be played by the AI */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged", meta = (ClampMin = 1, ClampMax = 20))
int32 MaxChargeLoops = 5;
/** Target number of charge animation loops to play in this charged attack */
int32 TargetChargeLoops = 0;
/** Number of charge animation loop currently playing */
int32 CurrentChargeLoop = 0;
/** Time to wait before removing this character from the level after it dies */
UPROPERTY(EditAnywhere, Category="Death")
float DeathRemovalTime = 5.0f;
/** Enemy death timer */
FTimerHandle DeathTimer;
/** Attack montage ended delegate */
FOnMontageEnded OnAttackMontageEnded;
/** Last recorded location we're being attacked from */
FVector LastDangerLocation = FVector::ZeroVector;
/** Last recorded game time we were attacked */
float LastDangerTime = -1000.0f;
public:
/** Attack completed internal delegate to notify StateTree tasks */
FOnEnemyAttackCompleted OnAttackCompleted;
/** Landed internal delegate to notify StateTree tasks. We use this instead of the built-in Landed delegate so we can bind to a Lambda in StateTree tasks */
FOnEnemyLanded OnEnemyLanded;
/** Enemy died delegate. Allows external subscribers to respond to enemy death */
UPROPERTY(BlueprintAssignable, Category="Events")
FOnEnemyDied OnEnemyDied;
public:
/** Performs an AI-initiated combo attack. Number of hits will be decided by this character */
void DoAIComboAttack();
/** Performs an AI-initiated charged attack. Charge time will be decided by this character */
void DoAIChargedAttack();
/** Called from a delegate when the attack montage ends */
void AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
/** Returns the last recorded location we were attacked from */
const FVector& GetLastDangerLocation() const;
/** Returns the last game time we were attacked */
float GetLastDangerTime() const;
public:
// ~begin ICombatAttacker interface
/** Performs an attack's collision check */
virtual void DoAttackTrace(FName DamageSourceBone) override;
/** Performs a combo attack's check to continue the string */
UFUNCTION(BlueprintCallable, Category="Attacker")
virtual void CheckCombo() override;
/** Performs a charged attack's check to loop the charge animation */
UFUNCTION(BlueprintCallable, Category="Attacker")
virtual void CheckChargedAttack() override;
// ~end ICombatAttacker interface
// ~begin ICombatDamageable interface
/** Handles damage and knockback events */
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
/** Handles death events */
virtual void HandleDeath() override;
/** Handles healing events */
virtual void ApplyHealing(float Healing, AActor* Healer) override;
/** Allows the enemy to react to incoming attacks */
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
// ~end ICombatDamageable interface
protected:
/** Removes this character from the level after it dies */
void RemoveFromLevel();
public:
/** Overrides the default TakeDamage functionality */
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
/** Overrides landing to reset damage ragdoll physics */
virtual void Landed(const FHitResult& Hit) override;
protected:
/** Blueprint handler to play damage received effects */
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
void ReceivedDamage(float Damage, const FVector& ImpactPoint, const FVector& DamageDirection);
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** EndPlay cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
};
@@ -1,126 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatEnemySpawner.h"
#include "Engine/World.h"
#include "Components/SceneComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/ArrowComponent.h"
#include "TimerManager.h"
#include "CombatEnemy.h"
ACombatEnemySpawner::ACombatEnemySpawner()
{
PrimaryActorTick.bCanEverTick = false;
// create the root
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the reference spawn capsule
SpawnCapsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Spawn Capsule"));
SpawnCapsule->SetupAttachment(RootComponent);
SpawnCapsule->SetRelativeLocation(FVector(0.0f, 0.0f, 90.0f));
SpawnCapsule->SetCapsuleSize(35.0f, 90.0f);
SpawnCapsule->SetCollisionProfileName(FName("NoCollision"));
SpawnDirection = CreateDefaultSubobject<UArrowComponent>(TEXT("Spawn Direction"));
SpawnDirection->SetupAttachment(RootComponent);
}
void ACombatEnemySpawner::BeginPlay()
{
Super::BeginPlay();
// should we spawn an enemy right away?
if (bShouldSpawnEnemiesImmediately)
{
// schedule the first enemy spawn
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnEnemy, InitialSpawnDelay);
}
}
void ACombatEnemySpawner::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the spawn timer
GetWorld()->GetTimerManager().ClearTimer(SpawnTimer);
}
void ACombatEnemySpawner::SpawnEnemy()
{
// ensure the enemy class is valid
if (IsValid(EnemyClass))
{
// spawn the enemy at the reference capsule's transform
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
ACombatEnemy* SpawnedEnemy = GetWorld()->SpawnActor<ACombatEnemy>(EnemyClass, SpawnCapsule->GetComponentTransform(), SpawnParams);
// was the enemy successfully created?
if (SpawnedEnemy)
{
// subscribe to the death delegate
SpawnedEnemy->OnEnemyDied.AddDynamic(this, &ACombatEnemySpawner::OnEnemyDied);
}
}
}
void ACombatEnemySpawner::OnEnemyDied()
{
// decrease the spawn counter
--SpawnCount;
// is this the last enemy we should spawn?
if (SpawnCount <= 0)
{
// schedule the activation on depleted message
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnerDepleted, ActivationDelay);
return;
}
// schedule the next enemy spawn
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnEnemy, RespawnDelay);
}
void ACombatEnemySpawner::SpawnerDepleted()
{
// process the actors to activate list
for (AActor* CurrentActor : ActorsToActivateWhenDepleted)
{
// check if the actor is activatable
if (ICombatActivatable* CombatActivatable = Cast<ICombatActivatable>(CurrentActor))
{
// activate the actor
CombatActivatable->ActivateInteraction(this);
}
}
}
void ACombatEnemySpawner::ToggleInteraction(AActor* ActivationInstigator)
{
// stub
}
void ACombatEnemySpawner::ActivateInteraction(AActor* ActivationInstigator)
{
// ensure we're only activated once, and only if we've deferred enemy spawning
if (bHasBeenActivated || bShouldSpawnEnemiesImmediately)
{
return;
}
// raise the activation flag
bHasBeenActivated = true;
// spawn the first enemy
SpawnEnemy();
}
void ACombatEnemySpawner::DeactivateInteraction(AActor* ActivationInstigator)
{
// stub
}
@@ -1,109 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CombatActivatable.h"
#include "CombatEnemySpawner.generated.h"
class UCapsuleComponent;
class UArrowComponent;
class ACombatEnemy;
/**
* A basic Actor in charge of spawning Enemy Characters and monitoring their deaths.
* Enemies will be spawned one by one, and the spawner will wait until the enemy dies before spawning a new one.
* The spawner can be remotely activated through the ICombatActivatable interface
* When the last spawned enemy dies, the spawner can also activate other ICombatActivatables
*/
UCLASS(abstract)
class ACombatEnemySpawner : public AActor, public ICombatActivatable
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UCapsuleComponent* SpawnCapsule;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UArrowComponent* SpawnDirection;
protected:
/** Type of enemy to spawn */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner")
TSubclassOf<ACombatEnemy> EnemyClass;
/** If true, the first enemy will be spawned as soon as the game starts */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner")
bool bShouldSpawnEnemiesImmediately = true;
/** Time to wait before spawning the first enemy on game start */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 10))
float InitialSpawnDelay = 5.0f;
/** Number of enemies to spawn */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 100))
int32 SpawnCount = 1;
/** Time to wait before spawning the next enemy after the current one dies */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 10))
float RespawnDelay = 5.0f;
/** Time to wait after this spawner is depleted before activating the actor list */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Activation", meta = (ClampMin = 0, ClampMax = 10))
float ActivationDelay = 1.0f;
/** List of actors to activate after the last enemy dies */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Activation")
TArray<AActor*> ActorsToActivateWhenDepleted;
/** Flag to ensure this is only activated once */
bool bHasBeenActivated = false;
/** Timer to spawn enemies after a delay */
FTimerHandle SpawnTimer;
public:
/** Constructor */
ACombatEnemySpawner();
public:
/** Initialization */
virtual void BeginPlay() override;
/** Cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
protected:
/** Spawn an enemy and subscribe to its death event */
void SpawnEnemy();
/** Called when the spawned enemy has died */
UFUNCTION()
void OnEnemyDied();
/** Called after the last spawned enemy has died */
void SpawnerDepleted();
public:
// ~begin ICombatActivatable interface
/** Toggles the Spawner */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void ToggleInteraction(AActor* ActivationInstigator) override;
/** Activates the Spawner */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void ActivateInteraction(AActor* ActivationInstigator) override;
/** Deactivates the Spawner */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void DeactivateInteraction(AActor* ActivationInstigator) override;
// ~end IActivatable interface
};
@@ -1,325 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatStateTreeUtility.h"
#include "StateTreeExecutionContext.h"
#include "StateTreeExecutionTypes.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "AIController.h"
#include "CombatEnemy.h"
#include "Kismet/GameplayStatics.h"
#include "StateTreeAsyncExecutionContext.h"
bool FStateTreeCharacterGroundedCondition::TestCondition(FStateTreeExecutionContext& Context) const
{
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// is the character currently grounded?
bool bCondition = InstanceData.Character->GetMovementComponent()->IsMovingOnGround();
return InstanceData.bMustBeOnAir ? !bCondition : bCondition;
}
#if WITH_EDITOR
FText FStateTreeCharacterGroundedCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Is Character Grounded</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
bool FStateTreeIsInDangerCondition::TestCondition(FStateTreeExecutionContext& Context) const
{
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// ensure we have a valid enemy character
if (InstanceData.Character)
{
// is the last detected danger event within the reaction threshold?
const float ReactionDelta = InstanceData.Character->GetWorld()->GetTimeSeconds() - InstanceData.Character->GetLastDangerTime();
if (ReactionDelta < InstanceData.MaxReactionTime && ReactionDelta > InstanceData.MinReactionTime)
{
// do a dot product check to determine if the danger location is within the character's detection cone
const FVector DangerDir = (InstanceData.Character->GetLastDangerLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal2D();
const float DangerDot = FVector::DotProduct(DangerDir, InstanceData.Character->GetActorForwardVector());
const float ConeAngleCos = FMath::Cos(FMath::DegreesToRadians(InstanceData.DangerSightConeAngle));
return DangerDot > ConeAngleCos;
}
}
return false;
}
#if WITH_EDITOR
FText FStateTreeIsInDangerCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Is Character In Danger</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeComboAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on attack completed delegate
InstanceData.Character->OnAttackCompleted.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
// tell the character to do a combo attack
InstanceData.Character->DoAIComboAttack();
}
return EStateTreeRunStatus::Running;
}
void FStateTreeComboAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on attack completed delegate
InstanceData.Character->OnAttackCompleted.Unbind();
}
}
#if WITH_EDITOR
FText FStateTreeComboAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Do Combo Attack</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeChargedAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on attack completed delegate
InstanceData.Character->OnAttackCompleted.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
// tell the character to do a charged attack
InstanceData.Character->DoAIChargedAttack();
}
return EStateTreeRunStatus::Running;
}
void FStateTreeChargedAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on attack completed delegate
InstanceData.Character->OnAttackCompleted.Unbind();
}
}
#if WITH_EDITOR
FText FStateTreeChargedAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Do Charged Attack</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeWaitForLandingTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind to the on enemy landed delegate
InstanceData.Character->OnEnemyLanded.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()]()
{
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
}
);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeWaitForLandingTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the on enemy landed delegate
InstanceData.Character->OnEnemyLanded.Unbind();
}
}
#if WITH_EDITOR
FText FStateTreeWaitForLandingTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Wait for Landing</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
}
#if WITH_EDITOR
FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Actor</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
}
#if WITH_EDITOR
FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Location</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeSetCharacterSpeedTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the character's max ground speed
InstanceData.Character->GetCharacterMovement()->MaxWalkSpeed = InstanceData.Speed;
}
return EStateTreeRunStatus::Running;
}
#if WITH_EDITOR
FText FStateTreeSetCharacterSpeedTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Set Character Speed</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeGetPlayerInfoTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// get the character possessed by the first local player
InstanceData.TargetPlayerCharacter = Cast<ACharacter>(UGameplayStatics::GetPlayerPawn(InstanceData.Character, 0));
// do we have a valid target?
if (InstanceData.TargetPlayerCharacter)
{
// update the last known location
InstanceData.TargetPlayerLocation = InstanceData.TargetPlayerCharacter->GetActorLocation();
}
// update the distance
InstanceData.DistanceToTarget = FVector::Distance(InstanceData.TargetPlayerLocation, InstanceData.Character->GetActorLocation());
return EStateTreeRunStatus::Running;
}
#if WITH_EDITOR
FText FStateTreeGetPlayerInfoTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Get Player Info</b>");
}
#endif // WITH_EDITOR
@@ -1,365 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "StateTreeTaskBase.h"
#include "StateTreeConditionBase.h"
#include "CombatStateTreeUtility.generated.h"
class ACharacter;
class AAIController;
class ACombatEnemy;
/**
* Instance data struct for the FStateTreeCharacterGroundedCondition condition
*/
USTRUCT()
struct FStateTreeCharacterGroundedConditionInstanceData
{
GENERATED_BODY()
/** Character to check grounded status on */
UPROPERTY(EditAnywhere, Category = "Context")
ACharacter* Character;
/** If true, the condition passes if the character is not grounded instead */
UPROPERTY(EditAnywhere, Category = "Condition")
bool bMustBeOnAir = false;
};
STATETREE_POD_INSTANCEDATA(FStateTreeCharacterGroundedConditionInstanceData);
/**
* StateTree condition to check if the character is grounded
*/
USTRUCT(DisplayName = "Character is Grounded")
struct FStateTreeCharacterGroundedCondition : public FStateTreeConditionCommonBase
{
GENERATED_BODY()
/** Set the instance data type */
using FInstanceDataType = FStateTreeCharacterGroundedConditionInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Default constructor */
FStateTreeCharacterGroundedCondition() = default;
/** Tests the StateTree condition */
virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;
#if WITH_EDITOR
/** Provides the description string */
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the FStateTreeIsInDangerCondition condition
*/
USTRUCT()
struct FStateTreeIsInDangerConditionInstanceData
{
GENERATED_BODY()
/** Character to check danger status on */
UPROPERTY(EditAnywhere, Category = "Context")
ACombatEnemy* Character;
/** Minimum time to wait before reacting to the danger event */
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "s"))
float MinReactionTime = 0.35f;
/** Maximum time to wait before ignoring the danger event */
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "s"))
float MaxReactionTime = 0.75f;
/** Line of sight half angle for detecting incoming danger, in degrees*/
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "degrees"))
float DangerSightConeAngle = 120.0f;
};
STATETREE_POD_INSTANCEDATA(FStateTreeIsInDangerConditionInstanceData);
/**
* StateTree condition to check if the character is about to be hit by an attack
*/
USTRUCT(DisplayName = "Character is in Danger")
struct FStateTreeIsInDangerCondition : public FStateTreeConditionCommonBase
{
GENERATED_BODY()
/** Set the instance data type */
using FInstanceDataType = FStateTreeIsInDangerConditionInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Default constructor */
FStateTreeIsInDangerCondition() = default;
/** Tests the StateTree condition */
virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;
#if WITH_EDITOR
/** Provides the description string */
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Combat StateTree tasks
*/
USTRUCT()
struct FStateTreeAttackInstanceData
{
GENERATED_BODY()
/** Character that will perform the attack */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<ACombatEnemy> Character;
};
/**
* StateTree task to perform a combo attack
*/
USTRUCT(meta=(DisplayName="Combo Attack", Category="Combat"))
struct FStateTreeComboAttackTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeAttackInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
/**
* StateTree task to perform a charged attack
*/
USTRUCT(meta=(DisplayName="Charged Attack", Category="Combat"))
struct FStateTreeChargedAttackTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeAttackInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
/**
* StateTree task to wait for the character to land
*/
USTRUCT(meta=(DisplayName="Wait for Landing", Category="Combat"))
struct FStateTreeWaitForLandingTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeAttackInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Face Towards Actor StateTree task
*/
USTRUCT()
struct FStateTreeFaceActorInstanceData
{
GENERATED_BODY()
/** AI Controller that will determine the focused actor */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AAIController> Controller;
/** Actor that will be faced towards */
UPROPERTY(EditAnywhere, Category = Input)
TObjectPtr<AActor> ActorToFaceTowards;
};
/**
* StateTree task to face an AI-Controlled Pawn towards an Actor
*/
USTRUCT(meta=(DisplayName="Face Towards Actor", Category="Combat"))
struct FStateTreeFaceActorTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeFaceActorInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Face Towards Location StateTree task
*/
USTRUCT()
struct FStateTreeFaceLocationInstanceData
{
GENERATED_BODY()
/** AI Controller that will determine the focused location */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AAIController> Controller;
/** Location that will be faced towards */
UPROPERTY(EditAnywhere, Category = Parameter)
FVector FaceLocation = FVector::ZeroVector;
};
/**
* StateTree task to face an AI-Controlled Pawn towards a world location
*/
USTRUCT(meta=(DisplayName="Face Towards Location", Category="Combat"))
struct FStateTreeFaceLocationTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeFaceLocationInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Set Character Speed StateTree task
*/
USTRUCT()
struct FStateTreeSetCharacterSpeedInstanceData
{
GENERATED_BODY()
/** Character that will be affected */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<ACharacter> Character;
/** Max ground speed to set for the character */
UPROPERTY(EditAnywhere, Category = Parameter)
float Speed = 600.0f;
};
/**
* StateTree task to change a Character's ground speed
*/
USTRUCT(meta=(DisplayName="Set Character Speed", Category="Combat"))
struct FStateTreeSetCharacterSpeedTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeSetCharacterSpeedInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Get Player Info task
*/
USTRUCT()
struct FStateTreeGetPlayerInfoInstanceData
{
GENERATED_BODY()
/** Character that owns this task */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<ACharacter> Character;
/** Character that owns this task */
UPROPERTY(VisibleAnywhere)
TObjectPtr<ACharacter> TargetPlayerCharacter;
/** Last known location for the target */
UPROPERTY(VisibleAnywhere)
FVector TargetPlayerLocation = FVector::ZeroVector;
/** Distance to the target */
UPROPERTY(VisibleAnywhere)
float DistanceToTarget = 0.0f;
};
/**
* StateTree task to get information about the player character
*/
USTRUCT(meta=(DisplayName="GetPlayerInfo", Category="Combat"))
struct FStateTreeGetPlayerInfoTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeGetPlayerInfoInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs while the owning state is active */
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
@@ -1,17 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Combat/AI/EnvQueryContext_Danger.h"
#include "Variant_Combat/AI/CombatEnemy.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "EnvironmentQuery/Items/EnvQueryItemType_Point.h"
void UEnvQueryContext_Danger::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
// get the querying enemy
if (ACombatEnemy* QuerierActor = Cast<ACombatEnemy>(QueryInstance.Owner.Get()))
{
// add the last recorded danger location to the context
UEnvQueryItemType_Point::SetContextHelper(ContextData, QuerierActor->GetLastDangerLocation());
}
}
@@ -1,23 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryContext.h"
#include "EnvQueryContext_Danger.generated.h"
/**
* UEnvQueryContext_Danger
* Returns the enemy character's last known danger location
*/
UCLASS()
class AGRARIANGAME_API UEnvQueryContext_Danger : public UEnvQueryContext
{
GENERATED_BODY()
public:
/** Provides the context locations or actors for this EnvQuery */
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
};
@@ -1,18 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "EnvQueryContext_Player.h"
#include "Kismet/GameplayStatics.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
#include "GameFramework/Pawn.h"
void UEnvQueryContext_Player::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
// get the player pawn for the first local player
AActor* PlayerPawn = UGameplayStatics::GetPlayerPawn(QueryInstance.Owner.Get(), 0);
check(PlayerPawn);
// add the actor data to the context
UEnvQueryItemType_Actor::SetContextHelper(ContextData, PlayerPawn);
}
@@ -1,22 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryContext.h"
#include "EnvQueryContext_Player.generated.h"
/**
* UEnvQueryContext_Player
* Basic EnvQuery Context that returns the first local player
*/
UCLASS()
class UEnvQueryContext_Player : public UEnvQueryContext
{
GENERATED_BODY()
public:
/** Provides the context locations or actors for this EnvQuery */
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
};
@@ -1,21 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNotify_CheckChargedAttack.h"
#include "CombatAttacker.h"
#include "Components/SkeletalMeshComponent.h"
void UAnimNotify_CheckChargedAttack::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
// cast the owner to the attacker interface
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
{
// tell the actor to check for a charged attack loop
AttackerInterface->CheckChargedAttack();
}
}
FString UAnimNotify_CheckChargedAttack::GetNotifyName_Implementation() const
{
return FString("Check Charged Attack");
}
@@ -1,24 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_CheckChargedAttack.generated.h"
/**
* AnimNotify to perform a charged attack hold check.
*/
UCLASS()
class UAnimNotify_CheckChargedAttack : public UAnimNotify
{
GENERATED_BODY()
public:
/** Perform the Anim Notify */
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
/** Get the notify name */
virtual FString GetNotifyName_Implementation() const override;
};
@@ -1,21 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNotify_CheckCombo.h"
#include "CombatAttacker.h"
#include "Components/SkeletalMeshComponent.h"
void UAnimNotify_CheckCombo::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
// cast the owner to the attacker interface
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
{
// tell the actor to check for combo string
AttackerInterface->CheckCombo();
}
}
FString UAnimNotify_CheckCombo::GetNotifyName_Implementation() const
{
return FString("Check Combo String");
}
@@ -1,24 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_CheckCombo.generated.h"
/**
* AnimNotify to perform a combo string check.
*/
UCLASS()
class UAnimNotify_CheckCombo : public UAnimNotify
{
GENERATED_BODY()
public:
/** Perform the Anim Notify */
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
/** Get the notify name */
virtual FString GetNotifyName_Implementation() const override;
};
@@ -1,20 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNotify_DoAttackTrace.h"
#include "CombatAttacker.h"
#include "Components/SkeletalMeshComponent.h"
void UAnimNotify_DoAttackTrace::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
// cast the owner to the attacker interface
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
{
AttackerInterface->DoAttackTrace(AttackBoneName);
}
}
FString UAnimNotify_DoAttackTrace::GetNotifyName_Implementation() const
{
return FString("Do Attack Trace");
}
@@ -1,30 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_DoAttackTrace.generated.h"
/**
* AnimNotify to tell the actor to perform an attack trace check to look for targets to damage.
*/
UCLASS()
class UAnimNotify_DoAttackTrace : public UAnimNotify
{
GENERATED_BODY()
protected:
/** Source bone for the attack trace */
UPROPERTY(EditAnywhere, Category="Attack")
FName AttackBoneName;
public:
/** Perform the Anim Notify */
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
/** Get the notify name */
virtual FString GetNotifyName_Implementation() const override;
};
@@ -1,547 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatCharacter.h"
#include "Components/CapsuleComponent.h"
#include "Components/WidgetComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "CombatLifeBar.h"
#include "Engine/DamageEvents.h"
#include "TimerManager.h"
#include "Engine/LocalPlayer.h"
#include "CombatPlayerController.h"
ACombatCharacter::ACombatCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// bind the attack montage ended delegate
OnAttackMontageEnded.BindUObject(this, &ACombatCharacter::AttackMontageEnded);
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
// Configure character movement
GetCharacterMovement()->MaxWalkSpeed = 400.0f;
// create the camera boom
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = DefaultCameraDistance;
CameraBoom->bUsePawnControlRotation = true;
CameraBoom->bEnableCameraLag = true;
CameraBoom->bEnableCameraRotationLag = true;
// create the orbiting camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// create the life bar widget component
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
LifeBar->SetupAttachment(RootComponent);
// set the player tag
Tags.Add(FName("Player"));
}
void ACombatCharacter::Move(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D MovementVector = Value.Get<FVector2D>();
// route the input
DoMove(MovementVector.X, MovementVector.Y);
}
void ACombatCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
// route the input
DoLook(LookAxisVector.X, LookAxisVector.Y);
}
void ACombatCharacter::ComboAttackPressed()
{
// route the input
DoComboAttackStart();
}
void ACombatCharacter::ChargedAttackPressed()
{
// route the input
DoChargedAttackStart();
}
void ACombatCharacter::ChargedAttackReleased()
{
// route the input
DoChargedAttackEnd();
}
void ACombatCharacter::ToggleCamera()
{
// call the BP hook
BP_ToggleCamera();
}
void ACombatCharacter::DoMove(float Right, float Forward)
{
if (GetController() != nullptr)
{
// find out which way is forward
const FRotator Rotation = GetController()->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// get forward vector
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// get right vector
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// add movement
AddMovementInput(ForwardDirection, Forward);
AddMovementInput(RightDirection, Right);
}
}
void ACombatCharacter::DoLook(float Yaw, float Pitch)
{
if (GetController() != nullptr)
{
// add yaw and pitch input to controller
AddControllerYawInput(Yaw);
AddControllerPitchInput(Pitch);
}
}
void ACombatCharacter::DoComboAttackStart()
{
// are we already playing an attack animation?
if (bIsAttacking)
{
// cache the input time so we can check it later
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
return;
}
// perform a combo attack
ComboAttack();
}
void ACombatCharacter::DoComboAttackEnd()
{
// stub
}
void ACombatCharacter::DoChargedAttackStart()
{
// raise the charging attack flag
bIsChargingAttack = true;
if (bIsAttacking)
{
// cache the input time so we can check it later
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
return;
}
ChargedAttack();
}
void ACombatCharacter::DoChargedAttackEnd()
{
// lower the charging attack flag
bIsChargingAttack = false;
// if we've done the charge loop at least once, release the charged attack right away
if (bHasLoopedChargedAttack)
{
CheckChargedAttack();
}
}
void ACombatCharacter::ResetHP()
{
// reset the current HP total
CurrentHP = MaxHP;
// update the life bar
LifeBarWidget->SetLifePercentage(1.0f);
}
void ACombatCharacter::ComboAttack()
{
// raise the attacking flag
bIsAttacking = true;
// reset the combo count
ComboCount = 0;
// notify enemies they are about to be attacked
NotifyEnemiesOfIncomingAttack();
// play the attack montage
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
// subscribe to montage completed and interrupted events
if (MontageLength > 0.0f)
{
// set the end delegate for the montage
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
}
}
}
void ACombatCharacter::ChargedAttack()
{
// raise the attacking flag
bIsAttacking = true;
// reset the charge loop flag
bHasLoopedChargedAttack = false;
// notify enemies they are about to be attacked
NotifyEnemiesOfIncomingAttack();
// play the charged attack montage
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
// subscribe to montage completed and interrupted events
if (MontageLength > 0.0f)
{
// set the end delegate for the montage
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
}
}
}
void ACombatCharacter::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
// reset the attacking flag
bIsAttacking = false;
// check if we have a non-stale cached input
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= AttackInputCacheTimeTolerance)
{
// are we holding the charged attack button?
if (bIsChargingAttack)
{
// do a charged attack
ChargedAttack();
}
else
{
// do a regular attack
ComboAttack();
}
}
}
void ACombatCharacter::DoAttackTrace(FName DamageSourceBone)
{
// sweep for objects in front of the character to be hit by the attack
TArray<FHitResult> OutHits;
// start at the provided socket location, sweep forward
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
// check for pawn and world dynamic collision object types
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
// use a sphere shape for the sweep
FCollisionShape CollisionShape;
CollisionShape.SetSphere(MeleeTraceRadius);
// ignore self
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
{
// iterate over each object hit
for (const FHitResult& CurrentHit : OutHits)
{
// check if we've hit a damageable actor
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
if (Damageable)
{
// knock upwards and away from the impact normal
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
// pass the damage event to the actor
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
// call the BP handler to play effects, etc.
DealtDamage(MeleeDamage, CurrentHit.ImpactPoint);
}
}
}
}
void ACombatCharacter::CheckCombo()
{
// are we playing a non-charge attack animation?
if (bIsAttacking && !bIsChargingAttack)
{
// is the last attack input not stale?
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= ComboInputCacheTimeTolerance)
{
// consume the attack input so we don't accidentally trigger it twice
CachedAttackInputTime = 0.0f;
// increase the combo counter
++ComboCount;
// do we still have a combo section to play?
if (ComboCount < ComboSectionNames.Num())
{
// notify enemies they are about to be attacked
NotifyEnemiesOfIncomingAttack();
// jump to the next combo section
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
AnimInstance->Montage_JumpToSection(ComboSectionNames[ComboCount], ComboAttackMontage);
}
}
}
}
}
void ACombatCharacter::CheckChargedAttack()
{
// raise the looped charged attack flag
bHasLoopedChargedAttack = true;
// jump to either the loop or the attack section depending on whether we're still holding the charge button
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
AnimInstance->Montage_JumpToSection(bIsChargingAttack ? ChargeLoopSection : ChargeAttackSection, ChargedAttackMontage);
}
}
void ACombatCharacter::NotifyEnemiesOfIncomingAttack()
{
// sweep for objects in front of the character to be hit by the attack
TArray<FHitResult> OutHits;
// start at the actor location, sweep forward
const FVector TraceStart = GetActorLocation();
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * DangerTraceDistance);
// check for pawn object types only
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
// use a sphere shape for the sweep
FCollisionShape CollisionShape;
CollisionShape.SetSphere(DangerTraceRadius);
// ignore self
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
{
// iterate over each object hit
for (const FHitResult& CurrentHit : OutHits)
{
// check if we've hit a damageable actor
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
if (Damageable)
{
// notify the enemy
Damageable->NotifyDanger(GetActorLocation(), this);
}
}
}
}
void ACombatCharacter::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
{
// pass the damage event to the actor
FDamageEvent DamageEvent;
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
// only process knockback and effects if we received nonzero damage
if (ActualDamage > 0.0f)
{
// apply the knockback impulse
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
// is the character ragdolling?
if (GetMesh()->IsSimulatingPhysics())
{
// apply an impulse to the ragdoll
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
}
// pass control to BP to play effects, etc.
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
}
}
void ACombatCharacter::HandleDeath()
{
// disable movement while we're dead
GetCharacterMovement()->DisableMovement();
// enable full ragdoll physics
GetMesh()->SetSimulatePhysics(true);
// hide the life bar
LifeBar->SetHiddenInGame(true);
// pull back the camera
GetCameraBoom()->TargetArmLength = DeathCameraDistance;
// schedule respawning
GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &ACombatCharacter::RespawnCharacter, RespawnTime, false);
}
void ACombatCharacter::ApplyHealing(float Healing, AActor* Healer)
{
// stub
}
void ACombatCharacter::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
{
// stub
}
void ACombatCharacter::RespawnCharacter()
{
// destroy the character and let it be respawned by the Player Controller
Destroy();
}
float ACombatCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// only process damage if the character is still alive
if (CurrentHP <= 0.0f)
{
return 0.0f;
}
// reduce the current HP
CurrentHP -= Damage;
// have we run out of HP?
if (CurrentHP <= 0.0f)
{
// die
HandleDeath();
}
else
{
// update the life bar
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
// enable partial ragdoll physics, but keep the pelvis vertical
GetMesh()->SetPhysicsBlendWeight(0.5f);
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
}
// return the received damage amount
return Damage;
}
void ACombatCharacter::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
// is the character still alive?
if (CurrentHP >= 0.0f)
{
// disable ragdoll physics
GetMesh()->SetPhysicsBlendWeight(0.0f);
}
}
void ACombatCharacter::BeginPlay()
{
Super::BeginPlay();
// get the life bar from the widget component
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
check(LifeBarWidget);
// initialize the camera
GetCameraBoom()->TargetArmLength = DefaultCameraDistance;
// save the relative transform for the mesh so we can reset the ragdoll later
MeshStartingTransform = GetMesh()->GetRelativeTransform();
// set the life bar color
LifeBarWidget->SetBarColor(LifeBarColor);
// reset HP to maximum
ResetHP();
}
void ACombatCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the respawn timer
GetWorld()->GetTimerManager().ClearTimer(RespawnTimer);
}
void ACombatCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Move);
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
// Combo Attack
EnhancedInputComponent->BindAction(ComboAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ComboAttackPressed);
// Charged Attack
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ChargedAttackPressed);
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Completed, this, &ACombatCharacter::ChargedAttackReleased);
// Camera Side Toggle
EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Triggered, this, &ACombatCharacter::ToggleCamera);
}
}
void ACombatCharacter::NotifyControllerChanged()
{
Super::NotifyControllerChanged();
// update the respawn transform on the Player Controller
if (ACombatPlayerController* PC = Cast<ACombatPlayerController>(GetController()))
{
PC->SetRespawnTransform(GetActorTransform());
}
}
@@ -1,334 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CombatAttacker.h"
#include "CombatDamageable.h"
#include "Animation/AnimInstance.h"
#include "CombatCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
struct FInputActionValue;
class UCombatLifeBar;
class UWidgetComponent;
DECLARE_LOG_CATEGORY_EXTERN(LogCombatCharacter, Log, All);
/**
* An enhanced Third Person Character with melee combat capabilities:
* - Combo attack string
* - Press and hold charged attack
* - Damage dealing and reaction
* - Death
* - Respawning
*/
UCLASS(abstract)
class ACombatCharacter : public ACharacter, public ICombatAttacker, public ICombatDamageable
{
GENERATED_BODY()
/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
/** Life bar widget component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UWidgetComponent* LifeBar;
protected:
/** Jump Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* LookAction;
/** Mouse Look Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MouseLookAction;
/** Combo Attack Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* ComboAttackAction;
/** Charged Attack Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* ChargedAttackAction;
/** Toggle Camera Side Input Action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* ToggleCameraAction;
/** Max amount of HP the character will have on respawn */
UPROPERTY(EditAnywhere, Category="Damage", meta = (ClampMin = 0, ClampMax = 100))
float MaxHP = 5.0f;
/** Current amount of HP the character has */
UPROPERTY(VisibleAnywhere, Category="Damage")
float CurrentHP = 0.0f;
/** Life bar widget fill color */
UPROPERTY(EditAnywhere, Category="Damage")
FLinearColor LifeBarColor;
/** Name of the pelvis bone, for damage ragdoll physics */
UPROPERTY(EditAnywhere, Category="Damage")
FName PelvisBoneName;
/** Pointer to the life bar widget */
UPROPERTY(EditAnywhere, Category="Damage")
TObjectPtr<UCombatLifeBar> LifeBarWidget;
/** Max amount of time that may elapse for a non-combo attack input to not be considered stale */
UPROPERTY(EditAnywhere, Category="Melee Attack", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float AttackInputCacheTimeTolerance = 1.0f;
/** Time at which an attack button was last pressed */
float CachedAttackInputTime = 0.0f;
/** If true, the character is currently playing an attack animation */
bool bIsAttacking = false;
/** Distance ahead of the character that melee attack sphere collision traces will extend */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units="cm"))
float MeleeTraceDistance = 75.0f;
/** Radius of the sphere trace for melee attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 200, Units = "cm"))
float MeleeTraceRadius = 75.0f;
/** Distance ahead of the character that enemies will be notified of incoming attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units="cm"))
float DangerTraceDistance = 300.0f;
/** Radius of the sphere trace to notify enemies of incoming attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 200, Units = "cm"))
float DangerTraceRadius = 100.0f;
/** Amount of damage a melee attack will deal */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 100))
float MeleeDamage = 1.0f;
/** Amount of knockback impulse a melee attack will apply */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
float MeleeKnockbackImpulse = 250.0f;
/** Amount of upwards impulse a melee attack will apply */
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
float MeleeLaunchImpulse = 300.0f;
/** AnimMontage that will play for combo attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
UAnimMontage* ComboAttackMontage;
/** Names of the AnimMontage sections that correspond to each stage of the combo attack */
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
TArray<FName> ComboSectionNames;
/** Max amount of time that may elapse for a combo attack input to not be considered stale */
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float ComboInputCacheTimeTolerance = 0.45f;
/** Index of the current stage of the melee attack combo */
int32 ComboCount = 0;
/** AnimMontage that will play for charged attacks */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
UAnimMontage* ChargedAttackMontage;
/** Name of the AnimMontage section that corresponds to the charge loop */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
FName ChargeLoopSection;
/** Name of the AnimMontage section that corresponds to the attack */
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
FName ChargeAttackSection;
/** Flag that determines if the player is currently holding the charged attack input */
bool bIsChargingAttack = false;
/** If true, the charged attack hold check has been tested at least once */
bool bHasLoopedChargedAttack = false;
/** Camera boom length while the character is dead */
UPROPERTY(EditAnywhere, Category="Camera", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
float DeathCameraDistance = 400.0f;
/** Camera boom length when the character respawns */
UPROPERTY(EditAnywhere, Category="Camera", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
float DefaultCameraDistance = 100.0f;
/** Time to wait before respawning the character */
UPROPERTY(EditAnywhere, Category="Respawn", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
float RespawnTime = 3.0f;
/** Attack montage ended delegate */
FOnMontageEnded OnAttackMontageEnded;
/** Character respawn timer */
FTimerHandle RespawnTimer;
/** Copy of the mesh's transform so we can reset it after ragdoll animations */
FTransform MeshStartingTransform;
public:
/** Constructor */
ACombatCharacter();
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** Called for combo attack input */
void ComboAttackPressed();
/** Called for combo attack input pressed */
void ChargedAttackPressed();
/** Called for combo attack input released */
void ChargedAttackReleased();
/** Called for toggle camera side input */
void ToggleCamera();
/** BP hook to animate the camera side switch */
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
void BP_ToggleCamera();
public:
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Right, float Forward);
/** Handles look inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoLook(float Yaw, float Pitch);
/** Handles combo attack pressed from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoComboAttackStart();
/** Handles combo attack released from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoComboAttackEnd();
/** Handles charged attack pressed from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoChargedAttackStart();
/** Handles charged attack released from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoChargedAttackEnd();
protected:
/** Resets the character's current HP to maximum */
void ResetHP();
/** Performs a combo attack */
void ComboAttack();
/** Performs a charged attack */
void ChargedAttack();
/** Called from a delegate when the attack montage ends */
void AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
public:
// ~begin CombatAttacker interface
/** Performs the collision check for an attack */
virtual void DoAttackTrace(FName DamageSourceBone) override;
/** Performs the combo string check */
virtual void CheckCombo() override;
/** Performs the charged attack hold check */
virtual void CheckChargedAttack() override;
// ~end CombatAttacker interface
// ~begin CombatDamageable interface
/** Notifies nearby enemies that an attack is coming so they can react */
void NotifyEnemiesOfIncomingAttack();
/** Handles damage and knockback events */
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
/** Handles death events */
virtual void HandleDeath() override;
/** Handles healing events */
virtual void ApplyHealing(float Healing, AActor* Healer) override;
/** Allows reaction to incoming attacks */
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
// ~end CombatDamageable interface
/** Called from the respawn timer to destroy and re-create the character */
void RespawnCharacter();
public:
/** Overrides the default TakeDamage functionality */
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
/** Overrides landing to reset damage ragdoll physics */
virtual void Landed(const FHitResult& Hit) override;
protected:
/** Blueprint handler to play damage dealt effects */
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
void DealtDamage(float Damage, const FVector& ImpactPoint);
/** Blueprint handler to play damage received effects */
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
void ReceivedDamage(float Damage, const FVector& ImpactPoint, const FVector& DamageDirection);
protected:
/** Initialization */
virtual void BeginPlay() override;
/** Cleanup */
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Handles input bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Handles possessed initialization */
virtual void NotifyControllerChanged() override;
public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};
@@ -1,9 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Combat/CombatGameMode.h"
ACombatGameMode::ACombatGameMode()
{
}
@@ -1,20 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "CombatGameMode.generated.h"
/**
* Simple GameMode for a third person combat game
*/
UCLASS(abstract)
class ACombatGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ACombatGameMode();
};
@@ -1,95 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Combat/CombatPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerStart.h"
#include "CombatCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Engine/World.h"
#include "Blueprint/UserWidget.h"
#include "AgrarianGame.h"
#include "Widgets/Input/SVirtualJoystick.h"
void ACombatPlayerController::BeginPlay()
{
Super::BeginPlay();
// only spawn touch controls on local player controllers
if (ShouldUseTouchControls() && IsLocalPlayerController())
{
// spawn the mobile controls widget
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget)
{
// add the controls to the player screen
MobileControlsWidget->AddToPlayerScreen(0);
} else {
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
}
}
}
void ACombatPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// only add IMCs for local player controllers
if (IsLocalPlayerController())
{
// add the input mapping context
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
}
void ACombatPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// subscribe to the pawn's OnDestroyed delegate
InPawn->OnDestroyed.AddDynamic(this, &ACombatPlayerController::OnPawnDestroyed);
}
void ACombatPlayerController::SetRespawnTransform(const FTransform& NewRespawn)
{
// save the new respawn transform
RespawnTransform = NewRespawn;
}
void ACombatPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
{
// spawn a new character at the respawn transform
if (ACombatCharacter* RespawnedCharacter = GetWorld()->SpawnActor<ACombatCharacter>(CharacterClass, RespawnTransform))
{
// possess the character
Possess(RespawnedCharacter);
}
}
bool ACombatPlayerController::ShouldUseTouchControls() const
{
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}
@@ -1,76 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "CombatPlayerController.generated.h"
class UInputMappingContext;
class ACombatCharacter;
/**
* Simple Player Controller for a third person combat game
* Manages input mappings
* Respawns the player character at the checkpoint when it's destroyed
*/
UCLASS(abstract, Config="Game")
class ACombatPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
/** Input mapping context for this player */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Mobile controls widget to spawn */
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
/** Pointer to the mobile controls widget */
UPROPERTY()
TObjectPtr<UUserWidget> MobileControlsWidget;
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false;
/** Character class to respawn when the possessed pawn is destroyed */
UPROPERTY(EditAnywhere, Category="Respawn")
TSubclassOf<ACombatCharacter> CharacterClass;
/** Transform to respawn the character at. Can be set to create checkpoints */
FTransform RespawnTransform;
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Initialize input bindings */
virtual void SetupInputComponent() override;
/** Pawn initialization */
virtual void OnPossess(APawn* InPawn) override;
public:
/** Updates the character respawn transform */
void SetRespawnTransform(const FTransform& NewRespawn);
protected:
/** Called if the possessed pawn is destroyed */
UFUNCTION()
void OnPawnDestroyed(AActor* DestroyedActor);
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
};
@@ -1,49 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatActivationVolume.h"
#include "Components/BoxComponent.h"
#include "GameFramework/Character.h"
#include "CombatActivatable.h"
ACombatActivationVolume::ACombatActivationVolume()
{
PrimaryActorTick.bCanEverTick = false;
// create the box volume
RootComponent = Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
check(Box);
// set the box's extent
Box->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
// set the default collision profile to overlap all dynamic
Box->SetCollisionProfileName(FName("OverlapAllDynamic"));
// bind the begin overlap
Box->OnComponentBeginOverlap.AddDynamic(this, &ACombatActivationVolume::OnOverlap);
}
void ACombatActivationVolume::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// has a Character entered the volume?
ACharacter* PlayerCharacter = Cast<ACharacter>(OtherActor);
if (PlayerCharacter)
{
// is the Character controlled by a player
if (PlayerCharacter->IsPlayerControlled())
{
// process the actors to activate list
for (AActor* CurrentActor : ActorsToActivate)
{
// is the referenced actor activatable?
if(ICombatActivatable* Activatable = Cast<ICombatActivatable>(CurrentActor))
{
Activatable->ActivateInteraction(PlayerCharacter);
}
}
}
}
}
@@ -1,40 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CombatActivationVolume.generated.h"
class UBoxComponent;
/**
* A simple volume that activates a list of actors when the player pawn enters.
*/
UCLASS()
class ACombatActivationVolume : public AActor
{
GENERATED_BODY()
/** Collision box volume */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
UBoxComponent* Box;
protected:
/** List of actors to activate when this volume is entered */
UPROPERTY(EditAnywhere, Category="Activation Volume")
TArray<AActor*> ActorsToActivate;
public:
/** Constructor */
ACombatActivationVolume();
protected:
/** Handles overlaps with the box volume */
UFUNCTION()
void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
@@ -1,47 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatCheckpointVolume.h"
#include "CombatCharacter.h"
#include "CombatPlayerController.h"
ACombatCheckpointVolume::ACombatCheckpointVolume()
{
// create the box volume
RootComponent = Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
check(Box);
// set the box's extent
Box->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
// set the default collision profile to overlap all dynamic
Box->SetCollisionProfileName(FName("OverlapAllDynamic"));
// bind the begin overlap
Box->OnComponentBeginOverlap.AddDynamic(this, &ACombatCheckpointVolume::OnOverlap);
}
void ACombatCheckpointVolume::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// ensure we use this only once
if (bCheckpointUsed)
{
return;
}
// has the player entered this volume?
ACombatCharacter* PlayerCharacter = Cast<ACombatCharacter>(OtherActor);
if (PlayerCharacter)
{
if (ACombatPlayerController* PC = Cast<ACombatPlayerController>(PlayerCharacter->GetController()))
{
// raise the checkpoint used flag
bCheckpointUsed = true;
// update the player's respawn checkpoint
PC->SetRespawnTransform(PlayerCharacter->GetActorTransform());
}
}
}
@@ -1,32 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/BoxComponent.h"
#include "CombatCheckpointVolume.generated.h"
UCLASS(abstract)
class ACombatCheckpointVolume : public AActor
{
GENERATED_BODY()
/** Collision box volume */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Components, meta = (AllowPrivateAccess = "true"))
UBoxComponent* Box;
public:
/** Constructor */
ACombatCheckpointVolume();
protected:
/** Set to true after use to avoid accidentally resetting the checkpoint */
bool bCheckpointUsed = false;
/** Handles overlaps with the box volume */
UFUNCTION()
void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
@@ -1,83 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatDamageableBox.h"
#include "Components/StaticMeshComponent.h"
#include "TimerManager.h"
#include "Engine/World.h"
ACombatDamageableBox::ACombatDamageableBox()
{
PrimaryActorTick.bCanEverTick = false;
// create the mesh
RootComponent = Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
// set the collision properties
Mesh->SetCollisionProfileName(FName("BlockAllDynamic"));
// enable physics
Mesh->SetSimulatePhysics(true);
// disable navigation relevance so boxes don't affect NavMesh generation
Mesh->bNavigationRelevant = false;
}
void ACombatDamageableBox::RemoveFromLevel()
{
// destroy this actor
Destroy();
}
void ACombatDamageableBox::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the death timer
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
}
void ACombatDamageableBox::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
{
// only process damage if we still have HP
if (CurrentHP > 0.0f)
{
// apply the damage
CurrentHP -= Damage;
// are we dead?
if (CurrentHP <= 0.0f)
{
HandleDeath();
}
// apply a physics impulse to the box, ignoring its mass
Mesh->AddImpulseAtLocation(DamageImpulse * Mesh->GetMass(), DamageLocation);
// call the BP handler to play effects, etc.
OnBoxDamaged(DamageLocation, DamageImpulse);
}
}
void ACombatDamageableBox::HandleDeath()
{
// change the collision object type to Visibility so we ignore most interactions but still retain physics collisions
Mesh->SetCollisionObjectType(ECC_Visibility);
// call the BP handler to play effects, etc.
OnBoxDestroyed();
// set up the death cleanup timer
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &ACombatDamageableBox::RemoveFromLevel, DeathDelayTime);
}
void ACombatDamageableBox::ApplyHealing(float Healing, AActor* Healer)
{
// stub
}
void ACombatDamageableBox::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
{
// stub
}
@@ -1,71 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CombatDamageable.h"
#include "CombatDamageableBox.generated.h"
/**
* A simple physics box that reacts to damage through the ICombatDamageable interface
*/
UCLASS(abstract)
class ACombatDamageableBox : public AActor, public ICombatDamageable
{
GENERATED_BODY()
/** Damageable box mesh */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Mesh;
public:
/** Constructor */
ACombatDamageableBox();
protected:
/** Amount of HP this box starts with. */
UPROPERTY(EditAnywhere, Category="Damage")
float CurrentHP = 3.0f;
/** Time to wait before we remove this box from the level. */
UPROPERTY(EditAnywhere, Category="Damage", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
float DeathDelayTime = 6.0f;
/** Timer to defer destruction of this box after its HP are depleted */
FTimerHandle DeathTimer;
/** Blueprint damage handler for effect playback */
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void OnBoxDamaged(const FVector& DamageLocation, const FVector& DamageImpulse);
/** Blueprint destruction handler for effect playback */
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void OnBoxDestroyed();
/** Timer callback to remove the box from the level after it dies */
void RemoveFromLevel();
public:
/** EndPlay cleanup */
void EndPlay(EEndPlayReason::Type EndPlayReason) override;
// ~Begin CombatDamageable interface
/** Handles damage and knockback events */
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
/** Handles death events */
virtual void HandleDeath() override;
/** Handles healing events */
virtual void ApplyHealing(float Healing, AActor* Healer) override;
/** Allows reaction to incoming attacks */
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
// ~End CombatDamageable interface
};
@@ -1,56 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatDummy.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "PhysicsEngine/PhysicsConstraintComponent.h"
ACombatDummy::ACombatDummy()
{
PrimaryActorTick.bCanEverTick = true;
// create the root
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(Root);
// create the base plate
BasePlate = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Plate"));
BasePlate->SetupAttachment(RootComponent);
// create the dummy
Dummy = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Dummy"));
Dummy->SetupAttachment(RootComponent);
Dummy->SetSimulatePhysics(true);
// create the physics constraint
PhysicsConstraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(TEXT("Physics Constraint"));
PhysicsConstraint->SetupAttachment(RootComponent);
PhysicsConstraint->SetConstrainedComponents(BasePlate, NAME_None, Dummy, NAME_None);
}
void ACombatDummy::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
{
// apply impulse to the dummy
Dummy->AddImpulseAtLocation(DamageImpulse, DamageLocation);
// call the BP handler
BP_OnDummyDamaged(DamageLocation, DamageImpulse.GetSafeNormal());
}
void ACombatDummy::HandleDeath()
{
// unused
}
void ACombatDummy::ApplyHealing(float Healing, AActor* Healer)
{
// unused
}
void ACombatDummy::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
{
// unused
}
@@ -1,63 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CombatDamageable.h"
#include "CombatDummy.generated.h"
class UStaticMeshComponent;
class UPhysicsConstraintComponent;
/**
* A simple invincible combat training dummy
*/
UCLASS(abstract)
class ACombatDummy : public AActor, public ICombatDamageable
{
GENERATED_BODY()
/** Root component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
USceneComponent* Root;
/** Static base plate */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* BasePlate;
/** Physics enabled dummy mesh */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Dummy;
/** Physics constraint holding the dummy and base plate together */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UPhysicsConstraintComponent* PhysicsConstraint;
public:
/** Constructor */
ACombatDummy();
// ~Begin CombatDamageable interface
/** Handles damage and knockback events */
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
/** Handles death events */
virtual void HandleDeath() override;
/** Handles healing events */
virtual void ApplyHealing(float Healing, AActor* Healer) override;
/** Allows reaction to incoming attacks */
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
// ~End CombatDamageable interface
protected:
/** Blueprint handle to apply damage effects */
UFUNCTION(BlueprintImplementableEvent, Category="Combat", meta = (DisplayName = "On Dummy Damaged"))
void BP_OnDummyDamaged(const FVector& Location, const FVector& Direction);
};
@@ -1,27 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatLavaFloor.h"
#include "CombatDamageable.h"
#include "Components/StaticMeshComponent.h"
ACombatLavaFloor::ACombatLavaFloor()
{
PrimaryActorTick.bCanEverTick = false;
// create the mesh
RootComponent = Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
// bind the hit handler
Mesh->OnComponentHit.AddDynamic(this, &ACombatLavaFloor::OnFloorHit);
}
void ACombatLavaFloor::OnFloorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
// check if the hit actor is damageable by casting to the interface
if (ICombatDamageable* Damageable = Cast<ICombatDamageable>(OtherActor))
{
// damage the actor
Damageable->ApplyDamage(Damage, this, Hit.ImpactPoint, FVector::ZeroVector);
}
}
@@ -1,40 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CombatLavaFloor.generated.h"
class UStaticMeshComponent;
class UPrimitiveComponent;
/**
* A basic actor that applies damage on contact through the ICombatDamageable interface.
*/
UCLASS(abstract)
class ACombatLavaFloor : public AActor
{
GENERATED_BODY()
/** Floor mesh */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Mesh;
protected:
/** Amount of damage to deal on contact */
UPROPERTY(EditAnywhere, Category="Damage")
float Damage = 10000.0f;
public:
/** Constructor */
ACombatLavaFloor();
protected:
/** Blocking hit handler */
UFUNCTION()
void OnFloorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
};
@@ -1,4 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatActivatable.h"
@@ -1,36 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "CombatActivatable.generated.h"
/**
* Interactable Interface
* Provides a context-agnostic way of activating, deactivating or toggling actors
*/
UINTERFACE(MinimalAPI, NotBlueprintable)
class UCombatActivatable : public UInterface
{
GENERATED_BODY()
};
class ICombatActivatable
{
GENERATED_BODY()
public:
/** Toggles the Interactable Actor */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void ToggleInteraction(AActor* ActivationInstigator) = 0;
/** Activates the Interactable Actor */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void ActivateInteraction(AActor* ActivationInstigator) = 0;
/** Deactivates the Interactable Actor */
UFUNCTION(BlueprintCallable, Category="Activatable")
virtual void DeactivateInteraction(AActor* ActivationInstigator) = 0;
};
@@ -1,4 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatAttacker.h"
@@ -1,36 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "CombatAttacker.generated.h"
/**
* CombatAttacker Interface
* Provides common functionality to trigger attack animation events.
*/
UINTERFACE(MinimalAPI, NotBlueprintable)
class UCombatAttacker : public UInterface
{
GENERATED_BODY()
};
class ICombatAttacker
{
GENERATED_BODY()
public:
/** Performs an attack's collision check. Usually called from a montage's AnimNotify */
UFUNCTION(BlueprintCallable, Category="Attacker")
virtual void DoAttackTrace(FName DamageSourceBone) = 0;
/** Performs a combo attack's check to continue the string. Usually called from a montage's AnimNotify */
UFUNCTION(BlueprintCallable, Category="Attacker")
virtual void CheckCombo() = 0;
/** Performs a charged attack's check to loop the charge animation. Usually called from a montage's AnimNotify */
UFUNCTION(BlueprintCallable, Category="Attacker")
virtual void CheckChargedAttack() = 0;
};
@@ -1,6 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatDamageable.h"
// Add default functionality here for any ICombatDamageable functions that are not pure virtual.
@@ -1,41 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "CombatDamageable.generated.h"
/**
* CombatDamageable interface
* Provides functionality to handle damage, healing, knockback and death
* Also provides functionality to warn characters of incoming sources of damage
*/
UINTERFACE(MinimalAPI, NotBlueprintable)
class UCombatDamageable : public UInterface
{
GENERATED_BODY()
};
class ICombatDamageable
{
GENERATED_BODY()
public:
/** Handles damage and knockback events */
UFUNCTION(BlueprintCallable, Category="Damageable")
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) = 0;
/** Handles death events */
UFUNCTION(BlueprintCallable, Category="Damageable")
virtual void HandleDeath() = 0;
/** Handles healing events */
UFUNCTION(BlueprintCallable, Category="Damageable")
virtual void ApplyHealing(float Healing, AActor* Healer) = 0;
/** Notifies the actor of impending danger such as an incoming hit, allowing it to react. */
UFUNCTION(BlueprintCallable, Category="Damageable")
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) = 0;
};
@@ -1,5 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CombatLifeBar.h"
@@ -1,26 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "CombatLifeBar.generated.h"
/**
* A basic life bar user widget.
*/
UCLASS(abstract)
class UCombatLifeBar : public UUserWidget
{
GENERATED_BODY()
public:
/** Sets the life bar to the provided 0-1 percentage value*/
UFUNCTION(BlueprintImplementableEvent, Category="Life Bar")
void SetLifePercentage(float Percent);
// Sets the life bar fill color
UFUNCTION(BlueprintImplementableEvent, Category="Life Bar")
void SetBarColor(FLinearColor Color);
};
@@ -1,21 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimNotify_EndDash.h"
#include "PlatformingCharacter.h"
#include "Components/SkeletalMeshComponent.h"
void UAnimNotify_EndDash::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
// cast the owner to the attacker interface
if (APlatformingCharacter* PlatformingCharacter = Cast<APlatformingCharacter>(MeshComp->GetOwner()))
{
// tell the actor to end the dash
PlatformingCharacter->EndDash();
}
}
FString UAnimNotify_EndDash::GetNotifyName_Implementation() const
{
return FString("End Dash");
}
@@ -1,24 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_EndDash.generated.h"
/**
* AnimNotify to finish the dash animation and restore player control
*/
UCLASS()
class UAnimNotify_EndDash : public UAnimNotify
{
GENERATED_BODY()
public:
/** Perform the Anim Notify */
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
/** Get the notify name */
virtual FString GetNotifyName_Implementation() const override;
};
@@ -1,367 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "PlatformingCharacter.h"
#include "Components/CapsuleComponent.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "TimerManager.h"
#include "Engine/LocalPlayer.h"
APlatformingCharacter::APlatformingCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// initialize the flags
bHasWallJumped = false;
bHasDoubleJumped = false;
bHasDashed = false;
bIsDashing = false;
// bind the dash montage ended delegate
OnDashMontageEnded.BindUObject(this, &APlatformingCharacter::DashMontageEnded);
// enable press and hold jump
JumpMaxHoldTime = 0.4f;
// set the jump max count to 3 so we can double jump and check for coyote time jumps
JumpMaxCount = 3;
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
// don't rotate the mesh when the controller rotates
bUseControllerRotationYaw = false;
// Configure character movement
GetCharacterMovement()->GravityScale = 2.5f;
GetCharacterMovement()->MaxAcceleration = 1500.0f;
GetCharacterMovement()->BrakingFrictionFactor = 1.0f;
GetCharacterMovement()->bUseSeparateBrakingFriction = true;
GetCharacterMovement()->GroundFriction = 4.0f;
GetCharacterMovement()->MaxWalkSpeed = 750.0f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
GetCharacterMovement()->BrakingDecelerationWalking = 2500.0f;
GetCharacterMovement()->PerchRadiusThreshold = 15.0f;
GetCharacterMovement()->JumpZVelocity = 350.0f;
GetCharacterMovement()->BrakingDecelerationFalling = 750.0f;
GetCharacterMovement()->AirControl = 1.0f;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->NavAgentProps.AgentRadius = 42.0;
GetCharacterMovement()->NavAgentProps.AgentHeight = 192.0;
// create the camera boom
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
CameraBoom->bEnableCameraLag = true;
CameraBoom->CameraLagSpeed = 8.0f;
CameraBoom->bEnableCameraRotationLag = true;
CameraBoom->CameraRotationLagSpeed = 8.0f;
// create the orbiting camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
void APlatformingCharacter::Move(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
// route the input
DoMove(MovementVector.X, MovementVector.Y);
}
void APlatformingCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
// route the input
DoLook(LookAxisVector.X, LookAxisVector.Y);
}
void APlatformingCharacter::Dash()
{
// route the input
DoDash();
}
void APlatformingCharacter::MultiJump()
{
// ignore jumps while dashing
if(bIsDashing)
return;
// are we already in the air?
if (GetCharacterMovement()->IsFalling())
{
// have we already wall jumped?
if (!bHasWallJumped)
{
// run a sphere sweep to check if we're in front of a wall
FHitResult OutHit;
const FVector TraceStart = GetActorLocation();
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * WallJumpTraceDistance);
const FCollisionShape TraceShape = FCollisionShape::MakeSphere(WallJumpTraceRadius);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepSingleByChannel(OutHit, TraceStart, TraceEnd, FQuat(), ECollisionChannel::ECC_Visibility, TraceShape, QueryParams))
{
// rotate the character to face away from the wall, so we're correctly oriented for the next wall jump
FRotator WallOrientation = OutHit.ImpactNormal.ToOrientationRotator();
WallOrientation.Pitch = 0.0f;
WallOrientation.Roll = 0.0f;
SetActorRotation(WallOrientation);
// apply a launch impulse to the character to perform the actual wall jump
const FVector WallJumpImpulse = (OutHit.ImpactNormal * WallJumpBounceImpulse) + (FVector::UpVector * WallJumpVerticalImpulse);
LaunchCharacter(WallJumpImpulse, true, true);
// enable the jump trail
SetJumpTrailState(true);
// raise the wall jump flag to prevent an immediate second wall jump
bHasWallJumped = true;
GetWorld()->GetTimerManager().SetTimer(WallJumpTimer, this, &APlatformingCharacter::ResetWallJump, DelayBetweenWallJumps, false);
}
// no wall jump, try a double jump next
else
{
// are we still within coyote time frames?
if (GetWorld()->GetTimeSeconds() - LastFallTime < MaxCoyoteTime)
{
UE_LOG(LogTemp, Warning, TEXT("Coyote Jump"));
// use the built-in CMC functionality to do the jump
Jump();
// enable the jump trail
SetJumpTrailState(true);
// no coyote time jump
} else {
// only double jump once while we're in the air
if (!bHasDoubleJumped)
{
bHasDoubleJumped = true;
// use the built-in CMC functionality to do the double jump
Jump();
// enable the jump trail
SetJumpTrailState(true);
}
}
}
}
}
else
{
// we're grounded so just do a regular jump
Jump();
// activate the jump trail
SetJumpTrailState(true);
}
}
void APlatformingCharacter::ResetWallJump()
{
// reset the wall jump input lock
bHasWallJumped = false;
}
void APlatformingCharacter::DoMove(float Right, float Forward)
{
if (GetController() != nullptr)
{
// momentarily disable movement inputs if we've just wall jumped
if (!bHasWallJumped)
{
// find out which way is forward
const FRotator Rotation = GetController()->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// get forward vector
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// get right vector
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// add movement
AddMovementInput(ForwardDirection, Forward);
AddMovementInput(RightDirection, Right);
}
}
}
void APlatformingCharacter::DoLook(float Yaw, float Pitch)
{
if (GetController() != nullptr)
{
// add yaw and pitch input to controller
AddControllerYawInput(Yaw);
AddControllerPitchInput(Pitch);
}
}
void APlatformingCharacter::DoDash()
{
// ignore the input if we've already dashed and have yet to reset
if (bHasDashed)
return;
// raise the dash flags
bIsDashing = true;
bHasDashed = true;
// disable gravity while dashing
GetCharacterMovement()->GravityScale = 0.0f;
// reset the character velocity so we don't carry momentum into the dash
GetCharacterMovement()->Velocity = FVector::ZeroVector;
// enable the jump trails
SetJumpTrailState(true);
// play the dash montage
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
{
const float MontageLength = AnimInstance->Montage_Play(DashMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
// has the montage played successfully?
if (MontageLength > 0.0f)
{
AnimInstance->Montage_SetEndDelegate(OnDashMontageEnded, DashMontage);
}
}
}
void APlatformingCharacter::DoJumpStart()
{
// handle special jump cases
MultiJump();
}
void APlatformingCharacter::DoJumpEnd()
{
// stop jumping
StopJumping();
}
void APlatformingCharacter::DashMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
// end the dash
EndDash();
}
void APlatformingCharacter::EndDash()
{
// restore gravity
GetCharacterMovement()->GravityScale = 2.5f;
// reset the dashing flag
bIsDashing = false;
// are we grounded after the dash?
if (GetCharacterMovement()->IsMovingOnGround())
{
// reset the dash usage flag, since we won't receive a landed event
bHasDashed = false;
// deactivate the jump trails
SetJumpTrailState(false);
}
}
bool APlatformingCharacter::HasDoubleJumped() const
{
return bHasDoubleJumped;
}
bool APlatformingCharacter::HasWallJumped() const
{
return bHasWallJumped;
}
void APlatformingCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the wall jump reset timer
GetWorld()->GetTimerManager().ClearTimer(WallJumpTimer);
}
void APlatformingCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &APlatformingCharacter::DoJumpStart);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &APlatformingCharacter::DoJumpEnd);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Move);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Look);
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Look);
// Dashing
EnhancedInputComponent->BindAction(DashAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Dash);
}
}
void APlatformingCharacter::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
// reset the double jump and dash flags
bHasDoubleJumped = false;
bHasDashed = false;
// deactivate the jump trail
SetJumpTrailState(false);
}
void APlatformingCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode /*= 0*/)
{
Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);
// are we falling?
if (GetCharacterMovement()->MovementMode == EMovementMode::MOVE_Falling)
{
// save the game time when we started falling, so we can check it later for coyote time jumps
LastFallTime = GetWorld()->GetTimeSeconds();
}
}
@@ -1,194 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Animation/AnimInstance.h"
#include "PlatformingCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
struct FInputActionValue;
class UAnimMontage;
/**
* An enhanced Third Person Character with the following functionality:
* - Platforming game character movement physics
* - Press and Hold Jump
* - Double Jump
* - Wall Jump
* - Dash
*/
UCLASS(abstract)
class APlatformingCharacter : public ACharacter
{
GENERATED_BODY()
/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
protected:
/** Jump Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* LookAction;
/** Mouse Look Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MouseLookAction;
/** Dash Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* DashAction;
public:
/** Constructor */
APlatformingCharacter();
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** Called for dash input */
void Dash();
/** Called for jump pressed to check for advanced multi-jump conditions */
void MultiJump();
/** Resets the wall jump input lock */
void ResetWallJump();
public:
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Right, float Forward);
/** Handles look inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoLook(float Yaw, float Pitch);
/** Handles dash inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoDash();
/** Handles jump pressed inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpStart();
/** Handles jump pressed inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpEnd();
protected:
/** Called from a delegate when the dash montage ends */
void DashMontageEnded(UAnimMontage* Montage, bool bInterrupted);
/** Passes control to Blueprint to enable or disable jump trails */
UFUNCTION(BlueprintImplementableEvent, Category="Platforming")
void SetJumpTrailState(bool bEnabled);
public:
/** Ends the dash state */
void EndDash();
public:
/** Returns true if the character has just double jumped */
UFUNCTION(BlueprintPure, Category="Platforming")
bool HasDoubleJumped() const;
/** Returns true if the character has just wall jumped */
UFUNCTION(BlueprintPure, Category="Platforming")
bool HasWallJumped() const;
public:
/** EndPlay cleanup */
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Sets up input action bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Handle landings to reset dash and advanced jump state */
virtual void Landed(const FHitResult& Hit) override;
/** Handle movement mode changes to keep track of coyote time jumps */
virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0) override;
protected:
/** movement state flag bits, packed into a uint8 for memory efficiency */
uint8 bHasWallJumped : 1;
uint8 bHasDoubleJumped : 1;
uint8 bHasDashed : 1;
uint8 bIsDashing : 1;
/** timer for wall jump input reset */
FTimerHandle WallJumpTimer;
/** Dash montage ended delegate */
FOnMontageEnded OnDashMontageEnded;
/** Distance to trace ahead of the character to look for walls to jump from */
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
float WallJumpTraceDistance = 50.0f;
/** Radius of the wall jump sphere trace check */
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 100, Units = "cm"))
float WallJumpTraceRadius = 25.0f;
/** Impulse to apply away from the wall when wall jumping */
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm/s"))
float WallJumpBounceImpulse = 800.0f;
/** Vertical impulse to apply when wall jumping */
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm/s"))
float WallJumpVerticalImpulse = 900.0f;
/** Time to ignore jump inputs after a wall jump */
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float DelayBetweenWallJumps = 0.1f;
/** AnimMontage to use for the Dash action */
UPROPERTY(EditAnywhere, Category="Dash")
UAnimMontage* DashMontage;
/** Last recorded time when this character started falling */
float LastFallTime = 0.0f;
/** Max amount of time that can pass since we started falling when we allow a regular jump */
UPROPERTY(EditAnywhere, Category="Coyote Time", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float MaxCoyoteTime = 0.16f;
public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};
@@ -1,9 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Platforming/PlatformingGameMode.h"
APlatformingGameMode::APlatformingGameMode()
{
// stub
}
@@ -1,21 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "PlatformingGameMode.generated.h"
/**
* Simple GameMode for a third person platforming game
*/
UCLASS()
class APlatformingGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
/** Constructor */
APlatformingGameMode();
};
@@ -1,98 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Platforming/PlatformingPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerStart.h"
#include "PlatformingCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Engine/World.h"
#include "Blueprint/UserWidget.h"
#include "AgrarianGame.h"
#include "Widgets/Input/SVirtualJoystick.h"
void APlatformingPlayerController::BeginPlay()
{
Super::BeginPlay();
// only spawn touch controls on local player controllers
if (ShouldUseTouchControls() && IsLocalPlayerController())
{
// spawn the mobile controls widget
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget)
{
// add the controls to the player screen
MobileControlsWidget->AddToPlayerScreen(0);
} else {
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
}
}
}
void APlatformingPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// only add IMCs for local player controllers
if (IsLocalPlayerController())
{
// add the input mapping context
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
}
void APlatformingPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// subscribe to the pawn's OnDestroyed delegate
InPawn->OnDestroyed.AddDynamic(this, &APlatformingPlayerController::OnPawnDestroyed);
}
void APlatformingPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
{
// find the player start
TArray<AActor*> ActorList;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList);
if (ActorList.Num() > 0)
{
// spawn a character at the player start
const FTransform SpawnTransform = ActorList[0]->GetActorTransform();
if (APlatformingCharacter* RespawnedCharacter = GetWorld()->SpawnActor<APlatformingCharacter>(CharacterClass, SpawnTransform))
{
// possess the character
Possess(RespawnedCharacter);
}
}
}
bool APlatformingPlayerController::ShouldUseTouchControls() const
{
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}
@@ -1,65 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "PlatformingPlayerController.generated.h"
class UInputMappingContext;
class APlatformingCharacter;
/**
* Simple Player Controller for a third person platforming game
* Manages input mappings
* Respawns the player character at the Player Start when it's destroyed
*/
UCLASS(abstract, Config="Game")
class APlatformingPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
/** Input mapping context for this player */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Mobile controls widget to spawn */
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
/** Pointer to the mobile controls widget */
UPROPERTY()
TObjectPtr<UUserWidget> MobileControlsWidget;
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false;
/** Character class to respawn when the possessed pawn is destroyed */
UPROPERTY(EditAnywhere, Category="Respawn")
TSubclassOf<APlatformingCharacter> CharacterClass;
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Initialize input bindings */
virtual void SetupInputComponent() override;
/** Pawn initialization */
virtual void OnPossess(APawn* InPawn) override;
/** Called if the possessed pawn is destroyed */
UFUNCTION()
void OnPawnDestroyed(AActor* DestroyedActor);
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
};
@@ -1,19 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingAIController.h"
#include "GameplayStateTreeModule/Public/Components/StateTreeAIComponent.h"
ASideScrollingAIController::ASideScrollingAIController()
{
// create the StateTree AI Component
StateTreeAI = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
check(StateTreeAI);
// ensure we start the StateTree when we possess the pawn
bStartAILogicOnPossess = true;
// ensure we're attached to the possessed character.
// this is necessary for EnvQueries to work correctly
bAttachToPawn = true;
}
@@ -1,27 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "SideScrollingAIController.generated.h"
class UStateTreeAIComponent;
/**
* A basic AI Controller capable of running StateTree
*/
UCLASS(abstract)
class ASideScrollingAIController : public AAIController
{
GENERATED_BODY()
/** StateTree Component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI", meta = (AllowPrivateAccess = "true"))
UStateTreeAIComponent* StateTreeAI;
public:
/** Constructor */
ASideScrollingAIController();
};
@@ -1,53 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingNPC.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "TimerManager.h"
ASideScrollingNPC::ASideScrollingNPC()
{
PrimaryActorTick.bCanEverTick = true;
GetCharacterMovement()->MaxWalkSpeed = 150.0f;
}
void ASideScrollingNPC::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the deactivation timer
GetWorld()->GetTimerManager().ClearTimer(DeactivationTimer);
}
void ASideScrollingNPC::Interaction(AActor* Interactor)
{
// ignore if this NPC has already been deactivated
if (bDeactivated)
{
return;
}
// reset the deactivation flag
bDeactivated = true;
// stop character movement immediately
GetCharacterMovement()->StopMovementImmediately();
// launch the NPC away from the interactor
FVector LaunchVector = Interactor->GetActorForwardVector() * LaunchImpulse;
LaunchVector.Y = 0.0f;
LaunchVector.Z = LaunchVerticalImpulse;
LaunchCharacter(LaunchVector, true, true);
// set up a timer to schedule reactivation
GetWorld()->GetTimerManager().SetTimer(DeactivationTimer, this, &ASideScrollingNPC::ResetDeactivation, DeactivationTime, false);
}
void ASideScrollingNPC::ResetDeactivation()
{
// reset the deactivation flag
bDeactivated = false;
}
@@ -1,64 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SideScrollingInteractable.h"
#include "SideScrollingNPC.generated.h"
/**
* Simple platforming NPC
* Its behaviors will be dictated by a possessing AI Controller
* It can be temporarily deactivated through Actor interactions
*/
UCLASS(abstract)
class ASideScrollingNPC : public ACharacter, public ISideScrollingInteractable
{
GENERATED_BODY()
protected:
/** Horizontal impulse to apply to the NPC when it's interacted with */
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10000, Units="cm/s"))
float LaunchImpulse = 500.0f;
/** Vertical impulse to apply to the NPC when it's interacted with */
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10000, Units="cm/s"))
float LaunchVerticalImpulse = 500.0f;
/** Time that the NPC remains deactivated after being interacted with */
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10, Units="s"))
float DeactivationTime = 3.0f;
public:
/** If true, this NPC is deactivated and will not be interacted with */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="NPC")
bool bDeactivated = false;
/** Timer to reactivate the NPC */
FTimerHandle DeactivationTimer;
public:
/** Constructor */
ASideScrollingNPC();
public:
/** Cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
public:
// ~begin IInteractable interface
/** Performs an interaction triggered by another actor */
virtual void Interaction(AActor* Interactor) override;
// ~end IInteractable interface
/** Reactivates the NPC */
void ResetDeactivation();
};
@@ -1,32 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingStateTreeUtility.h"
#include "StateTreeExecutionContext.h"
#include "StateTreeExecutionTypes.h"
#include "AIController.h"
#include "Kismet/GameplayStatics.h"
EStateTreeRunStatus FStateTreeGetPlayerTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the player pawn as the target
InstanceData.TargetPlayer = UGameplayStatics::GetPlayerPawn(InstanceData.Controller.Get(), 0);
// are the NPC and target valid?
if (IsValid(InstanceData.TargetPlayer) && IsValid(InstanceData.NPC))
{
InstanceData.bValidTarget = FVector::Distance(InstanceData.NPC->GetActorLocation(), InstanceData.TargetPlayer->GetActorLocation()) < InstanceData.RangeMax;
}
return EStateTreeRunStatus::Running;
}
#if WITH_EDITOR
FText FStateTreeGetPlayerTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Get Player</b>");
}
#endif // WITH_EDITOR
@@ -1,59 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "StateTreeTaskBase.h"
#include "SideScrollingStateTreeUtility.generated.h"
class AAIController;
/**
* Instance data for the FStateTreeGetPlayerTask task
*/
USTRUCT()
struct FStateTreeGetPlayerInstanceData
{
GENERATED_BODY()
/** NPC owning this task */
UPROPERTY(VisibleAnywhere, Category="Context")
TObjectPtr<APawn> NPC;
/** Holds the found player pawn */
UPROPERTY(VisibleAnywhere, Category="Context")
TObjectPtr<AAIController> Controller;
/** Holds the found player pawn */
UPROPERTY(VisibleAnywhere, Category="Output")
TObjectPtr<APawn> TargetPlayer;
/** Is the pawn close enough to be considered a valid target? */
UPROPERTY(VisibleAnywhere, Category="Output")
bool bValidTarget = false;
/** Max distance to be considered a valid target */
UPROPERTY(EditAnywhere, Category="Parameter", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm"))
float RangeMax = 1000.0f;
};
/**
* StateTree task to get the player-controlled character
*/
USTRUCT(meta=(DisplayName="Get Player", Category="Side Scrolling"))
struct FStateTreeGetPlayerTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeGetPlayerInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs while the owning state is active */
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
@@ -1,46 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingJumpPad.h"
#include "Components/BoxComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SceneComponent.h"
ASideScrollingJumpPad::ASideScrollingJumpPad()
{
PrimaryActorTick.bCanEverTick = false;
// create the root comp
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the bounding box
Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
Box->SetupAttachment(RootComponent);
// configure the bounding box
Box->SetBoxExtent(FVector(115.0f, 90.0f, 20.0f), false);
Box->SetRelativeLocation(FVector(0.0f, 0.0f, 16.0f));
Box->SetCollisionObjectType(ECC_WorldDynamic);
Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Box->SetCollisionResponseToAllChannels(ECR_Ignore);
Box->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// add the overlap handler
OnActorBeginOverlap.AddDynamic(this, &ASideScrollingJumpPad::BeginOverlap);
}
void ASideScrollingJumpPad::BeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
// were we overlapped by a character?
if (ACharacter* OverlappingCharacter = Cast<ACharacter>(OtherActor))
{
// force the character to jump
OverlappingCharacter->Jump();
// launch the character to override its vertical velocity
FVector LaunchVelocity = FVector::UpVector * ZStrength;
OverlappingCharacter->LaunchCharacter(LaunchVelocity, false, true);
}
}
@@ -1,39 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SideScrollingJumpPad.generated.h"
class UBoxComponent;
/**
* A simple jump pad that launches characters into the air
*/
UCLASS(abstract)
class ASideScrollingJumpPad : public AActor
{
GENERATED_BODY()
/** Jump pad bounding box */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UBoxComponent* Box;
protected:
/** Vertical velocity to set the character to when they use the jump pad */
UPROPERTY(EditAnywhere, Category="Jump Pad", meta = (ClampMin=0, ClampMax=10000, Units="cm/s"))
float ZStrength = 1000.0f;
public:
/** Constructor */
ASideScrollingJumpPad();
protected:
UFUNCTION()
void BeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
};
@@ -1,40 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingMovingPlatform.h"
#include "Components/SceneComponent.h"
ASideScrollingMovingPlatform::ASideScrollingMovingPlatform()
{
PrimaryActorTick.bCanEverTick = false;
// create the root comp
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
}
void ASideScrollingMovingPlatform::Interaction(AActor* Interactor)
{
// ignore interactions if we're already moving
if (bMoving)
{
return;
}
// raise the movement flag
bMoving = true;
// pass control to BP for the actual movement
BP_MoveToTarget();
}
void ASideScrollingMovingPlatform::ResetInteraction()
{
// ignore if this is a one-shot platform
if (bOneShot)
{
return;
}
// reset the movement flag
bMoving = false;
}
@@ -1,60 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SideScrollingInteractable.h"
#include "SideScrollingMovingPlatform.generated.h"
/**
* Simple moving platform that can be triggered through interactions by other actors.
* The actual movement is performed by Blueprint code through latent execution nodes.
*/
UCLASS(abstract)
class ASideScrollingMovingPlatform : public AActor, public ISideScrollingInteractable
{
GENERATED_BODY()
public:
/** Constructor */
ASideScrollingMovingPlatform();
protected:
/** If this is true, the platform is mid-movement and will ignore further interactions */
bool bMoving = false;
/** Destination of the platform in world space */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Moving Platform")
FVector PlatformTarget;
/** Time for the platform to move to the destination */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Moving Platform", meta = (ClampMin = 0, ClampMax = 10, Units="s"))
float MoveDuration = 5.0f;
/** If this is true, the platform will only move once. */
UPROPERTY(EditAnywhere, Category="Moving Platform")
bool bOneShot = false;
public:
// ~begin IInteractable interface
/** Performs an interaction triggered by another actor */
virtual void Interaction(AActor* Interactor) override;
// ~end IInteractable interface
/** Resets the interaction state. Must be called from BP code to reset the platform */
UFUNCTION(BlueprintCallable, Category="Moving Platform")
virtual void ResetInteraction();
protected:
/** Allows Blueprint code to do the actual platform movement */
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category="Moving Platform", meta = (DisplayName="Move to Target"))
void BP_MoveToTarget();
};
@@ -1,55 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingPickup.h"
#include "GameFramework/Character.h"
#include "SideScrollingGameMode.h"
#include "Components/SphereComponent.h"
#include "Components/SceneComponent.h"
#include "Engine/World.h"
ASideScrollingPickup::ASideScrollingPickup()
{
PrimaryActorTick.bCanEverTick = false;
// create the root comp
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the bounding sphere
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
Sphere->SetupAttachment(RootComponent);
Sphere->SetSphereRadius(100.0f);
Sphere->SetCollisionObjectType(ECC_WorldDynamic);
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// add the overlap handler
OnActorBeginOverlap.AddDynamic(this, &ASideScrollingPickup::BeginOverlap);
}
void ASideScrollingPickup::BeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
// have we collided against a character?
if (ACharacter* OverlappedCharacter = Cast<ACharacter>(OtherActor))
{
// is this the player character?
if (OverlappedCharacter->IsPlayerControlled())
{
// get the game mode
if (ASideScrollingGameMode* GM = Cast<ASideScrollingGameMode>(GetWorld()->GetAuthGameMode()))
{
// tell the game mode to process a pickup
GM->ProcessPickup();
// disable collision so we don't get picked up again
SetActorEnableCollision(false);
// Call the BP handler. It will be responsible for destroying the pickup
BP_OnPickedUp();
}
}
}
}
@@ -1,38 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SideScrollingPickup.generated.h"
class USphereComponent;
/**
* A simple side scrolling game pickup
* Increments a counter on the GameMode
*/
UCLASS(abstract)
class ASideScrollingPickup : public AActor
{
GENERATED_BODY()
/** Pickup bounding sphere */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
USphereComponent* Sphere;
public:
/** Constructor */
ASideScrollingPickup();
protected:
/** Handles pickup collision */
UFUNCTION()
void BeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
/** Passes control to BP to play effects on pickup */
UFUNCTION(BlueprintImplementableEvent, Category="Pickup", meta = (DisplayName = "On Picked Up"))
void BP_OnPickedUp();
};
@@ -1,59 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingSoftPlatform.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "SideScrollingCharacter.h"
ASideScrollingSoftPlatform::ASideScrollingSoftPlatform()
{
PrimaryActorTick.bCanEverTick = true;
// create the root component
RootComponent = Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the mesh
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(Root);
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Mesh->SetCollisionObjectType(ECC_WorldStatic);
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
// create the collision check box
CollisionCheckBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Collision Check Box"));
CollisionCheckBox->SetupAttachment(Mesh);
CollisionCheckBox->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
CollisionCheckBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
CollisionCheckBox->SetCollisionObjectType(ECC_WorldDynamic);
CollisionCheckBox->SetCollisionResponseToAllChannels(ECR_Ignore);
CollisionCheckBox->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// subscribe to the overlap events
CollisionCheckBox->OnComponentBeginOverlap.AddDynamic(this, &ASideScrollingSoftPlatform::OnSoftCollisionOverlap);
}
void ASideScrollingSoftPlatform::OnSoftCollisionOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// have we overlapped a character?
if (ASideScrollingCharacter* Char = Cast<ASideScrollingCharacter>(OtherActor))
{
// disable the soft collision channel
Char->SetSoftCollision(true);
}
}
void ASideScrollingSoftPlatform::NotifyActorEndOverlap(AActor* OtherActor)
{
Super::NotifyActorEndOverlap(OtherActor);
// have we overlapped a character?
if (ASideScrollingCharacter* Char = Cast<ASideScrollingCharacter>(OtherActor))
{
// enable the soft collision channel
Char->SetSoftCollision(false);
}
}
@@ -1,46 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SideScrollingSoftPlatform.generated.h"
class USceneComponent;
class UStaticMeshComponent;
class UBoxComponent;
/**
* A side scrolling game platform that the character can jump or drop through.
*/
UCLASS(abstract)
class ASideScrollingSoftPlatform : public AActor
{
GENERATED_BODY()
/** Root component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
USceneComponent* Root;
/** Platform mesh. The part we collide against and see */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Mesh;
/** Collision volume that toggles soft collision on the character when they're below the platform. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
UBoxComponent* CollisionCheckBox;
public:
/** Constructor */
ASideScrollingSoftPlatform();
protected:
/** Handles soft collision check box overlaps */
UFUNCTION()
void OnSoftCollisionOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
/** Restores soft collision state when overlap ends */
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
};
@@ -1,6 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingInteractable.h"
// Add default functionality here for any IInteractable functions that are not pure virtual.
@@ -1,31 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SideScrollingInteractable.generated.h"
/**
*
*/
UINTERFACE(MinimalAPI, NotBlueprintable)
class USideScrollingInteractable : public UInterface
{
GENERATED_BODY()
};
/**
* Simple interface to allow Actors to interact without having knowledge of their internal implementation.
*/
class ISideScrollingInteractable
{
GENERATED_BODY()
public:
/** Triggers an interaction by the provided Actor */
UFUNCTION(BlueprintCallable, Category="Interactable")
virtual void Interaction(AActor* Interactor) = 0;
};
@@ -1,105 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingCameraManager.h"
#include "GameFramework/Pawn.h"
#include "Engine/HitResult.h"
#include "CollisionQueryParams.h"
#include "Engine/World.h"
void ASideScrollingCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
{
// ensure the view target is a pawn
APawn* TargetPawn = Cast<APawn>(OutVT.Target);
// is our target valid?
if (IsValid(TargetPawn))
{
// set the view target FOV and rotation
OutVT.POV.Rotation = FRotator(0.0f, -90.0f, 0.0f);
OutVT.POV.FOV = 65.0f;
// cache the current location
FVector CurrentActorLocation = OutVT.Target->GetActorLocation();
// copy the current camera location
FVector CurrentCameraLocation = GetCameraLocation();
// calculate the "zoom distance" - in reality the distance we want to keep to the target
float CurrentY = CurrentZoom + CurrentActorLocation.Y;
// do first-time setup
if (bSetup)
{
// lower the setup flag
bSetup = false;
// initialize the camera viewpoint and return
OutVT.POV.Location.X = CurrentActorLocation.X;
OutVT.POV.Location.Y = CurrentY;
OutVT.POV.Location.Z = CurrentActorLocation.Z + CameraZOffset;
// save the current camera height
CurrentZ = OutVT.POV.Location.Z;
// skip the rest of the calculations
return;
}
// check if the camera needs to update its height
bool bZUpdate = false;
// is the character moving vertically?
if (FMath::IsNearlyZero(TargetPawn->GetVelocity().Z))
{
// determine if we need to do a height update
bZUpdate = FMath::IsNearlyEqual(CurrentZ, CurrentCameraLocation.Z, 25.0f);
} else {
// run a trace below the character to determine if we need to do a height update
FHitResult OutHit;
const FVector End = CurrentActorLocation + FVector(0.0f, 0.0f, -1000.0f);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(TargetPawn);
// only update height if we're not about to hit ground
bZUpdate = !GetWorld()->LineTraceSingleByChannel(OutHit, CurrentActorLocation, End, ECC_Visibility, QueryParams);
}
// do we need to do a height update?
if (bZUpdate)
{
// set the height goal from the actor location
CurrentZ = CurrentActorLocation.Z;
} else {
// are we close enough to the target height?
if (FMath::IsNearlyEqual(CurrentZ, CurrentActorLocation.Z, 100.0f))
{
// set the height goal from the actor location
CurrentZ = CurrentActorLocation.Z;
} else {
// blend the height towards the actor location
CurrentZ = FMath::FInterpTo(CurrentZ, CurrentActorLocation.Z, DeltaTime, 2.0f);
}
}
// clamp the X axis to the min and max camera bounds
float CurrentX = FMath::Clamp(CurrentActorLocation.X, CameraXMinBounds, CameraXMaxBounds);
// blend towards the new camera location and update the output
FVector TargetCameraLocation(CurrentX, CurrentY, CurrentZ);
OutVT.POV.Location = FMath::VInterpTo(CurrentCameraLocation, TargetCameraLocation, DeltaTime, 2.0f);
}
}
@@ -1,47 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Camera/PlayerCameraManager.h"
#include "SideScrollingCameraManager.generated.h"
/**
* Simple side scrolling camera with smooth scrolling and horizontal bounds
*/
UCLASS()
class ASideScrollingCameraManager : public APlayerCameraManager
{
GENERATED_BODY()
public:
/** Overrides the default camera view target calculation */
virtual void UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime) override;
public:
/** How close we want to stay to the view target */
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=0, ClampMax=10000, Units="cm"))
float CurrentZoom = 1000.0f;
/** How far above the target do we want the camera to focus */
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=0, ClampMax=10000, Units="cm"))
float CameraZOffset = 100.0f;
/** Minimum camera scrolling bounds in world space */
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=-100000, ClampMax=100000, Units="cm"))
float CameraXMinBounds = -400.0f;
/** Maximum camera scrolling bounds in world space */
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=-100000, ClampMax=100000, Units="cm"))
float CameraXMaxBounds = 10000.0f;
protected:
/** Last cached camera vertical location. The camera only adjusts its height if necessary. */
float CurrentZ = 0.0f;
/** First-time update camera setup flag */
bool bSetup = true;
};
@@ -1,350 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/CapsuleComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/InputComponent.h"
#include "InputActionValue.h"
#include "EnhancedInputComponent.h"
#include "InputAction.h"
#include "Engine/World.h"
#include "SideScrollingInteractable.h"
#include "Kismet/KismetMathLibrary.h"
#include "TimerManager.h"
ASideScrollingCharacter::ASideScrollingCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// create the camera component
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(RootComponent);
Camera->SetRelativeLocationAndRotation(FVector(0.0f, 300.0f, 0.0f), FRotator(0.0f, -90.0f, 0.0f));
// configure the collision capsule
GetCapsuleComponent()->SetCapsuleSize(35.0f, 90.0f);
// configure the Pawn properties
bUseControllerRotationYaw = false;
// configure the character movement component
GetCharacterMovement()->GravityScale = 1.75f;
GetCharacterMovement()->MaxAcceleration = 1500.0f;
GetCharacterMovement()->BrakingFrictionFactor = 1.0f;
GetCharacterMovement()->bUseSeparateBrakingFriction = true;
GetCharacterMovement()->Mass = 500.0f;
GetCharacterMovement()->SetWalkableFloorAngle(75.0f);
GetCharacterMovement()->MaxWalkSpeed = 500.0f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.0f;
GetCharacterMovement()->bIgnoreBaseRotation = true;
GetCharacterMovement()->PerchRadiusThreshold = 15.0f;
GetCharacterMovement()->LedgeCheckThreshold = 6.0f;
GetCharacterMovement()->JumpZVelocity = 750.0f;
GetCharacterMovement()->AirControl = 1.0f;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 750.0f, 0.0f);
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->SetPlaneConstraintNormal(FVector(0.0f, 1.0f, 0.0f));
GetCharacterMovement()->bConstrainToPlane = true;
// enable double jump and coyote time
JumpMaxCount = 3;
}
void ASideScrollingCharacter::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the wall jump timer
GetWorld()->GetTimerManager().ClearTimer(WallJumpTimer);
}
void ASideScrollingCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ASideScrollingCharacter::DoJumpStart);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ASideScrollingCharacter::DoJumpEnd);
// Interacting
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::DoInteract);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::Move);
// Dropping from platform
EnhancedInputComponent->BindAction(DropAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::Drop);
EnhancedInputComponent->BindAction(DropAction, ETriggerEvent::Completed, this, &ASideScrollingCharacter::DropReleased);
}
}
void ASideScrollingCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
{
Super::NotifyHit(MyComp, Other, OtherComp, bSelfMoved, HitLocation, HitNormal, NormalImpulse, Hit);
// only apply push impulse if we're falling
if (!GetCharacterMovement()->IsFalling())
{
return;
}
// ensure the colliding component is valid
if (OtherComp)
{
// ensure the component is movable and simulating physics
if (OtherComp->Mobility == EComponentMobility::Movable && OtherComp->IsSimulatingPhysics())
{
const FVector PushDir = FVector(ActionValueY > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f);
// push the component away
OtherComp->AddImpulse(PushDir * JumpPushImpulse, NAME_None, true);
}
}
}
void ASideScrollingCharacter::Landed(const FHitResult& Hit)
{
// reset the double jump
bHasDoubleJumped = false;
}
void ASideScrollingCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode /*= 0*/)
{
Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);
// are we falling?
if (GetCharacterMovement()->MovementMode == EMovementMode::MOVE_Falling)
{
// save the game time when we started falling, so we can check it later for coyote time jumps
LastFallTime = GetWorld()->GetTimeSeconds();
}
}
void ASideScrollingCharacter::Move(const FInputActionValue& Value)
{
FVector2D MoveVector = Value.Get<FVector2D>();
// route the input
DoMove(MoveVector.Y);
}
void ASideScrollingCharacter::Drop(const FInputActionValue& Value)
{
// route the input
DoDrop(Value.Get<float>());
}
void ASideScrollingCharacter::DropReleased(const FInputActionValue& Value)
{
// reset the input
DoDrop(0.0f);
}
void ASideScrollingCharacter::DoMove(float Forward)
{
// is movement temporarily disabled after wall jumping?
if (!bHasWallJumped)
{
// save the movement values
ActionValueY = Forward;
// figure out the movement direction
const FVector MoveDir = FVector(1.0f, Forward > 0.0f ? 0.1f : -0.1f, 0.0f);
// apply the movement input
AddMovementInput(MoveDir, Forward);
}
}
void ASideScrollingCharacter::DoDrop(float Value)
{
// save the movement value
DropValue = Value;
}
void ASideScrollingCharacter::DoJumpStart()
{
// handle advanced jump behaviors
MultiJump();
}
void ASideScrollingCharacter::DoJumpEnd()
{
StopJumping();
}
void ASideScrollingCharacter::DoInteract()
{
// do a sphere trace to look for interactive objects
FHitResult OutHit;
const FVector Start = GetActorLocation();
const FVector End = Start + FVector(100.0f, 0.0f, 0.0f);
FCollisionShape ColSphere;
ColSphere.SetSphere(InteractionRadius);
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (GetWorld()->SweepSingleByObjectType(OutHit, Start, End, FQuat::Identity, ObjectParams, ColSphere, QueryParams))
{
// have we hit an interactable?
if (ISideScrollingInteractable* Interactable = Cast<ISideScrollingInteractable>(OutHit.GetActor()))
{
// interact
Interactable->Interaction(this);
}
}
}
void ASideScrollingCharacter::MultiJump()
{
// does the user want to drop to a lower platform?
if (DropValue > 0.0f)
{
CheckForSoftCollision();
return;
}
// reset the drop value
DropValue = 0.0f;
// if we're grounded, disregard advanced jump logic
if (!GetCharacterMovement()->IsFalling())
{
Jump();
return;
}
// if we have a horizontal input, try for wall jump first
if (!bHasWallJumped && !FMath::IsNearlyZero(ActionValueY))
{
// trace ahead of the character for walls
FHitResult OutHit;
const FVector Start = GetActorLocation();
const FVector End = Start + (FVector(ActionValueY > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f) * WallJumpTraceDistance);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams);
if (OutHit.bBlockingHit)
{
// rotate to the bounce direction
const FRotator BounceRot = UKismetMathLibrary::MakeRotFromX(OutHit.ImpactNormal);
SetActorRotation(FRotator(0.0f, BounceRot.Yaw, 0.0f));
// calculate the impulse vector
FVector WallJumpImpulse = OutHit.ImpactNormal * WallJumpHorizontalImpulse;
WallJumpImpulse.Z = GetCharacterMovement()->JumpZVelocity * WallJumpVerticalMultiplier;
// launch the character away from the wall
LaunchCharacter(WallJumpImpulse, true, true);
// enable wall jump lockout for a bit
bHasWallJumped = true;
// schedule wall jump lockout reset
GetWorld()->GetTimerManager().SetTimer(WallJumpTimer, this, &ASideScrollingCharacter::ResetWallJump, DelayBetweenWallJumps, false);
return;
}
}
// test for double jump only if we haven't already tested for wall jump
if (!bHasWallJumped)
{
// are we still within coyote time frames?
if (GetWorld()->GetTimeSeconds() - LastFallTime < MaxCoyoteTime)
{
UE_LOG(LogTemp, Warning, TEXT("Coyote Jump"));
// use the built-in CMC functionality to do the jump
Jump();
// no coyote time jump
} else {
// The movement component handles double jump but we still need to manage the flag for animation
if (!bHasDoubleJumped)
{
// raise the double jump flag
bHasDoubleJumped = true;
// let the CMC handle jump
Jump();
}
}
}
}
void ASideScrollingCharacter::CheckForSoftCollision()
{
// reset the drop value
DropValue = 0.0f;
// trace down
FHitResult OutHit;
const FVector Start = GetActorLocation();
const FVector End = Start + (FVector::DownVector * SoftCollisionTraceDistance);
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(SoftCollisionObjectType);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
GetWorld()->LineTraceSingleByObjectType(OutHit, Start, End, ObjectParams, QueryParams);
// did we hit a soft floor?
if (OutHit.GetActor())
{
// drop through the floor
SetSoftCollision(true);
}
}
void ASideScrollingCharacter::ResetWallJump()
{
// reset the wall jump flag
bHasWallJumped = false;
}
void ASideScrollingCharacter::SetSoftCollision(bool bEnabled)
{
// enable or disable collision response to the soft collision channel
GetCapsuleComponent()->SetCollisionResponseToChannel(SoftCollisionObjectType, bEnabled ? ECR_Ignore : ECR_Block);
}
bool ASideScrollingCharacter::HasDoubleJumped() const
{
return bHasDoubleJumped;
}
bool ASideScrollingCharacter::HasWallJumped() const
{
return bHasWallJumped;
}
@@ -1,180 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SideScrollingCharacter.generated.h"
class UCameraComponent;
class UInputAction;
struct FInputActionValue;
/**
* A player-controllable character side scrolling game
*/
UCLASS(abstract)
class ASideScrollingCharacter : public ACharacter
{
GENERATED_BODY()
/** Player camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Camera", meta = (AllowPrivateAccess = "true"))
UCameraComponent* Camera;
protected:
/** Move Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* MoveAction;
/** Jump Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* JumpAction;
/** Drop from Platform Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* DropAction;
/** Interact Input Action */
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* InteractAction;
/** Impulse to manually push physics objects while we're in midair */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Jump")
float JumpPushImpulse = 600.0f;
/** Max distance that interactive objects can be triggered */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Interaction")
float InteractionRadius = 200.0f;
/** Time to disable input after a wall jump to preserve momentum */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
float DelayBetweenWallJumps = 0.3f;
/** Distance to trace ahead of the character for wall jumps */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
float WallJumpTraceDistance = 50.0f;
/** Horizontal impulse to apply to the character during wall jumps */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
float WallJumpHorizontalImpulse = 500.0f;
/** Multiplies the jump Z velocity for wall jumps. */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
float WallJumpVerticalMultiplier = 1.4f;
/** Collision object type to use for soft collision traces (dropping down floors) */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Soft Platforms")
TEnumAsByte<ECollisionChannel> SoftCollisionObjectType;
/** Distance to trace down during soft collision checks */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Soft Platforms")
float SoftCollisionTraceDistance = 1000.0f;
/** Last recorded time when this character started falling */
float LastFallTime = 0.0f;
/** Max amount of time that can pass since we started falling when we allow a regular jump */
UPROPERTY(EditAnywhere, Category="Side Scrolling|Coyote Time", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float MaxCoyoteTime = 0.16f;
/** Wall jump lockout timer */
FTimerHandle WallJumpTimer;
/** Last captured horizontal movement input value */
float ActionValueY = 0.0f;
/** Last captured platform drop axis value */
float DropValue = 0.0f;
/** If true, this character has already wall jumped */
bool bHasWallJumped = false;
/** If true, this character has already double jumped */
bool bHasDoubleJumped = false;
/** If true, this character is moving along the side scrolling axis */
bool bMovingHorizontally = false;
public:
/** Constructor */
ASideScrollingCharacter();
protected:
/** Gameplay cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
/** Initialize input action bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
/** Collision handling */
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
/** Landing handling */
virtual void Landed(const FHitResult& Hit) override;
/** Handle movement mode changes to keep track of coyote time jumps */
virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0) override;
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for drop from platform input */
void Drop(const FInputActionValue& Value);
/** Called for drop from platform input release */
void DropReleased(const FInputActionValue& Value);
public:
/** Handles move inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoMove(float Forward);
/** Handles drop inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoDrop(float Value);
/** Handles jump pressed inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpStart();
/** Handles jump pressed inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoJumpEnd();
/** Handles interact inputs from either controls or UI interfaces */
UFUNCTION(BlueprintCallable, Category="Input")
virtual void DoInteract();
protected:
/** Handles advanced jump logic */
void MultiJump();
/** Checks for soft collision with platforms */
void CheckForSoftCollision();
/** Resets wall jump lockout. Called from timer after a wall jump */
void ResetWallJump();
public:
/** Sets the soft collision response. True passes, False blocks */
void SetSoftCollision(bool bEnabled);
public:
/** Returns true if the character has just double jumped */
UFUNCTION(BlueprintPure, Category="Side Scrolling")
bool HasDoubleJumped() const;
/** Returns true if the character has just wall jumped */
UFUNCTION(BlueprintPure, Category="Side Scrolling")
bool HasWallJumped() const;
};
@@ -1,35 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingGameMode.h"
#include "Kismet/GameplayStatics.h"
#include "Blueprint/UserWidget.h"
#include "SideScrollingUI.h"
#include "SideScrollingPickup.h"
void ASideScrollingGameMode::BeginPlay()
{
Super::BeginPlay();
// create the game UI
APlayerController* OwningPlayer = UGameplayStatics::GetPlayerController(GetWorld(), 0);
UserInterface = CreateWidget<USideScrollingUI>(OwningPlayer, UserInterfaceClass);
check(UserInterface);
}
void ASideScrollingGameMode::ProcessPickup()
{
// increment the pickups counter
++PickupsCollected;
// if this is the first pickup we collect, show the UI
if (PickupsCollected == 1)
{
UserInterface->AddToViewport(0);
}
// update the pickups counter on the UI
UserInterface->UpdatePickups(PickupsCollected);
}
@@ -1,44 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SideScrollingGameMode.generated.h"
class USideScrollingUI;
/**
* Simple Side Scrolling Game Mode
* Spawns and manages the game UI
* Counts pickups collected by the player
*/
UCLASS(abstract)
class ASideScrollingGameMode : public AGameModeBase
{
GENERATED_BODY()
protected:
/** Class of UI widget to spawn when the game starts */
UPROPERTY(EditAnywhere, Category="UI")
TSubclassOf<USideScrollingUI> UserInterfaceClass;
/** User interface widget for the game */
UPROPERTY(BlueprintReadOnly, Category="UI")
TObjectPtr<USideScrollingUI> UserInterface;
/** Number of pickups collected by the player */
UPROPERTY(BlueprintReadOnly, Category="Pickups")
int32 PickupsCollected = 0;
protected:
/** Initialization */
virtual void BeginPlay() override;
public:
/** Receives an interaction event from another actor */
virtual void ProcessPickup();
};
@@ -1,98 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerStart.h"
#include "SideScrollingCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Engine/World.h"
#include "Blueprint/UserWidget.h"
#include "AgrarianGame.h"
#include "Widgets/Input/SVirtualJoystick.h"
void ASideScrollingPlayerController::BeginPlay()
{
Super::BeginPlay();
// only spawn touch controls on local player controllers
if (ShouldUseTouchControls() && IsLocalPlayerController())
{
// spawn the mobile controls widget
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget)
{
// add the controls to the player screen
MobileControlsWidget->AddToPlayerScreen(0);
} else {
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
}
}
}
void ASideScrollingPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// only add IMCs for local player controllers
if (IsLocalPlayerController())
{
// add the input mapping context
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!ShouldUseTouchControls())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
}
void ASideScrollingPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// subscribe to the pawn's OnDestroyed delegate
InPawn->OnDestroyed.AddDynamic(this, &ASideScrollingPlayerController::OnPawnDestroyed);
}
void ASideScrollingPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
{
// find the player start
TArray<AActor*> ActorList;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList);
if (ActorList.Num() > 0)
{
// spawn a character at the player start
const FTransform SpawnTransform = ActorList[0]->GetActorTransform();
if (ASideScrollingCharacter* RespawnedCharacter = GetWorld()->SpawnActor<ASideScrollingCharacter>(CharacterClass, SpawnTransform))
{
// possess the character
Possess(RespawnedCharacter);
}
}
}
bool ASideScrollingPlayerController::ShouldUseTouchControls() const
{
// are we on a mobile platform? Should we force touch?
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
}
@@ -1,67 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "EnhancedInput/Public/InputAction.h"
#include "SideScrollingPlayerController.generated.h"
class ASideScrollingCharacter;
class UInputMappingContext;
/**
* A simple Side Scrolling Player Controller
* Manages input mappings
* Respawns the player pawn at the player start if it is destroyed
*/
UCLASS(abstract, Config="Game")
class ASideScrollingPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
/** Input mapping context for this player */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Mobile controls widget to spawn */
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
/** Pointer to the mobile controls widget */
UPROPERTY()
TObjectPtr<UUserWidget> MobileControlsWidget;
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
bool bForceTouchControls = false;
/** Character class to respawn when the possessed pawn is destroyed */
UPROPERTY(EditAnywhere, Category="Respawn")
TSubclassOf<ASideScrollingCharacter> CharacterClass;
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Initialize input bindings */
virtual void SetupInputComponent() override;
/** Pawn initialization */
virtual void OnPossess(APawn* InPawn) override;
/** Called if the possessed pawn is destroyed */
UFUNCTION()
void OnPawnDestroyed(AActor* DestroyedActor);
/** Returns true if the player should use UMG touch controls */
bool ShouldUseTouchControls() const;
};
@@ -1,5 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SideScrollingUI.h"
@@ -1,23 +0,0 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SideScrollingUI.generated.h"
/**
* Simple Side Scrolling game UI
* Displays and manages a pickup counter
*/
UCLASS(abstract)
class USideScrollingUI : public UUserWidget
{
GENERATED_BODY()
public:
/** Update the widget's pickup counter */
UFUNCTION(BlueprintImplementableEvent, Category="UI")
void UpdatePickups(int32 Amount);
};