Fear of the Light
Platform
Engine
Language
Development Time
Team Size
PC
Unreal Engine
C++
7 Weeks
10
Fear of the Light is a survival horror game where light is scarier and more dangerous than darkness.
You’re the lone survivor on a spaceship filled with moth-like creatures. Explore the ship and escape. Use your tools to avoid the light, find your way forward, and manipulate the darkness to lure enemies and sneak around dangers.
My Role
I was a AI & Gameplay Programmer.
My main responsibilities were:
-
Swarm Enemy &
Enemy Light Detection -
Tool Manager
-
EMP Tool

Swarm Enemy & EnemyBase
EnemyBase
The Base Class for all enemies and the swarm enemy script detects light actors and assigns the light components to an array. When enemies are in a passive state, we filter out light components with an intensity greater than 0 and visible into a new array. Then we sort the array of light components by greatest light intensity. If the target light is not the same as the previous then we perform a raycast to see if it visible to the
Swarm
A state machine AI with a passive and an aggressive state. In its passive state, it moves to the latest light found if it is not already at a light. When a player is detected and the aggressive state is enabled it saves the latest light to be used later on, it also performs a line trace to see if the player is visible from the swams location. If visible then it moves to attack the player

Code
SwarmEnemyActor.cpp
#include "SwarmEnemyActor.h"
#include "GP3Character.h"
#include "Components/SphereComponent.h"
#include "Components/InterpToMovementComponent.h"
#include "Kismet/KismetMathLibrary.h"
#include "GameFramework/Character.h"
#include "Runtime/AIModule/Classes/AIController.h"
#include "Kismet/GameplayStatics.h"
#include "Components/LightComponent.h"
ASwarmEnemyActor::ASwarmEnemyActor()
{
RotationValue = CreateDefaultSubobject<USceneComponent>("YawValue");
RotationValue->SetupAttachment(RootComponent);
Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");
Sphere->SetupAttachment(RootComponent);
Sphere->SetSphereRadius(600.f);
Sphere->OnComponentBeginOverlap.AddDynamic(this, &ASwarmEnemyActor::PlayerSphereOverlap);
Sphere->OnComponentEndOverlap.AddDynamic(this, &ASwarmEnemyActor::PlayerSphereEndOverlap);
PrimaryActorTick.bCanEverTick = true;
}
void ASwarmEnemyActor::BeginPlay()
{
Super::BeginPlay();
AiController = Cast<AAIController>(GetController());
//Add Dynamic so we can use the function in parent script.
OnTargetLightChanged.AddDynamic(this, &ASwarmEnemyActor::HandleTargetLightChanged);
//Assign the player so we can access it health component.
GP3Character = Cast<AGP3Character>(UGameplayStatics::GetPlayerController(GetWorld(), 0)->GetCharacter());
if (GP3Character)
{
PlayerHealthComponent = GP3Character->GetComponentByClass<UHealthComponent>();
}
}
void ASwarmEnemyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//Plays the current state we are in.
(this->*StateHandlers[CurrentState])(DeltaTime);
}
void ASwarmEnemyActor::PlayerSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
//If the overlapping actor is a AGP3Character which is a then assign is as the Character we target.
if (OtherActor->IsA<AGP3Character>())
{
CharacterTarget = OtherActor;
}
}
void ASwarmEnemyActor::PlayerSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
//Remove Character Target if it leave the sphere.
if (OtherActor == CharacterTarget)
{
CharacterTarget = nullptr;
}
}
void ASwarmEnemyActor::PassiveState(float DeltaTime)
{
//If we get a Target Character then we stop going where we are going and switch to the Attack State.
if (CharacterTarget)
{
DistanceFromPlayer = GetDistanceTo(CharacterTarget);
float ClampedIgnoreLightDistance = FMath::Clamp(IgnoreLightDistance, 0.0f, Sphere->GetUnscaledSphereRadius() - 100);
//If the player is either lit up or within a certain distance from us then continue else we return here.
if (Cast<AGP3Character>(CharacterTarget)->PlayerIsLitUp == true || DistanceFromPlayer < ClampedIgnoreLightDistance)
{
AiController->StopMovement();
CurrentState = ESwarmEnemyState::Attack;
return;
}
}
if (bIsChasing)
{
//If the Swarm was previously going after the player.
bIsChasing = false;
//Changes the look of the swarm, calm them down.
ChaseCharachterChange();
//If we did not have a light target before we started going after the player then return.
if (!PreviousLight) return;
//Move towards the previous light we had.
ALightActor* PreviousLightLightActorTarget = Cast<ALightActor>(PreviousLight->GetOwner());
AiController->MoveToLocation(PreviousLightLightActorTarget->LightDownLocation);
}
//We play the sort and assign function in the parent script.
SortAndAssignLight();
}
void ASwarmEnemyActor::HuntPlayer(float DeltaTime)
{
if (!CharacterTarget)
{
//If the swarm losses the player then stop moving and switch to the passive state.
AiController->StopMovement();
CurrentState = ESwarmEnemyState::Passive;
return;
}
if (bIsChasing == false)
{
if (TargetLight)
{
//If we had a target light then we save it so we move towards it if we later switch to passive state.
PreviousLight = TargetLight;
}
//If we are in the light or we are close enough we do a line trace. And check if there is something blocking the view between the player and the swarm.
LookForPlayer();
if (!HitResult.bBlockingHit)
{
//If there were nothing blocking the line trace from the swarm to he player than we start chasing them.
bIsChasing = true;
//Changes the look of the swarm /makes them angry when attacking.
ChaseCharachterChange();
//Move towards the player.
AiController->MoveToActor(CharacterTarget);
}
}
}
void ASwarmEnemyActor::LookForPlayer()
{
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(this);
//Line trace from the swarm to the player location.
GetWorld()->LineTraceSingleByChannel(HitResult, GetActorLocation(), CharacterTarget->GetActorLocation(),
ECC_Visibility, CollisionQueryParams);
}
void ASwarmEnemyActor::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
void ASwarmEnemyActor::AttackPlayer()
{
if (PlayerHealthComponent != nullptr)
{
PlayerHealthComponent->TakeDamage(Damage);
}
}
void ASwarmEnemyActor::HandleTargetLightChanged()
{
//Get the target light actor and access its down location and move towards it, used in the EnemyActor script.
ALightActor* LightActorTarget = Cast<ALightActor>(TargetLight->GetOwner());
AiController->MoveToLocation(LightActorTarget->LightDownLocation);
}
SwarmEnemyActor.h
#pragma once
#include "CoreMinimal.h"
#include "EnemyActor.h"
#include "Components/SphereComponent.h"
#include "SwarmEnemyActor.generated.h"
class AAIController;
UENUM()
enum class ESwarmEnemyState
{
Passive,
Attack
};
UCLASS()
class GP3_API ASwarmEnemyActor : public AEnemyActor
{
GENERATED_BODY()
public:
ASwarmEnemyActor();
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemySettings")
float Damage = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemySettings")
bool bIsChasing = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemySettings")
float DistanceFromPlayer;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemySettings")
float IgnoreLightDistance = 400;
UPROPERTY(EditInstanceOnly, BlueprintReadWrite)
AActor* CharacterTarget = nullptr;
UPROPERTY(EditInstanceOnly, BlueprintReadOnly)
FHitResult HitResult;
UFUNCTION(BlueprintImplementableEvent)
void CheckLineTrace();
UFUNCTION(BlueprintImplementableEvent)
void ChaseCharachterChange();
UFUNCTION(BlueprintCallable)
void AttackPlayer();
protected:
virtual void BeginPlay() override;
UFUNCTION()
void PlayerSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult);
UFUNCTION()
void PlayerSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
//Current Actor
FVector Direction;
FVector StartLocation;
FVector CurrentDirection;
float TotalDistance;
float CurrentDistance;
//Target
FRotator TargetRotation;
FVector TargetDirection;
private:
UPROPERTY(EditDefaultsOnly)
AGP3Character* GP3Character = nullptr;
UPROPERTY(VisibleAnywhere)
ULightComponent* PreviousLight = nullptr;
UPROPERTY(EditDefaultsOnly)
UHealthComponent* PlayerHealthComponent = nullptr;
UPROPERTY(VisibleAnywhere)
USphereComponent* Sphere = nullptr;
UPROPERTY(VisibleAnywhere)
USceneComponent* RotationValue = nullptr;
UPROPERTY(VisibleAnywhere)
AAIController* AiController;
UPROPERTY(VisibleAnywhere)
ESwarmEnemyState CurrentState = ESwarmEnemyState::Passive;
TMap<ESwarmEnemyState, void (ASwarmEnemyActor::*)(float DeltaTime)> StateHandlers =
{
{ESwarmEnemyState::Passive, &ASwarmEnemyActor::PassiveState},
{ESwarmEnemyState::Attack, &ASwarmEnemyActor::HuntPlayer}
};
UFUNCTION()
void PassiveState(float DeltaTime);
UFUNCTION()
void HuntPlayer(float DeltaTime);
UFUNCTION()
void LookForPlayer();
UFUNCTION()
void HandleTargetLightChanged();
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
Enemy.cpp
#include "EnemyActor.h"
#include "Components/SphereComponent.h"
#include "Components/LightComponent.h"
#include "LightActor.h"
#include "Kismet/KismetSystemLibrary.h"
// Sets default values
AEnemyActor::AEnemyActor()
{
VisionSphere = CreateDefaultSubobject<USphereComponent>("VisionSphere");
VisionSphere->SetupAttachment(RootComponent);
VisionSphere->SetSphereRadius(900.f);
VisionSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemyActor::LightSphereOverlap);
VisionSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemyActor::LightSphereEndOverlap);
PrimaryActorTick.bCanEverTick = true;
}
void AEnemyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AEnemyActor::BeginPlay()
{
Super::BeginPlay();
BeginPlayAddLights();
}
void AEnemyActor::LightSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* LightOtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
FindLight(LightOtherActor);
}
void AEnemyActor::LightSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* LightOtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
RemoveLight(LightOtherActor);
}
void AEnemyActor::FindLight(AActor* LightOtherActor)
{
//Check if the actor that overlap is a light actor.
if (!LightOtherActor->IsA<ALightActor>()) return;
//If it was than add it to the ALightActor array.
ALightActor* ActorLight = Cast<ALightActor>(LightOtherActor);
AvailableLightActorsTArray.Add(ActorLight);
//Get the light component on the light actor and add it to the Available Lights TArray.
LightOtherActor->GetComponents<ULightComponent>(Lights, true);
AvailableLightsTArray.Append(Lights);
}
void AEnemyActor::RemoveLight(AActor* LightOtherActor)
{
//Check if the leaving actor is a light actor.
if (!LightOtherActor->IsA<ALightActor>()) return;
//Remove the LightActor from the available LightActors TArray.
AvailableLightActorsTArray.Remove(Cast<ALightActor>(LightOtherActor));
//Get the light actor light components and remove them from our light array.
LightOtherActor->GetComponents<ULightComponent>(Lights, true);
for (ULightComponent* light : Lights)
{
AvailableLightsTArray.Remove(light);
}
}
void AEnemyActor::SortAndAssignLight()
{
//If no light has entered then return
if (AvailableLightsTArray.Num() == 0) return;
//Filter all the lights and save the lights that are visible and have a higher intensity than 0 and add it no a new array.
TArray<ULightComponent*> FilteredAvailableLightsTArray = AvailableLightsTArray.FilterByPredicate(
[](ULightComponent* Light)
{
return Light->Intensity > 0 && Light->IsVisible();
});
//Check if the array is empty and if so then dont continue.
if (FilteredAvailableLightsTArray.Num() == 0) return;
//Sort the array so the light with the highest intensity is the first in the array.
FilteredAvailableLightsTArray.Sort([](ULightComponent& A, ULightComponent& B)
{
return A.Intensity >= B.Intensity;
});
//Assign the brightest light as the target.
TargetLight = FilteredAvailableLightsTArray[0];
//Check if our target light is the same as the previous light.
if (PreviousTargetLight != TargetLight)
{
//Line trace from the enemy to the target light location.
LookForLight();
//If we something is blocking then we stop.
if (HitResultLights.bBlockingHit) return;
//If we have a line of sight than we assign it as our previous light.
PreviousTargetLight = TargetLight;
//Use dynamic function in the swarm script that makes ut move towards the light.
OnTargetLightChanged.Broadcast();
}
}
void AEnemyActor::LookForLight()
{
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(this);
GetWorld()->LineTraceSingleByChannel(HitResultLights, GetActorLocation(),
TargetLight->GetOwner()->GetActorLocation(),
ECC_WorldDynamic, CollisionQueryParams);
}
void AEnemyActor::BeginPlayAddLights()
{
//Get the overlapping actors and check if it there is any light actors and add the light components on them to the lights array.
VisionSphere->GetOverlappingActors(StartActorArray, ALightActor::StaticClass());
for (int i = 0; i < StartActorArray.Num(); i++)
{
StartActorArray[i]->GetComponents<ULightComponent>(Lights, true);
if (Lights.Num() > 0)
{
AvailableLightActorsTArray.Add(Cast<ALightActor>(StartActorArray[i]));
}
AvailableLightsTArray.Append(Lights);
}
}
Enemy.h
#pragma once
#include "CoreMinimal.h"
#include "Components/SphereComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/Actor.h"
#include "EnemyLightDetect.h"
#include "Kismet/KismetSystemLibrary.h"
#include "GameFramework/Character.h"
#include "EnemyActor.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTargetLightChanged);
class UGP3SaveGame;
UCLASS()
class GP3_API AEnemyActor : public ACharacter
{
GENERATED_BODY()
public:
AEnemyActor();
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemySettings")
float LightDetectBrightness = 2;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "EnemySettings")
ULightComponent* TargetLight = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "EnemySettings")
TArray<ULightComponent*> AvailableLightsTArray;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "EnemySettings")
TArray<ALightActor*> AvailableLightActorsTArray;
UPROPERTY(EditAnywhere)
USphereComponent* VisionSphere;
UPROPERTY(VisibleAnywhere)
USceneComponent* Root = nullptr;
protected:
virtual void BeginPlay() override;
UFUNCTION()
void LightSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* LightOtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult);
UFUNCTION()
void LightSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* LightOtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
UFUNCTION()
void FindLight(AActor* LightOtherActor);
UFUNCTION()
void RemoveLight(AActor* LightOtherActor);
UFUNCTION()
void SortAndAssignLight();
UFUNCTION()
void LookForLight();
UFUNCTION()
void BeginPlayAddLights();
UPROPERTY(BlueprintAssignable)
FOnTargetLightChanged OnTargetLightChanged;
UPROPERTY(EditInstanceOnly, BlueprintReadOnly)
FHitResult HitResultLights;
UPROPERTY(EditInstanceOnly, BlueprintReadOnly)
TArray<AActor*> StartActorArray;
private:
UPROPERTY()
TArray<ULightComponent*> Lights;
UPROPERTY()
ULightComponent* PreviousTargetLight = nullptr;
};
Tool Manager
A system of controlling different tools player will be able to acquire throughout the game. This system will enable users to add, equip, unequip, and switch between tools

Code
ToolManager.cpp
#include "ToolManager.h"
#include "EnhancedInputSubsystems.h"
#include "GP3SaveGame.h"
#include "InputActionValue.h"
#include "GP3/BatteryComponent.h"
#include "GP3/GameManager.h"
// Sets default values for this component's properties
UToolManager::UToolManager()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = true;
// ...
}
void UToolManager::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
//PlayerInputComponent->BindAxis("ItemScrollerUp", this, &UToolManager::IncreaseWeaponIndex);
//PlayerInputComponent->BindAction(ItemScrollerUp, ETriggerEvent::Triggered, this, &UToolManager::IncreaseWeaponIndex);
//PlayerInputComponent->BindAxis("ItemScrollerDown", this, &UToolManager::DecreaseWeaponIndex);
//PlayerInputComponent->BindAction(ItemScrollerDown, ETriggerEvent::Triggered, this, &UToolManager::DecreaseWeaponIndex);
}
// Called when the game starts
void UToolManager::BeginPlay()
{
Super::BeginPlay();
BatteryComponent = GetOwner()->GetComponentByClass<UBatteryComponent>();
CurrentTool = ETool::NONE;
UGameManager* GameManager = GetWorld()->GetGameInstance<UGameManager>();
GameManager->OnSaveRequested.AddDynamic(this, &UToolManager::HandleSaveRequested);
GameManager->OnSaveDataLoaded.AddDynamic(this, &UToolManager::HandleSaveDataLoaded);
}
// Called every frame
void UToolManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (BatteryComponent && CurrentTool != ETool::EMP && !BatteryComponent->Consume(CurrentTool))
{
ToolToggleOn = false;
OnToolToggled.Broadcast(ToolToggleOn, CurrentTool);
switch (CurrentTool)
{
case ETool::RADAR:
SwitchToolRadar(FInputActionValue());
break;
case ETool::HEAT_VISION:
SwitchToolHeatVision(FInputActionValue());
break;
}
}
// ...
}
void UToolManager::AddTool(ETool Tool)
{
AvailableTools.Add(Tool);
OnToolAdded.Broadcast(Tool);
}
void UToolManager::EquipTool()
{
ToolToggleOn = false;
//int currentToolIndex = AvailableTools.IndexOfByKey(CurrentTool);
//CurrentTool = AvailableTools[currentToolIndex + 1];
CurrentTool = AvailableTools[Index];
switch (CurrentTool)
{
case ETool::NONE:
break;
case ETool::EMP:
OnSwitchedToEmp.Broadcast();
break;
case ETool::RADAR:
OnSwitchedToRadar.Broadcast();
break;
case ETool::HEAT_VISION:
OnSwitchedToHeatVision.Broadcast();
}
OnToolSwitched.Broadcast(CurrentTool);
}
void UToolManager::SwitchTool(const FInputActionValue& Value)
{
float Val = Value.Get<float>();
if (AvailableTools.Num() == 0) return;
if (CurrentTool == ETool::NONE)
{
Index = 0;
}
Index += Val;
if (Index < 0)
{
Index = AvailableTools.Num() - 1;
}
else if (Index >= AvailableTools.Num())
{
Index = 0;
}
CurrentTool = AvailableTools[Index];
EquipTool();
}
void UToolManager::SwitchToolRadar(const FInputActionValue& Value)
{
if (AvailableTools.Contains(ETool::RADAR))
{
if (CurrentTool != ETool::RADAR)
{
CurrentTool = ETool::RADAR;
}
else if (CurrentTool == ETool::RADAR)
{
CurrentTool = ETool::NONE;
}
OnSwitchedToRadar.Broadcast();
OnToolSwitched.Broadcast(CurrentTool);
}
}
void UToolManager::SwitchToolHeatVision(const FInputActionValue& Value)
{
if (AvailableTools.Contains(ETool::HEAT_VISION))
{
if (CurrentTool != ETool::HEAT_VISION)
{
CurrentTool = ETool::HEAT_VISION;
}
else if (CurrentTool == ETool::HEAT_VISION)
{
CurrentTool = ETool::NONE;
}
OnSwitchedToHeatVision.Broadcast();
OnToolSwitched.Broadcast(CurrentTool);
}
}
void UToolManager::SwitchToolEmp(const FInputActionValue& Value)
{
if (AvailableTools.Contains(ETool::EMP))
{
if (CurrentTool != ETool::EMP)
{
CurrentTool = ETool::EMP;
}
else if (CurrentTool == ETool::EMP)
{
CurrentTool = ETool::NONE;
}
OnSwitchedToEmp.Broadcast();
OnToolSwitched.Broadcast(CurrentTool);
}
}
bool UToolManager::bIsHoldingTool()
{
if (CurrentTool == ETool::RADAR || CurrentTool == ETool::EMP) return true;
return false;
}
void UToolManager::UnequipTool()
{
CurrentTool = ETool::NONE;
OnToolSwitched.Broadcast(CurrentTool);
}
void UToolManager::HandleSaveRequested(UGP3SaveGame* SaveGame)
{
SaveGame->UnlockedTools = AvailableTools;
SaveGame->CurrentTool = CurrentTool;
}
void UToolManager::HandleSaveDataLoaded(UGP3SaveGame* SaveGame)
{
AvailableTools = SaveGame->UnlockedTools;
switch (SaveGame->CurrentTool)
{
case ETool::NONE:
CurrentTool = ETool::NONE;
break;
case ETool::RADAR:
SwitchToolRadar(FInputActionValue());
break;
case ETool::HEAT_VISION:
SwitchToolHeatVision(FInputActionValue());
break;
case ETool::EMP:
SwitchToolEmp(FInputActionValue());
break;
}
};
ToolManager.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ToolManager.generated.h"
struct FInputActionValue;
class UBatteryComponent;
UENUM(BlueprintType)
enum class ETool : uint8
{
NONE UMETA(DisplayName = "NONE"),
RADAR UMETA(DisplayName = "RADAR"),
HEAT_VISION UMETA(DisplayName = "HEAT_VISION"),
EMP UMETA(DisplayName = "EMP"),
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSwitchToRadar);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSwitchToEmp);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSwitchToHeatVision);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnToolAdded, ETool, CurrentTool);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnToolToggled, bool, ToolToggleOn, ETool, CurrentTool);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnToolSwitched, ETool, CurrentTool);
class AGP3Character;
class UGP3SaveGame;
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class GP3_API UToolManager : public UActorComponent
{
GENERATED_BODY()
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ToolManager")
TArray<ETool> AvailableTools;
// Sets default values for this component's properties
UToolManager();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ToolManager")
ETool CurrentTool = ETool::NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
AGP3Character* OwnerCharacter = nullptr;
void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent);
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
UFUNCTION(BlueprintCallable, Category = "ToolManager")
void AddTool(ETool Tool);
UFUNCTION(BlueprintCallable, Category = "ToolManager")
void EquipTool();
UFUNCTION(BlueprintCallable)
void SwitchTool(const FInputActionValue& Value);
UFUNCTION()
ETool GetCurrentTool() const { return CurrentTool; }
UPROPERTY(BlueprintAssignable)
FSwitchToRadar OnSwitchedToRadar;
UPROPERTY(BlueprintAssignable)
FSwitchToEmp OnSwitchedToEmp;
UPROPERTY(BlueprintAssignable)
FSwitchToHeatVision OnSwitchedToHeatVision;
UPROPERTY(BlueprintAssignable)
FOnToolAdded OnToolAdded;
UPROPERTY(BlueprintAssignable)
FOnToolToggled OnToolToggled;
UPROPERTY(BlueprintAssignable)
FOnToolSwitched OnToolSwitched;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool ToolToggleOn = false;
UFUNCTION(BlueprintCallable)
void SwitchToolRadar(const FInputActionValue& Value);
UFUNCTION(BlueprintCallable)
void SwitchToolHeatVision(const FInputActionValue& Value);
UFUNCTION(BlueprintCallable)
void SwitchToolEmp(const FInputActionValue& Value);
UFUNCTION(BlueprintCallable)
bool bIsHoldingTool();
UFUNCTION(BlueprintCallable)
void UnequipTool();
private:
UPROPERTY(EditDefaultsOnly)
int Index;
UPROPERTY(VisibleAnywhere)
UBatteryComponent* BatteryComponent = nullptr;
UFUNCTION()
void HandleSaveRequested(UGP3SaveGame* SaveGame);
UFUNCTION()
void HandleSaveDataLoaded(UGP3SaveGame* SaveGame);
};
EMP & LightActor
Emp
A tool that allows you to turn of lightcomponents on specefic actors.
The emp works by shooting a linetrace towards the direction of where the player is looking.
LightActor
A light that we can deactivate with the emp tool. It records a specific location directly beneath it. Enemies navigate toward this location when they detect the light.

Code
EmpGun.cpp
#include "EmpGun.h"
#include "BatteryComponent.h"
#include "GP3Character.h"
#include "GP3.h"
#include "ToolManager.h"
#include "BatteryComponent.h"
#include "Camera/CameraComponent.h"
#include "Kismet/GameplayStatics.h"
// Sets default values
AEmpGun::AEmpGun()
{
// Set this actor to call Tick() every frame.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AEmpGun::BeginPlay()
{
Super::BeginPlay();
GP3Character = Cast<AGP3Character>(UGameplayStatics::GetPlayerController(GetWorld(), 0)->GetCharacter());
if (GP3Character)
{
ToolManager = GP3Character->GetComponentByClass<UToolManager>();
BatteryComponent = GP3Character->GetComponentByClass<UBatteryComponent>();
}
if (ToolManager)
{
ToolManager->OnToolToggled.AddDynamic(this, &AEmpGun::Shoot);
}
LastFireTime = 0;
MissLastFireTime = 0;
}
// Called every frame
void AEmpGun::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
BlueprintLineTraceFromPlayer();
}
void AEmpGun::Shoot(bool bIsToggledOn, ETool Tool)
{
//If our current tool is not the emp, if we dont have a target light and if the light is not visible/ intensity is 0, if //any then we stop.
if (Tool != ETool::EMP) return;
//If gun is in cooldown
if (!TargetLight)
{
Misfire();
return;
}
if ((!TargetLight->IsVisible()) || (TargetLight->Intensity == 0))
{
Misfire();
return;
}
//If we dont have battery larger than the emp depletion rate then we return.
if (BatteryComponent->CurrentPower < BatteryDepletion) return;
//If gun is in cooldown after shoot lamp
CurrentTime = GetWorld()->TimeSeconds;
if ((CurrentTime - LastFireTime < FireCoolDown) && (LastFireTime > 0)) return;
LastFireTime = CurrentTime;
//Remove battery power corresponding to our depletion rate.
BatteryComponent->CurrentPower -= BatteryDepletion;
//Play Emp sound the the emp blueprint
PlayEmpSound();
//Turn of the light
ALightActor* LightActorEffected = Cast<ALightActor>(TargetLight->GetOwner());
LightActorEffected->IsTurnedOfEmp = true;
}
void AEmpGun::BlueprintLineTraceFromPlayer()
{
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(this);
//CollisionQueryParams.AddIgnoredActor(GP3Character);
//If we have a player then get the get the players camera location and get the forwards direction from it.
if (!GP3Character) return;
FVector StartLocation = GP3Character->FirstPersonCameraComponent->GetComponentLocation();
FVector EndForward = GP3Character->FirstPersonCameraComponent->GetForwardVector() * Distance;
FVector EndLocation = EndForward + StartLocation;
//Do a line trace from the player camera location forwards using the EMP collision channel.
//GetWorld()->LineTraceSingleByChannel(HitResultEmp, StartLocation, EndLocation, ECC_EMP, CollisionQueryParams);
GetWorld()->LineTraceSingleByChannel(HitResultEmp, StartLocation, EndLocation, ECC_Visibility, CollisionQueryParams);
if (HitResultEmp.bBlockingHit)
{
//If we hit something we check if its a light actor.
//if (!HitResultEmp.GetActor()->IsA<ALightActor>()) return;
//If we hit something we check if its a light actor. If not then remove the current light target becuase now it is not //in line of sight, then return;
if (!HitResultEmp.GetActor()->IsA<ALightActor>())
{
TargetLight = nullptr;
return;
}
HitResultEmp.GetActor()->GetComponents(Lights, true);
//If the light component on the light actor is not visible or the light intensity is 0 then we dont continue.
if ((!Lights[0]->IsVisible()) || (Lights[0]->Intensity == 0)) return;
//If the light is visible and have a light intensity that is not 0 then we assign it as our target light.
TargetLight = Lights[0];
}
else
{
//Remove the Target light.
TargetLight = nullptr;
}
}
void AEmpGun::Misfire()
{
//Delay on mis fire
const float MissCurrentTime = GetWorld()->TimeSeconds;
if (MissCurrentTime - MissLastFireTime < MissCoolDown) return;
MissLastFireTime = MissCurrentTime;
//Mis fire blueprint event triggered
EmpMisfire();
}
EmpGun.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LightActor.h"
#include "GP3Character.h"
#include "EmpGun.generated.h"
UCLASS()
class GP3_API AEmpGun : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AEmpGun();
UPROPERTY(EditInstanceOnly, BlueprintReadOnly)
bool IsUsingLaserPointerBool = false;
UPROPERTY(EditInstanceOnly, BlueprintReadOnly)
bool TurnOfLightBool = false;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
float Distance = 2000;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
float BatteryDepletion = 10;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
ULightComponent* TargetLight = nullptr;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
float LastFireTime = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float FireCoolDown = 7;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float MissCoolDown = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float CurrentTime = 0;
UFUNCTION(BlueprintImplementableEvent)
void PlayEmpSound();
UFUNCTION(BlueprintImplementableEvent)
void EmpMisfire();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
private:
UPROPERTY(EditInstanceOnly)
FHitResult HitResultEmp;
UPROPERTY(EditDefaultsOnly)
TArray<ULightComponent*> Lights;
UPROPERTY(EditDefaultsOnly)
AGP3Character* GP3Character = nullptr;
UPROPERTY(EditDefaultsOnly)
UToolManager* ToolManager = nullptr;
UPROPERTY(EditDefaultsOnly)
UBatteryComponent* BatteryComponent = nullptr;
UPROPERTY(EditDefaultsOnly)
float MissLastFireTime = 0;
UFUNCTION()
void Shoot(bool bIsToggledOn, ETool Tool);
UFUNCTION()
void BlueprintLineTraceFromPlayer();
UFUNCTION()
void Misfire();
};
LightActor.cpp
#include "LightActor.h"
#include "GP3.h"
#include "SwarmEnemyActor.h"
#include "Components/SpotLightComponent.h"
ALightActor::ALightActor()
{
Root = CreateDefaultSubobject<USceneComponent>("Root");
RootComponent = Root;
LightSphere = CreateDefaultSubobject<USphereComponent>("VisionSphere");
LightSphere->SetupAttachment(Root);
LightSphere->SetSphereRadius(50.f);
LightSphere->SetCollisionObjectType(ECC_EMP);
LightSphere->SetVisibility(false);
PrimaryActorTick.bCanEverTick = true;
}
void ALightActor::BeginPlay()
{
Super::BeginPlay();
//Get the light component to the light target.
GetComponents(Lights, true);
LightActorsTargetLight = Lights[0];
//Set the call tracker to the timer time.
CallTracker = TurnOnDelay;
//Do line trace down and save the position so the enemy can move to it.
StartLineTrace();
}
void ALightActor::StartLineTrace()
{
FCollisionQueryParams CollisionQueryParams;
CollisionQueryParams.AddIgnoredActor(this);
FHitResult HitResult;
GetWorld()->LineTraceSingleByChannel(HitResult, GetActorLocation(), GetActorLocation() + FVector::DownVector * 1000,
ECC_WorldStatic, CollisionQueryParams);
if (HitResult.bBlockingHit)
{
//If we hit something assign the impact position as our down location.
LightDownLocation = HitResult.ImpactPoint;
}
}
void ALightActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//If the light shoot by the emp then do this.
if (IsTurnedOfEmp == true && bDoneOnce == false)
{
bDoneOnce = true;
//Get the light component.
GetComponents(Lights, true);
TargetLight = Lights[0];
//Trigger blueprint function to turn of the light component.
TurnOffLight();
//Start timer on the turn light back on function.
GetWorldTimerManager().SetTimer(MemberTimerHandle, this, &ALightActor::TurnLightBackOn, 1.0f, true, 0.5f);
}
}
void ALightActor::TurnLightBackOn()
{
//Lower the timer and if its get to 0 or bellow trigger something.
--CallTracker;
if (CallTracker <= 0)
{
//Clear the timer and reset it.
GetWorldTimerManager().ClearTimer(MemberTimerHandle);
CallTracker = TurnOnDelay;
bDoneOnce = false;
//Make it able to be shot by the emp again.
IsTurnedOfEmp = false;
//Trigger blueprint function to turn the light component back on.
TurnOnLight();
}
}
LightActor.h
#pragma once
#include "Components/SpotLightComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GP3Character.h"
#include "LightActor.generated.h"
class AGP3Character;
UCLASS()
class ALightActor : public AActor
{
GENERATED_BODY()
public:
ALightActor();
virtual void Tick(float DeltaTime) override;
UPROPERTY(VisibleAnywhere)
USceneComponent* Root = nullptr;
UPROPERTY(EditAnywhere, Category = "LightSettings")
bool IsTurnedOfEmp = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LightSettings")
float TurnOnDelay = 5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LightSettings")
FVector LightDownLocation;
UFUNCTION(BlueprintImplementableEvent)
void TurnOffLight();
UFUNCTION(BlueprintImplementableEvent)
void TurnOnLight();
UFUNCTION()
void StartLineTrace();
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
ULightComponent* TargetLight = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
ULightComponent* LightActorsTargetLight = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
USphereComponent* LightSphere;
protected:
private:
virtual void BeginPlay() override;
UFUNCTION()
void TurnLightBackOn();
UPROPERTY(EditDefaultsOnly)
TArray<ULightComponent*> Lights;
FTimerHandle MemberTimerHandle;
bool bDoneOnce = false;
int32 CallTracker;
};