This commit is contained in:
John
2026-03-11 11:25:15 +05:30
parent bcd6f827d5
commit 0327abb1a0
77 changed files with 7689 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/AI/EnvQueryContext_Target.h"
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
#include "EnvironmentQuery/EnvQueryTypes.h"
#include "ShooterAIController.h"
void UEnvQueryContext_Target::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
// get the controller from the query instance
if (AShooterAIController* Controller = Cast<AShooterAIController>(QueryInstance.Owner))
{
// ensure the target is valid
if (IsValid(Controller->GetCurrentTarget()))
{
// add the controller's target actor to the context
UEnvQueryItemType_Actor::SetContextHelper(ContextData, Controller->GetCurrentTarget());
} else {
// if for any reason there's no target, default to the controller
UEnvQueryItemType_Actor::SetContextHelper(ContextData, Controller);
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryContext.h"
#include "EnvQueryContext_Target.generated.h"
/**
* Custom EnvQuery Context that returns the actor currently targeted by an NPC
*/
UCLASS()
class AUDIOVIDEORECORD_API UEnvQueryContext_Target : public UEnvQueryContext
{
GENERATED_BODY()
public:
/** Provides the context locations or actors for this EnvQuery */
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
};

View File

@@ -0,0 +1,74 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/AI/ShooterAIController.h"
#include "ShooterNPC.h"
#include "Components/StateTreeAIComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Navigation/PathFollowingComponent.h"
#include "AI/Navigation/PathFollowingAgentInterface.h"
AShooterAIController::AShooterAIController()
{
// create the StateTree component
StateTreeAI = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
// create the AI perception component. It will be configured in BP
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
// subscribe to the AI perception delegates
AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AShooterAIController::OnPerceptionUpdated);
AIPerception->OnTargetPerceptionForgotten.AddDynamic(this, &AShooterAIController::OnPerceptionForgotten);
}
void AShooterAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// ensure we're possessing an NPC
if (AShooterNPC* NPC = Cast<AShooterNPC>(InPawn))
{
// add the team tag to the pawn
NPC->Tags.Add(TeamTag);
// subscribe to the pawn's OnDeath delegate
NPC->OnPawnDeath.AddDynamic(this, &AShooterAIController::OnPawnDeath);
}
}
void AShooterAIController::OnPawnDeath()
{
// stop movement
GetPathFollowingComponent()->AbortMove(*this, FPathFollowingResultFlags::UserAbort);
// stop StateTree logic
StateTreeAI->StopLogic(FString(""));
// unpossess the pawn
UnPossess();
// destroy this controller
Destroy();
}
void AShooterAIController::SetCurrentTarget(AActor* Target)
{
TargetEnemy = Target;
}
void AShooterAIController::ClearCurrentTarget()
{
TargetEnemy = nullptr;
}
void AShooterAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
// pass the data to the StateTree delegate hook
OnShooterPerceptionUpdated.ExecuteIfBound(Actor, Stimulus);
}
void AShooterAIController::OnPerceptionForgotten(AActor* Actor)
{
// pass the data to the StateTree delegate hook
OnShooterPerceptionForgotten.ExecuteIfBound(Actor);
}

View File

@@ -0,0 +1,85 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "ShooterAIController.generated.h"
class UStateTreeAIComponent;
class UAIPerceptionComponent;
struct FAIStimulus;
DECLARE_DELEGATE_TwoParams(FShooterPerceptionUpdatedDelegate, AActor*, const FAIStimulus&);
DECLARE_DELEGATE_OneParam(FShooterPerceptionForgottenDelegate, AActor*);
/**
* Simple AI Controller for a first person shooter enemy
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterAIController : public AAIController
{
GENERATED_BODY()
/** Runs the behavior StateTree for this NPC */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UStateTreeAIComponent* StateTreeAI;
/** Detects other actors through sight, hearing and other senses */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UAIPerceptionComponent* AIPerception;
protected:
/** Team tag for pawn friend or foe identification */
UPROPERTY(EditAnywhere, Category="Shooter")
FName TeamTag = FName("Enemy");
/** Enemy currently being targeted */
TObjectPtr<AActor> TargetEnemy;
public:
/** Called when an AI perception has been updated. StateTree task delegate hook */
FShooterPerceptionUpdatedDelegate OnShooterPerceptionUpdated;
/** Called when an AI perception has been forgotten. StateTree task delegate hook */
FShooterPerceptionForgottenDelegate OnShooterPerceptionForgotten;
public:
/** Constructor */
AShooterAIController();
protected:
/** Pawn initialization */
virtual void OnPossess(APawn* InPawn) override;
protected:
/** Called when the possessed pawn dies */
UFUNCTION()
void OnPawnDeath();
public:
/** Sets the targeted enemy */
void SetCurrentTarget(AActor* Target);
/** Clears the targeted enemy */
void ClearCurrentTarget();
/** Returns the targeted enemy */
AActor* GetCurrentTarget() const { return TargetEnemy; };
protected:
/** Called when the AI perception component updates a perception on a given actor */
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
/** Called when the AI perception component forgets a given actor */
UFUNCTION()
void OnPerceptionForgotten(AActor* Actor);
};

View File

@@ -0,0 +1,208 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/AI/ShooterNPC.h"
#include "ShooterWeapon.h"
#include "Components/SkeletalMeshComponent.h"
#include "Camera/CameraComponent.h"
#include "Kismet/KismetMathLibrary.h"
#include "Engine/World.h"
#include "ShooterGameMode.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "TimerManager.h"
void AShooterNPC::BeginPlay()
{
Super::BeginPlay();
// spawn the weapon
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = this;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Weapon = GetWorld()->SpawnActor<AShooterWeapon>(WeaponClass, GetActorTransform(), SpawnParams);
}
void AShooterNPC::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the death timer
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
}
float AShooterNPC::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// ignore if already dead
if (bIsDead)
{
return 0.0f;
}
// Reduce HP
CurrentHP -= Damage;
// Have we depleted HP?
if (CurrentHP <= 0.0f)
{
Die();
}
return Damage;
}
void AShooterNPC::AttachWeaponMeshes(AShooterWeapon* WeaponToAttach)
{
const FAttachmentTransformRules AttachmentRule(EAttachmentRule::SnapToTarget, false);
// attach the weapon actor
WeaponToAttach->AttachToActor(this, AttachmentRule);
// attach the weapon meshes
WeaponToAttach->GetFirstPersonMesh()->AttachToComponent(GetFirstPersonMesh(), AttachmentRule, FirstPersonWeaponSocket);
WeaponToAttach->GetThirdPersonMesh()->AttachToComponent(GetMesh(), AttachmentRule, FirstPersonWeaponSocket);
}
void AShooterNPC::PlayFiringMontage(UAnimMontage* Montage)
{
// unused
}
void AShooterNPC::AddWeaponRecoil(float Recoil)
{
// unused
}
void AShooterNPC::UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize)
{
// unused
}
FVector AShooterNPC::GetWeaponTargetLocation()
{
// start aiming from the camera location
const FVector AimSource = GetFirstPersonCameraComponent()->GetComponentLocation();
FVector AimDir, AimTarget = FVector::ZeroVector;
// do we have an aim target?
if (CurrentAimTarget)
{
// target the actor location
AimTarget = CurrentAimTarget->GetActorLocation();
// apply a vertical offset to target head/feet
AimTarget.Z += FMath::RandRange(MinAimOffsetZ, MaxAimOffsetZ);
// get the aim direction and apply randomness in a cone
AimDir = (AimTarget - AimSource).GetSafeNormal();
AimDir = UKismetMathLibrary::RandomUnitVectorInConeInDegrees(AimDir, AimVarianceHalfAngle);
} else {
// no aim target, so just use the camera facing
AimDir = UKismetMathLibrary::RandomUnitVectorInConeInDegrees(GetFirstPersonCameraComponent()->GetForwardVector(), AimVarianceHalfAngle);
}
// calculate the unobstructed aim target location
AimTarget = AimSource + (AimDir * AimRange);
// run a visibility trace to see if there's obstructions
FHitResult OutHit;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
GetWorld()->LineTraceSingleByChannel(OutHit, AimSource, AimTarget, ECC_Visibility, QueryParams);
// return either the impact point or the trace end
return OutHit.bBlockingHit ? OutHit.ImpactPoint : OutHit.TraceEnd;
}
void AShooterNPC::AddWeaponClass(const TSubclassOf<AShooterWeapon>& InWeaponClass)
{
// unused
}
void AShooterNPC::OnWeaponActivated(AShooterWeapon* InWeapon)
{
// unused
}
void AShooterNPC::OnWeaponDeactivated(AShooterWeapon* InWeapon)
{
// unused
}
void AShooterNPC::OnSemiWeaponRefire()
{
// are we still shooting?
if (bIsShooting)
{
// fire the weapon
Weapon->StartFiring();
}
}
void AShooterNPC::Die()
{
// ignore if already dead
if (bIsDead)
{
return;
}
// raise the dead flag
bIsDead = true;
// increment the team score
if (AShooterGameMode* GM = Cast<AShooterGameMode>(GetWorld()->GetAuthGameMode()))
{
GM->IncrementTeamScore(TeamByte);
}
// disable capsule collision
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// stop movement
GetCharacterMovement()->StopMovementImmediately();
GetCharacterMovement()->StopActiveMovement();
// enable ragdoll physics on the third person mesh
GetMesh()->SetCollisionProfileName(RagdollCollisionProfile);
GetMesh()->SetSimulatePhysics(true);
GetMesh()->SetPhysicsBlendWeight(1.0f);
// schedule actor destruction
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &AShooterNPC::DeferredDestruction, DeferredDestructionTime, false);
}
void AShooterNPC::DeferredDestruction()
{
Destroy();
}
void AShooterNPC::StartShooting(AActor* ActorToShoot)
{
// save the aim target
CurrentAimTarget = ActorToShoot;
// raise the flag
bIsShooting = true;
// signal the weapon
Weapon->StartFiring();
}
void AShooterNPC::StopShooting()
{
// lower the flag
bIsShooting = false;
// signal the weapon
Weapon->StopFiring();
}

View File

@@ -0,0 +1,153 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AudioVideoRecordCharacter.h"
#include "ShooterWeaponHolder.h"
#include "ShooterNPC.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPawnDeathDelegate);
class AShooterWeapon;
/**
* A simple AI-controlled shooter game NPC
* Executes its behavior through a StateTree managed by its AI Controller
* Holds and manages a weapon
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterNPC : public AAudioVideoRecordCharacter, public IShooterWeaponHolder
{
GENERATED_BODY()
public:
/** Current HP for this character. It dies if it reaches zero through damage */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Damage")
float CurrentHP = 100.0f;
protected:
/** Name of the collision profile to use during ragdoll death */
UPROPERTY(EditAnywhere, Category="Damage")
FName RagdollCollisionProfile = FName("Ragdoll");
/** Time to wait after death before destroying this actor */
UPROPERTY(EditAnywhere, Category="Damage")
float DeferredDestructionTime = 5.0f;
/** Team byte for this character */
UPROPERTY(EditAnywhere, Category="Team")
uint8 TeamByte = 1;
/** Pointer to the equipped weapon */
TObjectPtr<AShooterWeapon> Weapon;
/** Type of weapon to spawn for this character */
UPROPERTY(EditAnywhere, Category="Weapon")
TSubclassOf<AShooterWeapon> WeaponClass;
/** Name of the first person mesh weapon socket */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category ="Weapons")
FName FirstPersonWeaponSocket = FName("HandGrip_R");
/** Name of the third person mesh weapon socket */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category ="Weapons")
FName ThirdPersonWeaponSocket = FName("HandGrip_R");
/** Max range for aiming calculations */
UPROPERTY(EditAnywhere, Category="Aim")
float AimRange = 10000.0f;
/** Cone variance to apply while aiming */
UPROPERTY(EditAnywhere, Category="Aim")
float AimVarianceHalfAngle = 10.0f;
/** Minimum vertical offset from the target center to apply when aiming */
UPROPERTY(EditAnywhere, Category="Aim")
float MinAimOffsetZ = -35.0f;
/** Maximum vertical offset from the target center to apply when aiming */
UPROPERTY(EditAnywhere, Category="Aim")
float MaxAimOffsetZ = -60.0f;
/** Actor currently being targeted */
TObjectPtr<AActor> CurrentAimTarget;
/** If true, this character is currently shooting its weapon */
bool bIsShooting = false;
/** If true, this character has already died */
bool bIsDead = false;
/** Deferred destruction on death timer */
FTimerHandle DeathTimer;
public:
/** Delegate called when this NPC dies */
FPawnDeathDelegate OnPawnDeath;
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Gameplay cleanup */
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public:
/** Handle incoming damage */
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
public:
//~Begin IShooterWeaponHolder interface
/** Attaches a weapon's meshes to the owner */
virtual void AttachWeaponMeshes(AShooterWeapon* Weapon) override;
/** Plays the firing montage for the weapon */
virtual void PlayFiringMontage(UAnimMontage* Montage) override;
/** Applies weapon recoil to the owner */
virtual void AddWeaponRecoil(float Recoil) override;
/** Updates the weapon's HUD with the current ammo count */
virtual void UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) override;
/** Calculates and returns the aim location for the weapon */
virtual FVector GetWeaponTargetLocation() override;
/** Gives a weapon of this class to the owner */
virtual void AddWeaponClass(const TSubclassOf<AShooterWeapon>& WeaponClass) override;
/** Activates the passed weapon */
virtual void OnWeaponActivated(AShooterWeapon* Weapon) override;
/** Deactivates the passed weapon */
virtual void OnWeaponDeactivated(AShooterWeapon* Weapon) override;
/** Notifies the owner that the weapon cooldown has expired and it's ready to shoot again */
virtual void OnSemiWeaponRefire() override;
//~End IShooterWeaponHolder interface
protected:
/** Called when HP is depleted and the character should die */
void Die();
/** Called after death to destroy the actor */
void DeferredDestruction();
public:
/** Signals this character to start shooting at the passed actor */
void StartShooting(AActor* ActorToShoot);
/** Signals this character to stop shooting */
void StopShooting();
};

View File

@@ -0,0 +1,366 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/AI/ShooterStateTreeUtility.h"
#include "StateTreeExecutionContext.h"
#include "ShooterNPC.h"
#include "Camera/CameraComponent.h"
#include "AIController.h"
#include "Perception/AIPerceptionComponent.h"
#include "ShooterAIController.h"
#include "StateTreeAsyncExecutionContext.h"
bool FStateTreeLineOfSightToTargetCondition::TestCondition(FStateTreeExecutionContext& Context) const
{
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// ensure the target is valid
if (!IsValid(InstanceData.Target))
{
return !InstanceData.bMustHaveLineOfSight;
}
// check if the character is facing towards the target
const FVector TargetDir = (InstanceData.Target->GetActorLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal();
const float FacingDot = FVector::DotProduct(TargetDir, InstanceData.Character->GetActorForwardVector());
const float MaxDot = FMath::Cos(FMath::DegreesToRadians(InstanceData.LineOfSightConeAngle));
// is the facing outside of our cone half angle?
if (FacingDot <= MaxDot)
{
return !InstanceData.bMustHaveLineOfSight;
}
// get the target's bounding box
FVector CenterOfMass, Extent;
InstanceData.Target->GetActorBounds(true, CenterOfMass, Extent, false);
// divide the vertical extent by the number of line of sight checks we'll do
const float ExtentZOffset = Extent.Z * 2.0f / InstanceData.NumberOfVerticalLineOfSightChecks;
// get the character's camera location as the source for the line checks
const FVector Start = InstanceData.Character->GetFirstPersonCameraComponent()->GetComponentLocation();
// ignore the character and target. We want to ensure there's an unobstructed trace not counting them
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(InstanceData.Character);
QueryParams.AddIgnoredActor(InstanceData.Target);
FHitResult OutHit;
// run a number of vertically offset line traces to the target location
for (int32 i = 0; i < InstanceData.NumberOfVerticalLineOfSightChecks - 1; ++i)
{
// calculate the endpoint for the trace
const FVector End = CenterOfMass + FVector(0.0f, 0.0f, Extent.Z - ExtentZOffset * i);
InstanceData.Character->GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams);
// is the trace unobstructed?
if (!OutHit.bBlockingHit)
{
// we only need one unobstructed trace, so terminate early
return InstanceData.bMustHaveLineOfSight;
}
}
// no line of sight found
return !InstanceData.bMustHaveLineOfSight;
}
#if WITH_EDITOR
FText FStateTreeLineOfSightToTargetCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Has Line of Sight</b>");
}
#endif
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
}
#if WITH_EDITOR
FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Actor</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// set the AI Controller's focus
InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// clear the AI Controller's focus
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
}
#if WITH_EDITOR
FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Face Towards Location</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeSetRandomFloatTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// calculate the output value
InstanceData.OutValue = FMath::RandRange(InstanceData.MinValue, InstanceData.MaxValue);
}
return EStateTreeRunStatus::Running;
}
#if WITH_EDITOR
FText FStateTreeSetRandomFloatTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Set Random Float</b>");
}
#endif // WITH_EDITOR
////////////////////////////////////////////////////////////////////
EStateTreeRunStatus FStateTreeShootAtTargetTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// tell the character to shoot the target
InstanceData.Character->StartShooting(InstanceData.Target);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeShootAtTargetTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// tell the character to stop shooting
InstanceData.Character->StopShooting();
}
}
#if WITH_EDITOR
FText FStateTreeShootAtTargetTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Shoot at Target</b>");
}
#endif // WITH_EDITOR
EStateTreeRunStatus FStateTreeSenseEnemiesTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned from another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// bind the perception updated delegate on the controller
InstanceData.Controller->OnShooterPerceptionUpdated.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor, const FAIStimulus& Stimulus)
{
// get the instance data inside the lambda
const FStateTreeStrongExecutionContext StrongContext = WeakContext.MakeStrongExecutionContext();
if (FInstanceDataType* LambdaInstanceData = StrongContext.GetInstanceDataPtr<FInstanceDataType>())
{
if (SensedActor->ActorHasTag(LambdaInstanceData->SenseTag))
{
bool bDirectLOS = false;
// calculate the direction of the stimulus
const FVector StimulusDir = (Stimulus.StimulusLocation - LambdaInstanceData->Character->GetActorLocation()).GetSafeNormal();
// infer the angle from the dot product between the character facing and the stimulus direction
const float DirDot = FVector::DotProduct(StimulusDir, LambdaInstanceData->Character->GetActorForwardVector());
const float MaxDot = FMath::Cos(FMath::DegreesToRadians(LambdaInstanceData->DirectLineOfSightCone));
// is the direction within our perception cone?
if (DirDot >= MaxDot)
{
// run a line trace between the character and the sensed actor
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(LambdaInstanceData->Character);
QueryParams.AddIgnoredActor(SensedActor);
FHitResult OutHit;
// we have direct line of sight if this trace is unobstructed
bDirectLOS = !LambdaInstanceData->Character->GetWorld()->LineTraceSingleByChannel(OutHit, LambdaInstanceData->Character->GetActorLocation(), SensedActor->GetActorLocation(), ECC_Visibility, QueryParams);
}
// check if we have a direct line of sight to the stimulus
if (bDirectLOS)
{
// set the controller's target
LambdaInstanceData->Controller->SetCurrentTarget(SensedActor);
// set the task output
LambdaInstanceData->TargetActor = SensedActor;
// set the flags
LambdaInstanceData->bHasTarget = true;
LambdaInstanceData->bHasInvestigateLocation = false;
// no direct line of sight to target
} else {
// if we already have a target, ignore the partial sense and keep on them
if (!IsValid(LambdaInstanceData->TargetActor))
{
// is this stimulus stronger than the last one we had?
if (Stimulus.Strength > LambdaInstanceData->LastStimulusStrength)
{
// update the stimulus strength
LambdaInstanceData->LastStimulusStrength = Stimulus.Strength;
// set the investigate location
LambdaInstanceData->InvestigateLocation = Stimulus.StimulusLocation;
// set the investigate flag
LambdaInstanceData->bHasInvestigateLocation = true;
}
}
}
}
}
}
);
// bind the perception forgotten delegate on the controller
InstanceData.Controller->OnShooterPerceptionForgotten.BindLambda(
[WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor)
{
// get the instance data inside the lambda
FInstanceDataType* LambdaInstanceData = WeakContext.MakeStrongExecutionContext().GetInstanceDataPtr<FInstanceDataType>();
if (!LambdaInstanceData)
{
return;
}
bool bForget = false;
// are we forgetting the current target?
if (SensedActor == LambdaInstanceData->TargetActor)
{
bForget = true;
} else {
// are we forgetting about a partial sense?
if (!IsValid(LambdaInstanceData->TargetActor))
{
bForget = true;
}
}
if (bForget)
{
// clear the target
LambdaInstanceData->TargetActor = nullptr;
// clear the flags
LambdaInstanceData->bHasInvestigateLocation = false;
LambdaInstanceData->bHasTarget = false;
// reset the stimulus strength
LambdaInstanceData->LastStimulusStrength = 0.0f;
// clear the target on the controller
LambdaInstanceData->Controller->ClearCurrentTarget();
LambdaInstanceData->Controller->ClearFocus(EAIFocusPriority::Gameplay);
}
}
);
}
return EStateTreeRunStatus::Running;
}
void FStateTreeSenseEnemiesTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
{
// have we transitioned to another state?
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
{
// get the instance data
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
// unbind the perception delegates
InstanceData.Controller->OnShooterPerceptionUpdated.Unbind();
InstanceData.Controller->OnShooterPerceptionForgotten.Unbind();
}
}
#if WITH_EDITOR
FText FStateTreeSenseEnemiesTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
{
return FText::FromString("<b>Sense Enemies</b>");
}
#endif // WITH_EDITOR

View File

@@ -0,0 +1,309 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "StateTreeTaskBase.h"
#include "StateTreeConditionBase.h"
#include "ShooterStateTreeUtility.generated.h"
class AShooterNPC;
class AAIController;
class AShooterAIController;
/**
* Instance data struct for the FStateTreeLineOfSightToTargetCondition condition
*/
USTRUCT()
struct FStateTreeLineOfSightToTargetConditionInstanceData
{
GENERATED_BODY()
/** Targeting character */
UPROPERTY(EditAnywhere, Category = "Context")
AShooterNPC* Character;
/** Target to check line of sight for */
UPROPERTY(EditAnywhere, Category = "Condition")
AActor* Target;
/** Max allowed line of sight cone angle, in degrees */
UPROPERTY(EditAnywhere, Category = "Condition")
float LineOfSightConeAngle = 35.0f;
/** Number of vertical line of sight checks to run to try and get around low obstacles */
UPROPERTY(EditAnywhere, Category = "Condition")
int32 NumberOfVerticalLineOfSightChecks = 5;
/** If true, the condition passes if the character has line of sight */
UPROPERTY(EditAnywhere, Category = "Condition")
bool bMustHaveLineOfSight = true;
};
STATETREE_POD_INSTANCEDATA(FStateTreeLineOfSightToTargetConditionInstanceData);
/**
* StateTree condition to check if the character is grounded
*/
USTRUCT(DisplayName = "Has Line of Sight to Target", Category="Shooter")
struct FStateTreeLineOfSightToTargetCondition : public FStateTreeConditionCommonBase
{
GENERATED_BODY()
/** Set the instance data type */
using FInstanceDataType = FStateTreeLineOfSightToTargetConditionInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Default constructor */
FStateTreeLineOfSightToTargetCondition() = 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 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="Shooter"))
struct FStateTreeFaceActorTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeFaceActorInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Face Towards Location StateTree task
*/
USTRUCT()
struct FStateTreeFaceLocationInstanceData
{
GENERATED_BODY()
/** AI Controller that will determine the focused location */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AAIController> Controller;
/** Location that will be faced towards */
UPROPERTY(EditAnywhere, Category = Parameter)
FVector FaceLocation = FVector::ZeroVector;
};
/**
* StateTree task to face an AI-Controlled Pawn towards a world location
*/
USTRUCT(meta=(DisplayName="Face Towards Location", Category="Shooter"))
struct FStateTreeFaceLocationTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeFaceLocationInstanceData;
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
/** Runs when the owning state is entered */
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
/** Runs when the owning state is ended */
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
#if WITH_EDITOR
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
#endif // WITH_EDITOR
};
////////////////////////////////////////////////////////////////////
/**
* Instance data struct for the Set Random Float StateTree task
*/
USTRUCT()
struct FStateTreeSetRandomFloatData
{
GENERATED_BODY()
/** Minimum random value */
UPROPERTY(EditAnywhere, Category = Parameter)
float MinValue = 0.0f;
/** Maximum random value */
UPROPERTY(EditAnywhere, Category = Parameter)
float MaxValue = 0.0f;
/** Output calculated value */
UPROPERTY(EditAnywhere, Category = Output)
float OutValue = 0.0f;
};
/**
* StateTree task to calculate a random float value within the specified range
*/
USTRUCT(meta=(DisplayName="Set Random Float", Category="Shooter"))
struct FStateTreeSetRandomFloatTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeSetRandomFloatData;
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 Shoot At Target StateTree task
*/
USTRUCT()
struct FStateTreeShootAtTargetInstanceData
{
GENERATED_BODY()
/** NPC that will do the shooting */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AShooterNPC> Character;
/** Target to shoot at */
UPROPERTY(EditAnywhere, Category = Input)
TObjectPtr<AActor> Target;
};
/**
* StateTree task to have an NPC shoot at an actor
*/
USTRUCT(meta=(DisplayName="Shoot at Target", Category="Shooter"))
struct FStateTreeShootAtTargetTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeShootAtTargetInstanceData;
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 Sense Enemies StateTree task
*/
USTRUCT()
struct FStateTreeSenseEnemiesInstanceData
{
GENERATED_BODY()
/** Sensing AI Controller */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AShooterAIController> Controller;
/** Sensing NPC */
UPROPERTY(EditAnywhere, Category = Context)
TObjectPtr<AShooterNPC> Character;
/** Sensed actor to target */
UPROPERTY(EditAnywhere, Category = Output)
TObjectPtr<AActor> TargetActor;
/** Sensed location to investigate */
UPROPERTY(EditAnywhere, Category = Output)
FVector InvestigateLocation = FVector::ZeroVector;
/** True if a target was successfully sensed */
UPROPERTY(EditAnywhere, Category = Output)
bool bHasTarget = false;
/** True if an investigate location was successfully sensed */
UPROPERTY(EditAnywhere, Category = Output)
bool bHasInvestigateLocation = false;
/** Tag required on sensed actors */
UPROPERTY(EditAnywhere, Category = Parameter)
FName SenseTag = FName("Player");
/** Line of sight cone half angle to consider a full sense */
UPROPERTY(EditAnywhere, Category = Parameter)
float DirectLineOfSightCone = 85.0f;
/** Strength of the last processed stimulus */
UPROPERTY(EditAnywhere)
float LastStimulusStrength = 0.0f;
};
/**
* StateTree task to have an NPC process AI Perceptions and sense nearby enemies
*/
USTRUCT(meta=(DisplayName="Sense Enemies", Category="Shooter"))
struct FStateTreeSenseEnemiesTask : public FStateTreeTaskCommonBase
{
GENERATED_BODY()
/* Ensure we're using the correct instance data struct */
using FInstanceDataType = FStateTreeSenseEnemiesInstanceData;
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
};
////////////////////////////////////////////////////////////////////