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

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


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"));
}