Hello everyone, here is a video as well as a few screenshots of my escape room.
The level plays on a space station above a gas planet where freight ships may dock to trade resources.
I created the level with assets from the “Modular Scifi Season 1 & 2” Starterpacks that you can grab from the marketplace for free. The door sounds are from mixkit, they have plenty of other sounds as well.
Unfortunately I struggled a bit with aligning the dimensions of the assets correctly so I kept the level short.
Gameplay video of the level:
Room Escape Walkthrough
When you look at the footage you will likely notice the flickering at the bottom of the screen.
I don’t exactly know where it comes from, I suspect it might be a result of practically every material in the scene being really glossy combined with indirect lightning being turned all the way up. I will leave the project as it is for now since I cannot fix it, but I would love to hear if someone has suggestions as to what might be the cause for this.
The main “extra” feature that I wanted my level to have were the clouds outside, and I managed to get them working with the “Volumetrics” plugin that comes with Unreal Engine. An issue that I ran into while doing this was that in order to have my space station above the clouds, I lifted everything up by 20km. Anyone that is trying to do this as well will notice that the level will break if you try to start it with the objects so far out, because actors get despawned if they move outside of the world border. The world border exists because unreal coordinates are floats, and the borders are about where the precision gets so low that the engine can’t reliably keep track of objects. In order to prevent running into this issue I ended up figuring out how to lower the height at which clouds start spawning, the adjustment can be made on the VolumetricCloud actor. Go to the Layer section and lower “Layer Bottom Altitude”, I hope this helps anyone that tries something similar.
Since I wanted to have a door that slides up instead of a rotating one, but also did not want to break the tutorial level I ended up refactoring the OpenDoor Component so it does not contain anything specific to opening a rotating door. I addded a seperate sound source for closing the door as well. Afterwards I could inherit from OpenDoor to create specialized cases for rotating and sliding doors. The code for this is below for anyone that is interested:
OpenDoor.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/TriggerVolume.h"
#include "OpenDoor.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class BUILDINGESCAPE_API UOpenDoor : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UOpenDoor();
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void TriggerDoor(float DeltaTime, bool bOpen);
void InitAudio();
void CheckPressurePlate();
protected:
// Called when the game starts
virtual void BeginPlay() override;
float DoorLastOpened = 0.f;
UPROPERTY(EditAnywhere)
float DoorCloseDelay = 2.f;
UPROPERTY(EditAnywhere)
float DoorCloseSpeed = 2.f;
UPROPERTY(EditAnywhere)
float DoorOpenSpeed = 2.f;
private:
bool bDoorOpened = false;
UPROPERTY(EditAnywhere)
ATriggerVolume* PressurePlate = nullptr;
UPROPERTY(EditAnywhere, meta = (Tooltip = "Weight to trigger the door."))
float OpenWeight = 60.f;
UPROPERTY(EditAnywhere)
UAudioComponent* OpenAudioComponent = nullptr;
UPROPERTY(EditAnywhere)
UAudioComponent* CloseAudioComponent = nullptr;
float TotalWeightOnPlate() const;
};
OpenDoor.cpp
#include "OpenDoor.h"
#include "Components/AudioComponent.h"
#include "Components/PrimitiveComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
#ifndef OUTPARAM
#define OUTPARAM
#endif
UOpenDoor::UOpenDoor()
{
PrimaryComponentTick.bCanEverTick = true;
}
// Called when the game starts
void UOpenDoor::BeginPlay()
{
Super::BeginPlay();
CheckPressurePlate();
InitAudio();
DoorLastOpened = GetWorld()->GetTimeSeconds();
}
void UOpenDoor::CheckPressurePlate() {
// Consider using isValid(PressurePlate) if there is a risk of the object being destroyed as the pointer is being checked
if (!PressurePlate) {
UE_LOG(LogTemp, Error, TEXT("Actor %s is missing the PressurePlate property!"), *GetOwner()->GetActorLabel());
}
}
void UOpenDoor::InitAudio() {
TArray<UAudioComponent*> AudioComponents;
GetOwner()->GetComponents<UAudioComponent>(AudioComponents);
if (AudioComponents.Num() < 2) {
UE_LOG(LogTemp, Error, TEXT("Door does not have 2 audio components!"));
}
OpenAudioComponent = AudioComponents[1];
CloseAudioComponent = AudioComponents[0];
if (!OpenAudioComponent || !CloseAudioComponent) {
UE_LOG(LogTemp, Error, TEXT("Door actor %s is missing an AudioComponent to play the sound of opening the door!"), *GetOwner()->GetName());
}
}
float UOpenDoor::TotalWeightOnPlate() const {
float TotalWeight = 0.f;
TArray<AActor*> OverlappingActors;
PressurePlate->GetOverlappingActors(OUTPARAM OverlappingActors); // GetOverlappingActors takes a set, but a TArray is a set
for (AActor* Actor : OverlappingActors) {
// Use calculate mass instead of get mass!!! GetMass includes all subcomponents!
TotalWeight += Actor->FindComponentByClass<UPrimitiveComponent>()->CalculateMass();
}
return TotalWeight;
}
void UOpenDoor::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
float PlateLoad = TotalWeightOnPlate();
if (PlateLoad >= OpenWeight) {
if (!bDoorOpened) {
OpenAudioComponent->Play();
bDoorOpened = true;
UE_LOG(LogTemp, Display, TEXT("Door %s | Trigger Open"), *GetOwner()->GetActorLabel());
}
TriggerDoor(DeltaTime, true);
DoorLastOpened = GetWorld()->GetTimeSeconds();
} else {
if (GetWorld()->GetTimeSeconds() >= DoorLastOpened + DoorCloseDelay) {
if (bDoorOpened) {
CloseAudioComponent->Play();
bDoorOpened = false;
UE_LOG(LogTemp, Display, TEXT("Door %s | Trigger Close"), *GetOwner()->GetActorLabel());
}
TriggerDoor(DeltaTime, false);
}
}
}
void UOpenDoor::TriggerDoor(float DeltaTime, bool bOpen) {
UE_LOG(LogTemp, Warning, TEXT("This open door component is only a base class and will not open anything!"))
}
With OpenDoor as the base case, Rotating and sliding Doors become rather short:
OpenRotatingDoor.h
#pragma once
#include "CoreMinimal.h"
#include "OpenDoor.h"
#include "OpenRotatingDoor.generated.h"
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class BUILDINGESCAPE_API UOpenRotatingDoor : public UOpenDoor
{
GENERATED_BODY()
public:
UOpenRotatingDoor();
void TriggerDoor(float DeltaTime, bool bOpen) override;
protected:
void BeginPlay() override;
private:
float InitialYaw = 0.f;
float CurrentYaw = 0.f;
UPROPERTY(EditAnywhere)
float TargetAngle = 90.f;
};
OpenRotatingDoor.cpp
#include "OpenRotatingDoor.h"
#include "Components/AudioComponent.h"
#include "Components/PrimitiveComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
UOpenRotatingDoor::UOpenRotatingDoor()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UOpenRotatingDoor::BeginPlay()
{
Super::BeginPlay();
//AActor* Owner = GetOwner();
FRotator CurrentRotation = GetOwner()->GetActorRotation();
CurrentYaw = CurrentRotation.Yaw;
InitialYaw = CurrentYaw;
TargetAngle = InitialYaw + TargetAngle;
UE_LOG(LogTemp, Warning, TEXT("%s: InitialYaw: %f, CurrentYaw: %f, TargetYaw: %f"), *GetOwner()->GetActorLabel(), InitialYaw, CurrentYaw, TargetAngle);
}
void UOpenRotatingDoor::TriggerDoor(float DeltaTime, bool bOpen)
{
FRotator CurrentRotation = GetOwner()->GetActorRotation();
CurrentRotation.Yaw = FMath::Lerp
(
CurrentRotation.Yaw,
bOpen ? TargetAngle : InitialYaw,
DeltaTime * (bOpen ? DoorOpenSpeed : DoorCloseSpeed)
);
GetOwner()->SetActorRotation(CurrentRotation);
}
OpenSlidingDoor.h
#pragma once
#include "CoreMinimal.h"
#include "OpenDoor.h"
//#include "Components/ActorComponent.h"
//#include "Engine/TriggerVolume.h"
#include "OpenSlidingDoor.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class BUILDINGESCAPE_API UOpenSlidingDoor : public UOpenDoor
{
GENERATED_BODY()
public:
UOpenSlidingDoor();
void TriggerDoor(float DeltaTime, bool bOpen) override; // mark these with virtual in the base class
protected:
void BeginPlay() override;
private:
UPROPERTY(EditAnywhere)
float TargetZ = 400.f;
UPROPERTY(EditAnywhere)
float InitialZ = 0.f;
};
OpenSlidingDoor.cpp
# pragma once
#include "OpenSlidingDoor.h"
#include "Components/AudioComponent.h"
#include "Components/PrimitiveComponent.h"
#include "GameFramework/Actor.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
#ifndef OUTPARAM
#define OUTPARAM
#endif
UOpenSlidingDoor::UOpenSlidingDoor()
{
// Enable running the tick function for this component
PrimaryComponentTick.bCanEverTick = true;
}
void UOpenSlidingDoor::BeginPlay()
{
Super::BeginPlay();
FVector CurrentLocation = GetOwner()->GetActorLocation();
// Translate door movement constraints into world space
TargetZ = TargetZ + CurrentLocation.Z;
InitialZ = InitialZ + CurrentLocation.Z;
}
void UOpenSlidingDoor::TriggerDoor(float DeltaTime, bool bOpen) {
FVector CurrentLocation = GetOwner()->GetActorLocation();
CurrentLocation.Z = FMath::Lerp
(
CurrentLocation.Z,
bOpen ? TargetZ : InitialZ,
DeltaTime * (bOpen ? DoorOpenSpeed : DoorCloseSpeed)
);
GetOwner()->SetActorLocation(CurrentLocation);
}
I chose to implement sliding door only along the Z axis because I did not know about the possibility to adress the fields of CurrentLocation via index (CurrentLocation[2]) rather than the name CurrentLocation.Z, if I would start over I would add a property for this to the component.
That is all from me for now, looking forward to your feedback!