Step 1: the Salty project, two modules, tests and the gym

Salty.uproject (UE 5.8) from the Third Person template with class redirects, the SaltyCore and Salty modules,
the three build targets, USaltyAssetManager calling InitGlobalData, Config/Tags with the 23 root namespaces,
Scripts/run-tests.sh, build.sh and Authoring/create_gym.py, L_Gym, Git LFS attributes and the placeholder test.
Template variants kept as reference (D-41). The four Fab packs stay out of the repository for now (~10 GB).
The editor serves the engine MCP plugin on 127.0.0.1:8000 through DefaultEditorPerProjectUserSettings.ini.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rainer Leit
2026-09-16 20:10:43 +03:00
co-authored by Claude Fable 5.1
parent 0e61a77346
commit 4f2c55cd2a
1434 changed files with 11358 additions and 27 deletions
@@ -0,0 +1,28 @@
// 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 = false;
StateTreeAI->SetStartLogicAutomatically(false);
// ensure we're attached to the possessed character.
// this is necessary for EnvQueries to work correctly
bAttachToPawn = true;
}
void ACombatAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// start AI logic
StateTreeAI->StartLogic();
}
@@ -0,0 +1,32 @@
// 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();
protected:
/** Pawn Initialization */
virtual void OnPossess(APawn* InPawn) override;
};
@@ -0,0 +1,343 @@
// 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);
}
@@ -0,0 +1,232 @@
// 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;
};
@@ -0,0 +1,126 @@
// 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
}
@@ -0,0 +1,109 @@
// 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
};
@@ -0,0 +1,316 @@
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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
{
// 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::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// reset the selected target
ACharacter* SelectedTarget = nullptr;
// iterate through each local player
const int32 NumPlayers = UGameplayStatics::GetNumLocalPlayerControllers(InstanceData.Character);
for (int32 i = 0; i < NumPlayers; ++i)
{
if (ACharacter* Current = Cast<ACharacter>(UGameplayStatics::GetPlayerPawn(InstanceData.Character, i)))
{
// compute the distance to the target
const float TargetDist = (Current->GetActorLocation() - InstanceData.Character->GetActorLocation()).Size();
// is this target within range?
if (TargetDist < InstanceData.MaxRange)
{
// have we selected a valid target already?
if (SelectedTarget)
{
// randomly switch to the new target
if (FMath::RandBool())
{
SelectedTarget = Current;
}
}
else
{
// no valid target yet, so choose this one
SelectedTarget = Current;
}
}
}
}
// set the new target
InstanceData.TargetPlayerCharacter = SelectedTarget;
// if the target is not valid, fail the task
if (!SelectedTarget)
{
return EStateTreeRunStatus::Failed;
}
// set the target location and distance
InstanceData.TargetPlayerLocation = SelectedTarget->GetActorLocation();
InstanceData.DistanceToTarget = (SelectedTarget->GetActorLocation() - InstanceData.Character->GetActorLocation()).Size();
// succeed
return EStateTreeRunStatus::Succeeded;
}
#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
@@ -0,0 +1,437 @@
// 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")
TObjectPtr<ACharacter> Character;
/** If true, the condition passes if the character is not grounded instead */
UPROPERTY(EditAnywhere, Category = "Condition")
bool bMustBeOnAir = false;
};
/**
* 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")
TObjectPtr<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 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()
/** Constructor */
FStateTreeComboAttackTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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()
/** Constructor */
FStateTreeChargedAttackTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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()
/** Constructor */
FStateTreeWaitForLandingTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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()
/** Constructor */
FStateTreeFaceActorTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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()
/** Constructor */
FStateTreeFaceLocationTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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()
/** Constructor */
FStateTreeSetCharacterSpeedTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* 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, Category="Output")
TObjectPtr<ACharacter> TargetPlayerCharacter;
/** Last known location for the target */
UPROPERTY(VisibleAnywhere, Category="Output")
FVector TargetPlayerLocation = FVector::ZeroVector;
/** Distance to the target */
UPROPERTY(VisibleAnywhere, Category="Output")
float DistanceToTarget = 0.0f;
/** Maximum allowed targeting range */
UPROPERTY(VisibleAnywhere, Category="Parameter", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm"))
float MaxRange = 2500.0f;
};
/**
* StateTree task to get information about the player character
*/
USTRUCT(meta=(DisplayName="GetPlayerInfo", Category="Combat"))
struct FStateTreeGetPlayerInfoTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/** Constructor */
FStateTreeGetPlayerInfoTask()
{
// disable tick
bShouldCallTick = false;
// skip state change events if this is sustained
bShouldStateChangeOnReselect = false;
}
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeGetPlayerInfoInstanceData;
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
};
@@ -0,0 +1,17 @@
// 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());
}
}
@@ -0,0 +1,23 @@
// 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 SALTY_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;
};
@@ -0,0 +1,18 @@
// 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);
}
@@ -0,0 +1,22 @@
// 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;
};