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
};
////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,283 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterCharacter.h"
#include "ShooterWeapon.h"
#include "EnhancedInputComponent.h"
#include "Components/InputComponent.h"
#include "Components/PawnNoiseEmitterComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Engine/World.h"
#include "Camera/CameraComponent.h"
#include "TimerManager.h"
#include "ShooterGameMode.h"
AShooterCharacter::AShooterCharacter()
{
// create the noise emitter component
PawnNoiseEmitter = CreateDefaultSubobject<UPawnNoiseEmitterComponent>(TEXT("Pawn Noise Emitter"));
// configure movement
GetCharacterMovement()->RotationRate = FRotator(0.0f, 600.0f, 0.0f);
}
void AShooterCharacter::BeginPlay()
{
Super::BeginPlay();
// reset HP to max
CurrentHP = MaxHP;
// update the HUD
OnDamaged.Broadcast(1.0f);
}
void AShooterCharacter::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the respawn timer
GetWorld()->GetTimerManager().ClearTimer(RespawnTimer);
}
void AShooterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// base class handles move, aim and jump inputs
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
// Firing
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &AShooterCharacter::DoStartFiring);
EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Completed, this, &AShooterCharacter::DoStopFiring);
// Switch weapon
EnhancedInputComponent->BindAction(SwitchWeaponAction, ETriggerEvent::Triggered, this, &AShooterCharacter::DoSwitchWeapon);
}
}
float AShooterCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// ignore if already dead
if (CurrentHP <= 0.0f)
{
return 0.0f;
}
// Reduce HP
CurrentHP -= Damage;
// Have we depleted HP?
if (CurrentHP <= 0.0f)
{
Die();
}
// update the HUD
OnDamaged.Broadcast(FMath::Max(0.0f, CurrentHP / MaxHP));
return Damage;
}
void AShooterCharacter::DoStartFiring()
{
// fire the current weapon
if (CurrentWeapon)
{
CurrentWeapon->StartFiring();
}
}
void AShooterCharacter::DoStopFiring()
{
// stop firing the current weapon
if (CurrentWeapon)
{
CurrentWeapon->StopFiring();
}
}
void AShooterCharacter::DoSwitchWeapon()
{
// ensure we have at least two weapons two switch between
if (OwnedWeapons.Num() > 1)
{
// deactivate the old weapon
CurrentWeapon->DeactivateWeapon();
// find the index of the current weapon in the owned list
int32 WeaponIndex = OwnedWeapons.Find(CurrentWeapon);
// is this the last weapon?
if (WeaponIndex == OwnedWeapons.Num() - 1)
{
// loop back to the beginning of the array
WeaponIndex = 0;
}
else {
// select the next weapon index
++WeaponIndex;
}
// set the new weapon as current
CurrentWeapon = OwnedWeapons[WeaponIndex];
// activate the new weapon
CurrentWeapon->ActivateWeapon();
}
}
void AShooterCharacter::AttachWeaponMeshes(AShooterWeapon* Weapon)
{
const FAttachmentTransformRules AttachmentRule(EAttachmentRule::SnapToTarget, false);
// attach the weapon actor
Weapon->AttachToActor(this, AttachmentRule);
// attach the weapon meshes
Weapon->GetFirstPersonMesh()->AttachToComponent(GetFirstPersonMesh(), AttachmentRule, FirstPersonWeaponSocket);
Weapon->GetThirdPersonMesh()->AttachToComponent(GetMesh(), AttachmentRule, FirstPersonWeaponSocket);
}
void AShooterCharacter::PlayFiringMontage(UAnimMontage* Montage)
{
}
void AShooterCharacter::AddWeaponRecoil(float Recoil)
{
// apply the recoil as pitch input
AddControllerPitchInput(Recoil);
}
void AShooterCharacter::UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize)
{
OnBulletCountUpdated.Broadcast(MagazineSize, CurrentAmmo);
}
FVector AShooterCharacter::GetWeaponTargetLocation()
{
// trace ahead from the camera viewpoint
FHitResult OutHit;
const FVector Start = GetFirstPersonCameraComponent()->GetComponentLocation();
const FVector End = Start + (GetFirstPersonCameraComponent()->GetForwardVector() * MaxAimDistance);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams);
// return either the impact point or the trace end
return OutHit.bBlockingHit ? OutHit.ImpactPoint : OutHit.TraceEnd;
}
void AShooterCharacter::AddWeaponClass(const TSubclassOf<AShooterWeapon>& WeaponClass)
{
// do we already own this weapon?
AShooterWeapon* OwnedWeapon = FindWeaponOfType(WeaponClass);
if (!OwnedWeapon)
{
// spawn the new weapon
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = this;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.TransformScaleMethod = ESpawnActorScaleMethod::MultiplyWithRoot;
AShooterWeapon* AddedWeapon = GetWorld()->SpawnActor<AShooterWeapon>(WeaponClass, GetActorTransform(), SpawnParams);
if (AddedWeapon)
{
// add the weapon to the owned list
OwnedWeapons.Add(AddedWeapon);
// if we have an existing weapon, deactivate it
if (CurrentWeapon)
{
CurrentWeapon->DeactivateWeapon();
}
// switch to the new weapon
CurrentWeapon = AddedWeapon;
CurrentWeapon->ActivateWeapon();
}
}
}
void AShooterCharacter::OnWeaponActivated(AShooterWeapon* Weapon)
{
// update the bullet counter
OnBulletCountUpdated.Broadcast(Weapon->GetMagazineSize(), Weapon->GetBulletCount());
// set the character mesh AnimInstances
GetFirstPersonMesh()->SetAnimInstanceClass(Weapon->GetFirstPersonAnimInstanceClass());
GetMesh()->SetAnimInstanceClass(Weapon->GetThirdPersonAnimInstanceClass());
}
void AShooterCharacter::OnWeaponDeactivated(AShooterWeapon* Weapon)
{
// unused
}
void AShooterCharacter::OnSemiWeaponRefire()
{
// unused
}
AShooterWeapon* AShooterCharacter::FindWeaponOfType(TSubclassOf<AShooterWeapon> WeaponClass) const
{
// check each owned weapon
for (AShooterWeapon* Weapon : OwnedWeapons)
{
if (Weapon->IsA(WeaponClass))
{
return Weapon;
}
}
// weapon not found
return nullptr;
}
void AShooterCharacter::Die()
{
// deactivate the weapon
if (IsValid(CurrentWeapon))
{
CurrentWeapon->DeactivateWeapon();
}
// increment the team score
if (AShooterGameMode* GM = Cast<AShooterGameMode>(GetWorld()->GetAuthGameMode()))
{
GM->IncrementTeamScore(TeamByte);
}
// stop character movement
GetCharacterMovement()->StopMovementImmediately();
// disable controls
DisableInput(nullptr);
// reset the bullet counter UI
OnBulletCountUpdated.Broadcast(0, 0);
// call the BP handler
BP_OnDeath();
// schedule character respawn
GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &AShooterCharacter::OnRespawn, RespawnTime, false);
}
void AShooterCharacter::OnRespawn()
{
// destroy the character to force the PC to respawn
Destroy();
}

View File

@@ -0,0 +1,166 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AudioVideoRecordCharacter.h"
#include "ShooterWeaponHolder.h"
#include "ShooterCharacter.generated.h"
class AShooterWeapon;
class UInputAction;
class UInputComponent;
class UPawnNoiseEmitterComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FBulletCountUpdatedDelegate, int32, MagazineSize, int32, Bullets);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDamagedDelegate, float, LifePercent);
/**
* A player controllable first person shooter character
* Manages a weapon inventory through the IShooterWeaponHolder interface
* Manages health and death
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterCharacter : public AAudioVideoRecordCharacter, public IShooterWeaponHolder
{
GENERATED_BODY()
/** AI Noise emitter component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UPawnNoiseEmitterComponent* PawnNoiseEmitter;
protected:
/** Fire weapon input action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* FireAction;
/** Switch weapon input action */
UPROPERTY(EditAnywhere, Category ="Input")
UInputAction* SwitchWeaponAction;
/** Name of the first person mesh weapon socket */
UPROPERTY(EditAnywhere, Category ="Weapons")
FName FirstPersonWeaponSocket = FName("HandGrip_R");
/** Name of the third person mesh weapon socket */
UPROPERTY(EditAnywhere, Category ="Weapons")
FName ThirdPersonWeaponSocket = FName("HandGrip_R");
/** Max distance to use for aim traces */
UPROPERTY(EditAnywhere, Category ="Aim", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm"))
float MaxAimDistance = 10000.0f;
/** Max HP this character can have */
UPROPERTY(EditAnywhere, Category="Health")
float MaxHP = 500.0f;
/** Current HP remaining to this character */
float CurrentHP = 0.0f;
/** Team ID for this character*/
UPROPERTY(EditAnywhere, Category="Team")
uint8 TeamByte = 0;
/** List of weapons picked up by the character */
TArray<AShooterWeapon*> OwnedWeapons;
/** Weapon currently equipped and ready to shoot with */
TObjectPtr<AShooterWeapon> CurrentWeapon;
UPROPERTY(EditAnywhere, Category ="Destruction", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
float RespawnTime = 5.0f;
FTimerHandle RespawnTimer;
public:
/** Bullet count updated delegate */
FBulletCountUpdatedDelegate OnBulletCountUpdated;
/** Damaged delegate */
FDamagedDelegate OnDamaged;
public:
/** Constructor */
AShooterCharacter();
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Gameplay cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
/** Set up input action bindings */
virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
public:
/** Handle incoming damage */
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
public:
/** Handles start firing input */
UFUNCTION(BlueprintCallable, Category="Input")
void DoStartFiring();
/** Handles stop firing input */
UFUNCTION(BlueprintCallable, Category="Input")
void DoStopFiring();
/** Handles switch weapon input */
UFUNCTION(BlueprintCallable, Category="Input")
void DoSwitchWeapon();
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:
/** Returns true if the character already owns a weapon of the given class */
AShooterWeapon* FindWeaponOfType(TSubclassOf<AShooterWeapon> WeaponClass) const;
/** Called when this character's HP is depleted */
void Die();
/** Called to allow Blueprint code to react to this character's death */
UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta = (DisplayName = "On Death"))
void BP_OnDeath();
/** Called from the respawn timer to destroy this character and force the PC to respawn */
void OnRespawn();
};

View File

@@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/ShooterGameMode.h"
#include "ShooterUI.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/World.h"
void AShooterGameMode::BeginPlay()
{
Super::BeginPlay();
// create the UI
ShooterUI = CreateWidget<UShooterUI>(UGameplayStatics::GetPlayerController(GetWorld(), 0), ShooterUIClass);
ShooterUI->AddToViewport(0);
}
void AShooterGameMode::IncrementTeamScore(uint8 TeamByte)
{
// retrieve the team score if any
int32 Score = 0;
if (int32* FoundScore = TeamScores.Find(TeamByte))
{
Score = *FoundScore;
}
// increment the score for the given team
++Score;
TeamScores.Add(TeamByte, Score);
// update the UI
ShooterUI->BP_UpdateScore(TeamByte, Score);
}

View File

@@ -0,0 +1,42 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "ShooterGameMode.generated.h"
class UShooterUI;
/**
* Simple GameMode for a first person shooter game
* Manages game UI
* Keeps track of team scores
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterGameMode : public AGameModeBase
{
GENERATED_BODY()
protected:
/** Type of UI widget to spawn */
UPROPERTY(EditAnywhere, Category="Shooter")
TSubclassOf<UShooterUI> ShooterUIClass;
/** Pointer to the UI widget */
TObjectPtr<UShooterUI> ShooterUI;
/** Map of scores by team ID */
TMap<uint8, int32> TeamScores;
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
public:
/** Increases the score for the given team */
void IncrementTeamScore(uint8 TeamByte);
};

View File

@@ -0,0 +1,142 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Variant_Shooter/ShooterPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/LocalPlayer.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerStart.h"
#include "ShooterCharacter.h"
#include "ShooterBulletCounterUI.h"
#include "AudioVideoRecord.h"
#include "Widgets/Input/SVirtualJoystick.h"
void AShooterPlayerController::BeginPlay()
{
Super::BeginPlay();
// only spawn touch controls on local player controllers
if (IsLocalPlayerController())
{
if (SVirtualJoystick::ShouldDisplayTouchInterface())
{
// spawn the mobile controls widget
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
if (MobileControlsWidget)
{
// add the controls to the player screen
MobileControlsWidget->AddToPlayerScreen(0);
} else {
UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn mobile controls widget."));
}
}
// create the bullet counter widget and add it to the screen
BulletCounterUI = CreateWidget<UShooterBulletCounterUI>(this, BulletCounterUIClass);
if (BulletCounterUI)
{
BulletCounterUI->AddToPlayerScreen(0);
} else {
UE_LOG(LogAudioVideoRecord, Error, TEXT("Could not spawn bullet counter widget."));
}
}
}
void AShooterPlayerController::SetupInputComponent()
{
// only add IMCs for local player controllers
if (IsLocalPlayerController())
{
// add the input mapping contexts
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
// only add these IMCs if we're not using mobile touch input
if (!SVirtualJoystick::ShouldDisplayTouchInterface())
{
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
{
Subsystem->AddMappingContext(CurrentContext, 0);
}
}
}
}
}
void AShooterPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// subscribe to the pawn's OnDestroyed delegate
InPawn->OnDestroyed.AddDynamic(this, &AShooterPlayerController::OnPawnDestroyed);
// is this a shooter character?
if (AShooterCharacter* ShooterCharacter = Cast<AShooterCharacter>(InPawn))
{
// add the player tag
ShooterCharacter->Tags.Add(PlayerPawnTag);
// subscribe to the pawn's delegates
ShooterCharacter->OnBulletCountUpdated.AddDynamic(this, &AShooterPlayerController::OnBulletCountUpdated);
ShooterCharacter->OnDamaged.AddDynamic(this, &AShooterPlayerController::OnPawnDamaged);
// force update the life bar
ShooterCharacter->OnDamaged.Broadcast(1.0f);
}
}
void AShooterPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
{
// reset the bullet counter HUD
BulletCounterUI->BP_UpdateBulletCounter(0, 0);
// find the player start
TArray<AActor*> ActorList;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList);
if (ActorList.Num() > 0)
{
// select a random player start
AActor* RandomPlayerStart = ActorList[FMath::RandRange(0, ActorList.Num() - 1)];
// spawn a character at the player start
const FTransform SpawnTransform = RandomPlayerStart->GetActorTransform();
if (AShooterCharacter* RespawnedCharacter = GetWorld()->SpawnActor<AShooterCharacter>(CharacterClass, SpawnTransform))
{
// possess the character
Possess(RespawnedCharacter);
}
}
}
void AShooterPlayerController::OnBulletCountUpdated(int32 MagazineSize, int32 Bullets)
{
// update the UI
if (BulletCounterUI)
{
BulletCounterUI->BP_UpdateBulletCounter(MagazineSize, Bullets);
}
}
void AShooterPlayerController::OnPawnDamaged(float LifePercent)
{
if (IsValid(BulletCounterUI))
{
BulletCounterUI->BP_Damaged(LifePercent);
}
}

View File

@@ -0,0 +1,77 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "ShooterPlayerController.generated.h"
class UInputMappingContext;
class AShooterCharacter;
class UShooterBulletCounterUI;
/**
* Simple PlayerController for a first person shooter game
* Manages input mappings
* Respawns the player pawn when it's destroyed
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
/** Input mapping contexts for this player */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> DefaultMappingContexts;
/** Input Mapping Contexts */
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
/** Mobile controls widget to spawn */
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
/** Pointer to the mobile controls widget */
TObjectPtr<UUserWidget> MobileControlsWidget;
/** Character class to respawn when the possessed pawn is destroyed */
UPROPERTY(EditAnywhere, Category="Shooter|Respawn")
TSubclassOf<AShooterCharacter> CharacterClass;
/** Type of bullet counter UI widget to spawn */
UPROPERTY(EditAnywhere, Category="Shooter|UI")
TSubclassOf<UShooterBulletCounterUI> BulletCounterUIClass;
/** Tag to grant the possessed pawn to flag it as the player */
UPROPERTY(EditAnywhere, Category="Shooter|Player")
FName PlayerPawnTag = FName("Player");
/** Pointer to the bullet counter UI widget */
TObjectPtr<UShooterBulletCounterUI> BulletCounterUI;
protected:
/** Gameplay Initialization */
virtual void BeginPlay() override;
/** Initialize input bindings */
virtual void SetupInputComponent() override;
/** Pawn initialization */
virtual void OnPossess(APawn* InPawn) override;
/** Called if the possessed pawn is destroyed */
UFUNCTION()
void OnPawnDestroyed(AActor* DestroyedActor);
/** Called when the bullet count on the possessed pawn is updated */
UFUNCTION()
void OnBulletCountUpdated(int32 MagazineSize, int32 Bullets);
/** Called when the possessed pawn is damaged */
UFUNCTION()
void OnPawnDamaged(float LifePercent);
};

View File

@@ -0,0 +1,5 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterBulletCounterUI.h"

View File

@@ -0,0 +1,26 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ShooterBulletCounterUI.generated.h"
/**
* Simple bullet counter UI widget for a first person shooter game
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API UShooterBulletCounterUI : public UUserWidget
{
GENERATED_BODY()
public:
/** Allows Blueprint to update sub-widgets with the new bullet count */
UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta=(DisplayName = "UpdateBulletCounter"))
void BP_UpdateBulletCounter(int32 MagazineSize, int32 BulletCount);
/** Allows Blueprint to update sub-widgets with the new life total and play a damage effect on the HUD */
UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta=(DisplayName = "Damaged"))
void BP_Damaged(float LifePercent);
};

View File

@@ -0,0 +1,5 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterUI.h"

View File

@@ -0,0 +1,22 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ShooterUI.generated.h"
/**
* Simple scoreboard UI for a first person shooter game
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API UShooterUI : public UUserWidget
{
GENERATED_BODY()
public:
/** Allows Blueprint to update score sub-widgets */
UFUNCTION(BlueprintImplementableEvent, Category="Shooter", meta = (DisplayName = "Update Score"))
void BP_UpdateScore(uint8 TeamByte, int32 Score);
};

View File

@@ -0,0 +1,108 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterPickup.h"
#include "Components/SceneComponent.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "ShooterWeaponHolder.h"
#include "ShooterWeapon.h"
#include "Engine/World.h"
#include "TimerManager.h"
AShooterPickup::AShooterPickup()
{
PrimaryActorTick.bCanEverTick = true;
// create the root
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the collision sphere
SphereCollision = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere Collision"));
SphereCollision->SetupAttachment(RootComponent);
SphereCollision->SetRelativeLocation(FVector(0.0f, 0.0f, 84.0f));
SphereCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
SphereCollision->SetCollisionObjectType(ECC_WorldStatic);
SphereCollision->SetCollisionResponseToAllChannels(ECR_Ignore);
SphereCollision->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
SphereCollision->bFillCollisionUnderneathForNavmesh = true;
// subscribe to the collision overlap on the sphere
SphereCollision->OnComponentBeginOverlap.AddDynamic(this, &AShooterPickup::OnOverlap);
// create the mesh
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
Mesh->SetupAttachment(SphereCollision);
Mesh->SetCollisionProfileName(FName("NoCollision"));
}
void AShooterPickup::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (FWeaponTableRow* WeaponData = WeaponType.GetRow<FWeaponTableRow>(FString()))
{
// set the mesh
Mesh->SetStaticMesh(WeaponData->StaticMesh.LoadSynchronous());
}
}
void AShooterPickup::BeginPlay()
{
Super::BeginPlay();
if (FWeaponTableRow* WeaponData = WeaponType.GetRow<FWeaponTableRow>(FString()))
{
// copy the weapon class
WeaponClass = WeaponData->WeaponToSpawn;
}
}
void AShooterPickup::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the respawn timer
GetWorld()->GetTimerManager().ClearTimer(RespawnTimer);
}
void AShooterPickup::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
// have we collided against a weapon holder?
if (IShooterWeaponHolder* WeaponHolder = Cast<IShooterWeaponHolder>(OtherActor))
{
WeaponHolder->AddWeaponClass(WeaponClass);
// hide this mesh
SetActorHiddenInGame(true);
// disable collision
SetActorEnableCollision(false);
// disable ticking
SetActorTickEnabled(false);
// schedule the respawn
GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &AShooterPickup::RespawnPickup, RespawnTime, false);
}
}
void AShooterPickup::RespawnPickup()
{
// unhide this pickup
SetActorHiddenInGame(false);
// call the BP handler
BP_OnRespawn();
}
void AShooterPickup::FinishRespawn()
{
// enable collision
SetActorEnableCollision(true);
// enable tick
SetActorTickEnabled(true);
}

View File

@@ -0,0 +1,96 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Engine/DataTable.h"
#include "Engine/StaticMesh.h"
#include "ShooterPickup.generated.h"
class USphereComponent;
class UPrimitiveComponent;
class AShooterWeapon;
/**
* Holds information about a type of weapon pickup
*/
USTRUCT(BlueprintType)
struct FWeaponTableRow : public FTableRowBase
{
GENERATED_BODY()
/** Mesh to display on the pickup */
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UStaticMesh> StaticMesh;
/** Weapon class to grant on pickup */
UPROPERTY(EditAnywhere)
TSubclassOf<AShooterWeapon> WeaponToSpawn;
};
/**
* Simple shooter game weapon pickup
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterPickup : public AActor
{
GENERATED_BODY()
/** Collision sphere */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USphereComponent* SphereCollision;
/** Weapon pickup mesh. Its mesh asset is set from the weapon data table */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Mesh;
protected:
/** Data on the type of picked weapon and visuals of this pickup */
UPROPERTY(EditAnywhere, Category="Pickup")
FDataTableRowHandle WeaponType;
/** Type to weapon to grant on pickup. Set from the weapon data table. */
TSubclassOf<AShooterWeapon> WeaponClass;
/** Time to wait before respawning this pickup */
UPROPERTY(EditAnywhere, Category="Pickup", meta = (ClampMin = 0, ClampMax = 120, Units = "s"))
float RespawnTime = 4.0f;
/** Timer to respawn the pickup */
FTimerHandle RespawnTimer;
public:
/** Constructor */
AShooterPickup();
protected:
/** Native construction script */
virtual void OnConstruction(const FTransform& Transform) override;
/** Gameplay Initialization*/
virtual void BeginPlay() override;
/** Gameplay cleanup */
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/** Handles collision overlap */
UFUNCTION()
virtual void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
protected:
/** Called when it's time to respawn this pickup */
void RespawnPickup();
/** Passes control to Blueprint to animate the pickup respawn. Should end by calling FinishRespawn */
UFUNCTION(BlueprintImplementableEvent, Category="Pickup", meta = (DisplayName = "OnRespawn"))
void BP_OnRespawn();
/** Enables this pickup after respawning */
UFUNCTION(BlueprintCallable, Category="Pickup")
void FinishRespawn();
};

View File

@@ -0,0 +1,167 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterProjectile.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/DamageType.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "Engine/OverlapResult.h"
#include "Engine/World.h"
#include "TimerManager.h"
AShooterProjectile::AShooterProjectile()
{
PrimaryActorTick.bCanEverTick = true;
// create the collision component and assign it as the root
RootComponent = CollisionComponent = CreateDefaultSubobject<USphereComponent>(TEXT("Collision Component"));
CollisionComponent->SetSphereRadius(16.0f);
CollisionComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
CollisionComponent->SetCollisionResponseToAllChannels(ECR_Block);
CollisionComponent->CanCharacterStepUpOn = ECanBeCharacterBase::ECB_No;
// create the projectile movement component. No need to attach it because it's not a Scene Component
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Projectile Movement"));
ProjectileMovement->InitialSpeed = 3000.0f;
ProjectileMovement->MaxSpeed = 3000.0f;
ProjectileMovement->bShouldBounce = true;
// set the default damage type
HitDamageType = UDamageType::StaticClass();
}
void AShooterProjectile::BeginPlay()
{
Super::BeginPlay();
// ignore the pawn that shot this projectile
CollisionComponent->IgnoreActorWhenMoving(GetInstigator(), true);
}
void AShooterProjectile::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the destruction timer
GetWorld()->GetTimerManager().ClearTimer(DestructionTimer);
}
void AShooterProjectile::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
{
// ignore if we've already hit something else
if (bHit)
{
return;
}
bHit = true;
// disable collision on the projectile
CollisionComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// make AI perception noise
MakeNoise(NoiseLoudness, GetInstigator(), GetActorLocation(), NoiseRange, NoiseTag);
if (bExplodeOnHit)
{
// apply explosion damage centered on the projectile
ExplosionCheck(GetActorLocation());
} else {
// single hit projectile. Process the collided actor
ProcessHit(Other, OtherComp, Hit.ImpactPoint, -Hit.ImpactNormal);
}
// pass control to BP for any extra effects
BP_OnProjectileHit(Hit);
// check if we should schedule deferred destruction of the projectile
if (DeferredDestructionTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(DestructionTimer, this, &AShooterProjectile::OnDeferredDestruction, DeferredDestructionTime, false);
} else {
// destroy the projectile right away
Destroy();
}
}
void AShooterProjectile::ExplosionCheck(const FVector& ExplosionCenter)
{
// do a sphere overlap check look for nearby actors to damage
TArray<FOverlapResult> Overlaps;
FCollisionShape OverlapShape;
OverlapShape.SetSphere(ExplosionRadius);
FCollisionObjectQueryParams ObjectParams;
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
ObjectParams.AddObjectTypesToQuery(ECC_PhysicsBody);
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
if (!bDamageOwner)
{
QueryParams.AddIgnoredActor(GetInstigator());
}
GetWorld()->OverlapMultiByObjectType(Overlaps, ExplosionCenter, FQuat::Identity, ObjectParams, OverlapShape, QueryParams);
TArray<AActor*> DamagedActors;
// process the overlap results
for (const FOverlapResult& CurrentOverlap : Overlaps)
{
// overlaps may return the same actor multiple times per each component overlapped
// ensure we only damage each actor once by adding it to a damaged list
if (DamagedActors.Find(CurrentOverlap.GetActor()) == INDEX_NONE)
{
DamagedActors.Add(CurrentOverlap.GetActor());
// apply physics force away from the explosion
const FVector& ExplosionDir = CurrentOverlap.GetActor()->GetActorLocation() - GetActorLocation();
// push and/or damage the overlapped actor
ProcessHit(CurrentOverlap.GetActor(), CurrentOverlap.GetComponent(), GetActorLocation(), ExplosionDir.GetSafeNormal());
}
}
}
void AShooterProjectile::ProcessHit(AActor* HitActor, UPrimitiveComponent* HitComp, const FVector& HitLocation, const FVector& HitDirection)
{
// have we hit a character?
if (ACharacter* HitCharacter = Cast<ACharacter>(HitActor))
{
// ignore the owner of this projectile
if (HitCharacter != GetOwner() || bDamageOwner)
{
// apply damage to the character
UGameplayStatics::ApplyDamage(HitCharacter, HitDamage, GetInstigator()->GetController(), this, HitDamageType);
}
}
// have we hit a physics object?
if (HitComp->IsSimulatingPhysics())
{
// give some physics impulse to the object
HitComp->AddImpulseAtLocation(HitDirection * PhysicsForce, HitLocation);
}
}
void AShooterProjectile::OnDeferredDestruction()
{
// destroy this actor
Destroy();
}

View File

@@ -0,0 +1,109 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ShooterProjectile.generated.h"
class USphereComponent;
class UProjectileMovementComponent;
class ACharacter;
class UPrimitiveComponent;
/**
* Simple projectile class for a first person shooter game
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterProjectile : public AActor
{
GENERATED_BODY()
/** Provides collision detection for the projectile */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USphereComponent* CollisionComponent;
/** Handles movement for the projectile */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
UProjectileMovementComponent* ProjectileMovement;
protected:
/** Loudness of the AI perception noise done by this projectile on hit */
UPROPERTY(EditAnywhere, Category="Projectile|Noise", meta = (ClampMin = 0, ClampMax = 100))
float NoiseLoudness = 3.0f;
/** Range of the AI perception noise done by this projectile on hit */
UPROPERTY(EditAnywhere, Category="Projectile|Noise", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm"))
float NoiseRange = 3000.0f;
/** Tag of the AI perception noise done by this projectile on hit */
UPROPERTY(EditAnywhere, Category="Noise")
FName NoiseTag = FName("Projectile");
/** Physics force to apply on hit */
UPROPERTY(EditAnywhere, Category="Projectile|Hit", meta = (ClampMin = 0, ClampMax = 50000))
float PhysicsForce = 100.0f;
/** Damage to apply on hit */
UPROPERTY(EditAnywhere, Category="Projectile|Hit", meta = (ClampMin = 0, ClampMax = 100))
float HitDamage = 25.0f;
/** Type of damage to apply. Can be used to represent specific types of damage such as fire, explosion, etc. */
UPROPERTY(EditAnywhere, Category="Projectile|Hit")
TSubclassOf<UDamageType> HitDamageType;
/** If true, the projectile can damage the character that shot it */
UPROPERTY(EditAnywhere, Category="Projectile|Hit")
bool bDamageOwner = false;
/** If true, the projectile will explode and apply radial damage to all actors in range */
UPROPERTY(EditAnywhere, Category="Projectile|Explosion")
bool bExplodeOnHit = false;
/** Max distance for actors to be affected by explosion damage */
UPROPERTY(EditAnywhere, Category="Projectile|Explosion", meta = (ClampMin = 0, ClampMax = 5000, Units = "cm"))
float ExplosionRadius = 500.0f;
/** If true, this projectile has already hit another surface */
bool bHit = false;
/** How long to wait after a hit before destroying this projectile */
UPROPERTY(EditAnywhere, Category="Projectile|Destruction", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
float DeferredDestructionTime = 5.0f;
/** Timer to handle deferred destruction of this projectile */
FTimerHandle DestructionTimer;
public:
/** Constructor */
AShooterProjectile();
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Gameplay cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
/** Handles collision */
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
protected:
/** Looks up actors within the explosion radius and damages them */
void ExplosionCheck(const FVector& ExplosionCenter);
/** Processes a projectile hit for the given actor */
void ProcessHit(AActor* HitActor, UPrimitiveComponent* HitComp, const FVector& HitLocation, const FVector& HitDirection);
/** Passes control to Blueprint to implement any effects on hit. */
UFUNCTION(BlueprintImplementableEvent, Category="Projectile", meta = (DisplayName = "On Projectile Hit"))
void BP_OnProjectileHit(const FHitResult& Hit);
/** Called from the destruction timer to destroy this projectile */
void OnDeferredDestruction();
};

View File

@@ -0,0 +1,218 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterWeapon.h"
#include "Kismet/KismetMathLibrary.h"
#include "Engine/World.h"
#include "ShooterProjectile.h"
#include "ShooterWeaponHolder.h"
#include "Components/SceneComponent.h"
#include "TimerManager.h"
#include "Animation/AnimInstance.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Pawn.h"
AShooterWeapon::AShooterWeapon()
{
PrimaryActorTick.bCanEverTick = true;
// create the root
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
// create the first person mesh
FirstPersonMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("First Person Mesh"));
FirstPersonMesh->SetupAttachment(RootComponent);
FirstPersonMesh->SetCollisionProfileName(FName("NoCollision"));
FirstPersonMesh->SetFirstPersonPrimitiveType(EFirstPersonPrimitiveType::FirstPerson);
FirstPersonMesh->bOnlyOwnerSee = true;
// create the third person mesh
ThirdPersonMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Third Person Mesh"));
ThirdPersonMesh->SetupAttachment(RootComponent);
ThirdPersonMesh->SetCollisionProfileName(FName("NoCollision"));
ThirdPersonMesh->SetFirstPersonPrimitiveType(EFirstPersonPrimitiveType::WorldSpaceRepresentation);
ThirdPersonMesh->bOwnerNoSee = true;
}
void AShooterWeapon::BeginPlay()
{
Super::BeginPlay();
// subscribe to the owner's destroyed delegate
GetOwner()->OnDestroyed.AddDynamic(this, &AShooterWeapon::OnOwnerDestroyed);
// cast the weapon owner
WeaponOwner = Cast<IShooterWeaponHolder>(GetOwner());
PawnOwner = Cast<APawn>(GetOwner());
// fill the first ammo clip
CurrentBullets = MagazineSize;
// attach the meshes to the owner
WeaponOwner->AttachWeaponMeshes(this);
}
void AShooterWeapon::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// clear the refire timer
GetWorld()->GetTimerManager().ClearTimer(RefireTimer);
}
void AShooterWeapon::OnOwnerDestroyed(AActor* DestroyedActor)
{
// ensure this weapon is destroyed when the owner is destroyed
Destroy();
}
void AShooterWeapon::ActivateWeapon()
{
// unhide this weapon
SetActorHiddenInGame(false);
// notify the owner
WeaponOwner->OnWeaponActivated(this);
}
void AShooterWeapon::DeactivateWeapon()
{
// ensure we're no longer firing this weapon while deactivated
StopFiring();
// hide the weapon
SetActorHiddenInGame(true);
// notify the owner
WeaponOwner->OnWeaponDeactivated(this);
}
void AShooterWeapon::StartFiring()
{
// raise the firing flag
bIsFiring = true;
// check how much time has passed since we last shot
// this may be under the refire rate if the weapon shoots slow enough and the player is spamming the trigger
const float TimeSinceLastShot = GetWorld()->GetTimeSeconds() - TimeOfLastShot;
if (TimeSinceLastShot > RefireRate)
{
// fire the weapon right away
Fire();
} else {
// if we're full auto, schedule the next shot
if (bFullAuto)
{
GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::Fire, TimeSinceLastShot, false);
}
}
}
void AShooterWeapon::StopFiring()
{
// lower the firing flag
bIsFiring = false;
// clear the refire timer
GetWorld()->GetTimerManager().ClearTimer(RefireTimer);
}
void AShooterWeapon::Fire()
{
// ensure the player still wants to fire. They may have let go of the trigger
if (!bIsFiring)
{
return;
}
// fire a projectile at the target
FireProjectile(WeaponOwner->GetWeaponTargetLocation());
// update the time of our last shot
TimeOfLastShot = GetWorld()->GetTimeSeconds();
// make noise so the AI perception system can hear us
MakeNoise(ShotLoudness, PawnOwner, PawnOwner->GetActorLocation(), ShotNoiseRange, ShotNoiseTag);
// are we full auto?
if (bFullAuto)
{
// schedule the next shot
GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::Fire, RefireRate, false);
} else {
// for semi-auto weapons, schedule the cooldown notification
GetWorld()->GetTimerManager().SetTimer(RefireTimer, this, &AShooterWeapon::FireCooldownExpired, RefireRate, false);
}
}
void AShooterWeapon::FireCooldownExpired()
{
// notify the owner
WeaponOwner->OnSemiWeaponRefire();
}
void AShooterWeapon::FireProjectile(const FVector& TargetLocation)
{
// get the projectile transform
FTransform ProjectileTransform = CalculateProjectileSpawnTransform(TargetLocation);
// spawn the projectile
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.TransformScaleMethod = ESpawnActorScaleMethod::OverrideRootScale;
SpawnParams.Owner = GetOwner();
SpawnParams.Instigator = PawnOwner;
AShooterProjectile* Projectile = GetWorld()->SpawnActor<AShooterProjectile>(ProjectileClass, ProjectileTransform, SpawnParams);
// play the firing montage
WeaponOwner->PlayFiringMontage(FiringMontage);
// add recoil
WeaponOwner->AddWeaponRecoil(FiringRecoil);
// consume bullets
--CurrentBullets;
// if the clip is depleted, reload it
if (CurrentBullets <= 0)
{
CurrentBullets = MagazineSize;
}
// update the weapon HUD
WeaponOwner->UpdateWeaponHUD(CurrentBullets, MagazineSize);
}
FTransform AShooterWeapon::CalculateProjectileSpawnTransform(const FVector& TargetLocation) const
{
// find the muzzle location
const FVector MuzzleLoc = FirstPersonMesh->GetSocketLocation(MuzzleSocketName);
// calculate the spawn location ahead of the muzzle
const FVector SpawnLoc = MuzzleLoc + ((TargetLocation - MuzzleLoc).GetSafeNormal() * MuzzleOffset);
// find the aim rotation vector while applying some variance to the target
const FRotator AimRot = UKismetMathLibrary::FindLookAtRotation(SpawnLoc, TargetLocation + (UKismetMathLibrary::RandomUnitVector() * AimVariance));
// return the built transform
return FTransform(AimRot, SpawnLoc, FVector::OneVector);
}
const TSubclassOf<UAnimInstance>& AShooterWeapon::GetFirstPersonAnimInstanceClass() const
{
return FirstPersonAnimInstanceClass;
}
const TSubclassOf<UAnimInstance>& AShooterWeapon::GetThirdPersonAnimInstanceClass() const
{
return ThirdPersonAnimInstanceClass;
}

View File

@@ -0,0 +1,180 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ShooterWeaponHolder.h"
#include "Animation/AnimInstance.h"
#include "ShooterWeapon.generated.h"
class IShooterWeaponHolder;
class AShooterProjectile;
class USkeletalMeshComponent;
class UAnimMontage;
class UAnimInstance;
/**
* Base class for a simple first person shooter weapon
* Provides both first person and third person perspective meshes
* Handles ammo and firing logic
* Interacts with the weapon owner through the ShooterWeaponHolder interface
*/
UCLASS(abstract)
class AUDIOVIDEORECORD_API AShooterWeapon : public AActor
{
GENERATED_BODY()
/** First person perspective mesh */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USkeletalMeshComponent* FirstPersonMesh;
/** Third person perspective mesh */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
USkeletalMeshComponent* ThirdPersonMesh;
protected:
/** Cast pointer to the weapon owner */
IShooterWeaponHolder* WeaponOwner;
/** Type of projectiles this weapon will shoot */
UPROPERTY(EditAnywhere, Category="Ammo")
TSubclassOf<AShooterProjectile> ProjectileClass;
/** Number of bullets in a magazine */
UPROPERTY(EditAnywhere, Category="Ammo", meta = (ClampMin = 0, ClampMax = 100))
int32 MagazineSize = 10;
/** Number of bullets in the current magazine */
int32 CurrentBullets = 0;
/** Animation montage to play when firing this weapon */
UPROPERTY(EditAnywhere, Category="Animation")
UAnimMontage* FiringMontage;
/** AnimInstance class to set for the first person character mesh when this weapon is active */
UPROPERTY(EditAnywhere, Category="Animation")
TSubclassOf<UAnimInstance> FirstPersonAnimInstanceClass;
/** AnimInstance class to set for the third person character mesh when this weapon is active */
UPROPERTY(EditAnywhere, Category="Animation")
TSubclassOf<UAnimInstance> ThirdPersonAnimInstanceClass;
/** Cone half-angle for variance while aiming */
UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 90, Units = "Degrees"))
float AimVariance = 0.0f;
/** Amount of firing recoil to apply to the owner */
UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 100))
float FiringRecoil = 0.0f;
/** Name of the first person muzzle socket where projectiles will spawn */
UPROPERTY(EditAnywhere, Category="Aim")
FName MuzzleSocketName;
/** Distance ahead of the muzzle that bullets will spawn at */
UPROPERTY(EditAnywhere, Category="Aim", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
float MuzzleOffset = 10.0f;
/** If true, this weapon will automatically fire at the refire rate */
UPROPERTY(EditAnywhere, Category="Refire")
bool bFullAuto = false;
/** Time between shots for this weapon. Affects both full auto and semi auto modes */
UPROPERTY(EditAnywhere, Category="Refire", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
float RefireRate = 0.5f;
/** Game time of last shot fired, used to enforce refire rate on semi auto */
float TimeOfLastShot = 0.0f;
/** If true, the weapon is currently firing */
bool bIsFiring = false;
/** Timer to handle full auto refiring */
FTimerHandle RefireTimer;
/** Cast pawn pointer to the owner for AI perception system interactions */
TObjectPtr<APawn> PawnOwner;
/** Loudness of the shot for AI perception system interactions */
UPROPERTY(EditAnywhere, Category="Perception", meta = (ClampMin = 0, ClampMax = 100))
float ShotLoudness = 1.0f;
/** Max range of shot AI perception noise */
UPROPERTY(EditAnywhere, Category="Perception", meta = (ClampMin = 0, ClampMax = 100000, Units = "cm"))
float ShotNoiseRange = 3000.0f;
/** Tag to apply to noise generated by shooting this weapon */
UPROPERTY(EditAnywhere, Category="Perception")
FName ShotNoiseTag = FName("Shot");
public:
/** Constructor */
AShooterWeapon();
protected:
/** Gameplay initialization */
virtual void BeginPlay() override;
/** Gameplay Cleanup */
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
protected:
/** Called when the weapon's owner is destroyed */
UFUNCTION()
void OnOwnerDestroyed(AActor* DestroyedActor);
public:
/** Activates this weapon and gets it ready to fire */
void ActivateWeapon();
/** Deactivates this weapon */
void DeactivateWeapon();
/** Start firing this weapon */
void StartFiring();
/** Stop firing this weapon */
void StopFiring();
protected:
/** Fire the weapon */
virtual void Fire();
/** Called when the refire rate time has passed while shooting semi auto weapons */
void FireCooldownExpired();
/** Fire a projectile towards the target location */
virtual void FireProjectile(const FVector& TargetLocation);
/** Calculates the spawn transform for projectiles shot by this weapon */
FTransform CalculateProjectileSpawnTransform(const FVector& TargetLocation) const;
public:
/** Returns the first person mesh */
UFUNCTION(BlueprintPure, Category="Weapon")
USkeletalMeshComponent* GetFirstPersonMesh() const { return FirstPersonMesh; };
/** Returns the third person mesh */
UFUNCTION(BlueprintPure, Category="Weapon")
USkeletalMeshComponent* GetThirdPersonMesh() const { return ThirdPersonMesh; };
/** Returns the first person anim instance class */
const TSubclassOf<UAnimInstance>& GetFirstPersonAnimInstanceClass() const;
/** Returns the third person anim instance class */
const TSubclassOf<UAnimInstance>& GetThirdPersonAnimInstanceClass() const;
/** Returns the magazine size */
int32 GetMagazineSize() const { return MagazineSize; };
/** Returns the current bullet count */
int32 GetBulletCount() const { return CurrentBullets; }
};

View File

@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ShooterWeaponHolder.h"
// Add default functionality here for any IShooterWeaponHolder functions that are not pure virtual.

View File

@@ -0,0 +1,55 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ShooterWeaponHolder.generated.h"
class AShooterWeapon;
class UAnimMontage;
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UShooterWeaponHolder : public UInterface
{
GENERATED_BODY()
};
/**
* Common interface for Shooter Game weapon holder classes
*/
class AUDIOVIDEORECORD_API IShooterWeaponHolder
{
GENERATED_BODY()
public:
/** Attaches a weapon's meshes to the owner */
virtual void AttachWeaponMeshes(AShooterWeapon* Weapon) = 0;
/** Plays the firing montage for the weapon */
virtual void PlayFiringMontage(UAnimMontage* Montage) = 0;
/** Applies weapon recoil to the owner */
virtual void AddWeaponRecoil(float Recoil) = 0;
/** Updates the weapon's HUD with the current ammo count */
virtual void UpdateWeaponHUD(int32 CurrentAmmo, int32 MagazineSize) = 0;
/** Calculates and returns the aim location for the weapon */
virtual FVector GetWeaponTargetLocation() = 0;
/** Gives a weapon of this class to the owner */
virtual void AddWeaponClass(const TSubclassOf<AShooterWeapon>& WeaponClass) = 0;
/** Activates the passed weapon */
virtual void OnWeaponActivated(AShooterWeapon* Weapon) = 0;
/** Deactivates the passed weapon */
virtual void OnWeaponDeactivated(AShooterWeapon* Weapon) = 0;
/** Notifies the owner that the weapon cooldown has expired and it's ready to shoot again */
virtual void OnSemiWeaponRefire() = 0;
};