Exposure
Post-Apocalyptic Survial Horror Game
Post-Apocalyptic Survial Horror Game

Exposure is an exploration and stealth-based survival horror game. The objective is to take photos of specific locations based on reference polaroids. To achieve this, the player must navigate through the abandoned area while avoiding the shadows-hostile creatures inhabiting this irradiated city.
This project is a 3-week challenge aimed at creating a complete short gaming experience, built by an 8-person team in just 3 weeks.
Photo & Save System
Generating, processing, and saving in-game images can cause major performance drops (stuttering/hitches) and RAM saturation. The objective was to design an in-game photo system capable of exporting renders directly to the disk without affecting the frame rate, while ensuring data persistence (even in the event of a game crash).
Technical Approach & Architecture
- AI Hijacking (
UAIPerceptionComponent): Repurposed use of the AI perception component attached to the camera. The sight sense cone acts as a detection frustum to instantly filter on-screen tags (Stimuli), avoiding multiple LineTraces or expensive GPU/CPU collision tests. - Subsystem Architecture (
UGameInstanceSubsystem): Centralization of the save logic in C++. Offers global access without tight coupling and guarantees image data persistence during scene transitions. - Render Target Memory Management: On-demand capture via a
USceneCaptureComponent2D. The Render Target is generated, passed to the system, and then immediately destroyed to free VRAM. - Persistence & I/O (
IFileManager&UKismetRenderingLibrary): Image export to disk viaExportRenderTarget. Synchronization with the save file (USaveGame) to maintain an index (name/path) of captures, eliminating synchronous folder scans on the disk.
bool UPhotoSubsysteme::PrintPhoto(UObject* WorldContextObject, USceneCaptureComponent2D* SceneCaptureComponent, const FName& TargetLocationName)
{
auto T = UKismetRenderingLibrary::CreateRenderTarget2D(GetWorld(), PhotoSize,PhotoSize);
if (!T)
{
UE_LOG(LogPhotoSubsystem, Error, TEXT("Failed to create render target 2D"));
return false;
}
SceneCaptureComponent->TextureTarget = T;
SceneCaptureComponent->CaptureScene();
const FString FileName = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
FString AbsSavePath = FPaths::ConvertRelativePathToFull(SavePath);
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
if (!PF.DirectoryExists(*AbsSavePath))
{
PF.CreateDirectoryTree(*AbsSavePath);
}
FString Extention;
if (T->RenderTargetFormat == RTF_RGBA16f || T->RenderTargetFormat == RTF_RGBA32f || T->RenderTargetFormat == RTF_RGB10A2)
{
Extention = TEXT(".exr");
}
else
{
Extention = TEXT(".png");
}
FString FullName = FileName + Extention;
FString FullFilePath = FPaths::Combine(AbsSavePath, FullName);
UKismetRenderingLibrary::ExportRenderTarget(WorldContextObject, T, AbsSavePath, FullName);
if (PF.FileExists(*FullFilePath))
{
TempPhoto.Add(TargetLocationName, FullFilePath);
SceneCaptureComponent->TextureTarget = nullptr;
return true;
}
else
{
UE_LOG(LogPhotoSubsystem, Warning, TEXT("Export rate"));
SceneCaptureComponent->TextureTarget = nullptr;
return false;
}
}
Event-Driven Architecture & Decoupled Communication System
In video game architectures, direct dependencies between systems (e.g., player health querying the UI, or the UI polling the player state every frame) create tight coupling, circular references, and wasted CPU cycles through the heavy use of the Tick event. The goal was to design a completely decoupled and reactive Observer communication model.
Technical Approach & Architecture
- Multicast Delegates (Event Dispatchers): Implementation of a Publisher/Subscriber model. Publishers broadcast an event without knowing the existence or the number of their subscribers.
- Polling Elimination (Tickless Design): Transition from a frame-by-frame pull model to a push model triggered only upon state changes.
- Prevention of Circular References: Ensuring that no low-level module (e.g., PlayerState) includes high-level modules (e.g., HUD / UI), preventing cross-compilation errors and simplifying maintenance.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChanged, int, Health, int, MaxHealth);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnRadiationChanged, float, RadiationValue, float, MaxRadiationValue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerDeath, EDeathReason, DeathReason);
void ULifeComponent::TakeDamage()
{
if (Health <= 0) return;
Health--;
OnHealthChanged.Broadcast(Health, MaxHealth);
if (Health <= 0)
{
Die(EDeathReason::Health);
return;
}
UE_LOG(LogTemp, Warning, TEXT("Damage sound : %hhd"), GetSound("Audio.Player.Damage.Health") ? true : false);
UGameplayStatics::PlaySound2D(GetOwner()->GetWorld(), GetSound("Audio.Player.Damage.Health"));
}
