Unreal 5.2 C++ & Blueprints project for Ravensbourne University of London Work Based Learning module and the 2024 Panic horror Jam
As this was my first main project for C++ in the Unreal engine, i wanted to learn how i could make interactions between objects, and i spent a lot of time creating the Pick-up flash light mechanic and an object interaction component that could be added to any object to make it interactable with by the player.
For the flashlight, as i wanted to learn how the engine worked i largely based the code off of the First Person Shooter template's rifle that can be picked up. I had a lot of issues with the code as i was mostly breaking it down to guess how it worked, but i was able to create a functioning flashlight that the player could pick up and use effectively.
// Fill out your copyright notice in the Description page of Project Settings. #include "CPP_Flashlight.h" #include "EditorDemoCharacter.h" #include "GameFramework/PlayerController.h" #include "Camera/PlayerCameraManager.h" #include "Kismet/GameplayStatics.h" #include "EnhancedInputComponent.h" #include "EnhancedInputSubsystems.h" // Sets default values ACPP_Flashlight::ACPP_Flashlight() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; flashlightMesh = CreateDefaultSubobject(TEXT("FlashLightMesh")); RootComponent = flashlightMesh; flashLightSource = CreateDefaultSubobject (TEXT("FlashLightSource")); flashLightSource->SetupAttachment(flashlightMesh); timeBetweenCalls = 0.1; } void ACPP_Flashlight::Tick(float DeltaTime) { timer += DeltaTime; UE_LOG(LogTemp, Warning, TEXT("Timer is %d"), timer); } void ACPP_Flashlight::AttachWeapon(AEditorDemoCharacter* TargetCharacter) { Character = TargetCharacter; if (Character == nullptr) { return; } // Attach the weapon to the First Person Character FAttachmentTransformRules AttachmentRules(EAttachmentRule::SnapToTarget, true); AttachToComponent(Character->GetMesh1P(), AttachmentRules, FName(TEXT("GripPoint"))); // switch bHasRifle so the animation blueprint can switch to another animation set Character->SetHasRifle(true); // Set up action bindings if (APlayerController* PlayerController = Cast (Character->GetController())) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem (PlayerController->GetLocalPlayer())) { // Set the priority of the mapping to 1, so that it overrides the Jump action with the Fire action when using touch input Subsystem->AddMappingContext(FireMappingContext, 1); } if (UEnhancedInputComponent* EnhancedInputComponent = Cast (PlayerController->InputComponent)) { // Fire EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Triggered, this, &ACPP_Flashlight::Fire); } } } void ACPP_Flashlight::Fire() { //timer created to counteract double-firing issue, when called increases "timer" so that it only triggers on the 2nd call when double-called timer += 0.1; if (timer > timeBetweenCalls) { if (flashLightSound != nullptr) { UGameplayStatics::PlaySoundAtLocation(this, flashLightSound, this->GetActorLocation()); } flashLightSource->ToggleVisibility(); timer = 0; } else { return; } } void ACPP_Flashlight::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (Character == nullptr) { return; } //if the player ref is stored when the game ends, undoes the mapping contexts it set if (APlayerController* PlayerController = Cast (Character->GetController())) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem (PlayerController->GetLocalPlayer())) { Subsystem->RemoveMappingContext(FireMappingContext); } } }
// Fill out your copyright notice in the Description page of Project Settings. #include "CPP_InteractionComponent.h" UCPP_InteractionComponent::UCPP_InteractionComponent() { } void UCPP_InteractionComponent::BeginPlay() { Super::BeginPlay(); OnComponentBeginOverlap.AddDynamic(this, &UCPP_InteractionComponent::OnSphereBeginOverlap); } void UCPP_InteractionComponent::OnSphereBeginOverlap ( UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult ) { // Checking if it is a First Person Character overlapping AEditorDemoCharacter* Character = Cast(OtherActor); if (Character != nullptr) { OnPlayerEnter.Broadcast(Character); } } void UCPP_InteractionComponent::Interact(AActor* OtherActor) { AEditorDemoCharacter* Character = Cast (OtherActor); if (Character != nullptr) { OnPlayerEnter.Broadcast(Character); } }
While an incredibly simple script, this script allows for a brief interaction through blueprints by connecting the player and the desired scripts with this script. The basis of this script was to be added to any object that had a basic single function, such as picking up an item to add to the inventory of the player or interacting with a box to use up items in the inventory. It was a useful script that allowed me to think a bit more freely when implimenting, as i could this script as a basis for other simple objects i was creating and it sped up my development as i didnt have to add these interactions to every single script and could just add the component to the object i wanted to have the player interact with.
What justified the use of my interaction component was my need for puzzle elements. Most simple horror games will have a "collct [object/s] to open a door" and so i wanted to add those and a few of those elements to my game along with a light switch, being able to focus on individual functionality for these and use my interaction script to enable their functionality made it a lot easier to develop.
The light switch i wanted to make was designed so that i could add as many lights as needed, keeping a TArray of lights that i could expand as needed that, when triggered, will cycle through and active/ deactivate all the lights.
#include "CPP_LightSwitch.h" // Sets default values ACPP_LightSwitch::ACPP_LightSwitch() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; lightSwitchOnMesh = CreateDefaultSubobject(TEXT("lightBoxOnMesh")); RootComponent = lightSwitchOnMesh; lightSwitchOffMesh = CreateDefaultSubobject (TEXT("lightBoxOffMesh")); lightSwitchOffMesh->SetupAttachment(lightSwitchOnMesh); lightSwitchOnMesh->ToggleVisibility(); } // Called when the game starts or when spawned void ACPP_LightSwitch::BeginPlay() { Super::BeginPlay(); lights.Init({}, lightObjects.Num()); for (int i = 0; i < lightObjects.Num(); i++) { lights[i] = lightObjects[i]->GetComponentByClass (); lights[i]->ToggleVisibility(); } } // Called every frame void ACPP_LightSwitch::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void ACPP_LightSwitch::Toggle() { lightSwitchOnMesh->ToggleVisibility(); lightSwitchOffMesh->ToggleVisibility(); for (int i = 0; i < lightObjects.Num(); i++) { lights[i]->ToggleVisibility(); } }
For the Fuses and Fusebox, they reference a very simple inventory component in the player to handle the inventory. The fusebox also includes a similar feature to the lightswitch, but also requires the player to have the correct amount of fuses before activating, also having fuse objects that appear inside the box as if the player is placing them inside.
#include "CPP_PickUpFuse.h" #include "CPP_PlayerInventory.h" // Sets default values ACPP_PickUpFuse::ACPP_PickUpFuse() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; fuseMesh = CreateDefaultSubobject(TEXT("fuseMesh")); RootComponent = fuseMesh; } void ACPP_PickUpFuse::PickUp(AActor* actor) { //incriments the player's inventory for this item by 1 actor->GetComponentByClass ()->AddToInventory(1); Destroy(); } // Called when the game starts or when spawned void ACPP_PickUpFuse::BeginPlay() { Super::BeginPlay(); } // Called every frame void ACPP_PickUpFuse::Tick(float DeltaTime) { Super::Tick(DeltaTime); }
#include "CPP_FuseBox.h" #include "CPP_PlayerInventory.h" #include "Kismet/GameplayStatics.h" // Sets default values ACPP_FuseBox::ACPP_FuseBox() { // Set this actor to call Tick() every frame. PrimaryActorTick.bCanEverTick = true; generatorMesh = CreateDefaultSubobject(TEXT("generatorMesh")); RootComponent = generatorMesh; fuseMesh1 = CreateDefaultSubobject (TEXT("fuseMesh1")); fuseMesh1->SetupAttachment(RootComponent); fuseMesh2 = CreateDefaultSubobject (TEXT("fuseMesh2")); fuseMesh2->SetupAttachment(RootComponent); fuseMesh3 = CreateDefaultSubobject (TEXT("fuseMesh3")); fuseMesh3->SetupAttachment(RootComponent); } // Called when the game starts or when spawned void ACPP_FuseBox::BeginPlay() { Super::BeginPlay(); generatorLights.Init({}, generatorLightObjects.Num()); fuseMesh1->ToggleVisibility(); fuseMesh2->ToggleVisibility(); fuseMesh3->ToggleVisibility(); for (int i = 0; i < generatorLightObjects.Num(); i++) { generatorLights[i] = generatorLightObjects[i]->GetComponentByClass (); generatorLights[i]->ToggleVisibility(); } } // Called every frame void ACPP_FuseBox::Tick(float DeltaTime) { Super::Tick(DeltaTime); } void ACPP_FuseBox::GeneratorInteract(AActor* playerActor) { UCPP_PlayerInventory* playerInv = playerActor->GetComponentByClass (); int playerFuseCount = playerInv->fuseAmount; if (playerFuseCount >= fuseRequirement) { fuseMesh1->ToggleVisibility(); fuseMesh2->ToggleVisibility(); fuseMesh3->ToggleVisibility(); playerInv->RemoveFromInventory(fuseRequirement); UGameplayStatics::PlaySoundAtLocation( this, fuseBoxSound, this->GetActorLocation(), 1.0f, 1.0f, 0.0f, fuseBoxAttenuation); OnFuseBoxCompleted.Broadcast(TEXT("CalledTheFunction")); for (int i = 0; i < generatorLightObjects.Num(); i++) { generatorLights[i]->ToggleVisibility(); } } }