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:
co-authored by
Claude Fable 5.1
parent
0e61a77346
commit
4f2c55cd2a
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user