← Back to Portfolio

ZoneClear

Six years of Unity taught me one engine deeply. This is the project where I learned to think in Unreal instead of translating from Unity in my head.

IDE Visual Studio
Engine Unreal Engine 5.7.4
Status In Progress
Timeline August 2026, Sprint 2–3

Every Unreal-only listing I skipped over the summer was the same signal: six years of Unity depth doesn't answer the question "have you shipped anything in Unreal." ZoneClear exists to answer that question directly — a hide-and-seek systems demo built in UE5, in C++, proving population spawning, interaction, and camouflage mechanics rather than modifying a template and calling it done.

Unreal isn't Unity with different menu names. GetComponent<T>() becomes FindComponentByClass<T>(). ScriptableObjects become UDataAssets. A null reference in Unity throws a catchable exception; a null pointer in Unreal is a hard crash. The actual challenge wasn't learning new syntax — it was un-learning the assumption that "I know how to build this system" transfers automatically just because I'd built the equivalent system in C# before.

Reasoning

I built ZoneClear as a separate project from an in-progress third-person controller, deliberately using Unreal's default Third Person template untouched — the point wasn't to prove I can build a character controller, it was to prove I can build gameplay systems in C++ against Unreal's actor/component model without leaning on Blueprint as a crutch.

ZoneClear places the player in a search space populated with a target NPC hidden among decoys. Finding the right one resolves the zone; guessing wrong doesn't end the round, it just tells you to keep looking.

Four C++ classes divide the system cleanly: a data asset for the pool, an actor that owns spawning and zone state, a component attached to every spawned NPC that knows whether it's the target, and a component on the player that fires the interaction trace. No class reaches into another's internals — the player's interaction component doesn't know what a "zone" is, it just calls OnInteract() on whatever it hits and lets that object decide what happens next.

UPopulationPoolData (UDataAsset)
    → weighted decoy prefabs + target prefab, pure data

AZoneMarkerActor (AActor)
    → owns Populate() / ClearPopulation() / OnTargetFound()
    → weighted random decoy selection at spawn time

UInteractableComponent (UActorComponent)
    → attached to every spawned NPC
    → bIsTarget + OwnerZone set at spawn, not baked into Blueprint
    → OnInteract() routes to OwnerZone->OnTargetFound() or logs a miss

UPlayerInteractionComponent (UActorComponent)
    → attached to the player
    → LineTraceSingleByChannel from camera on TryInteract()
    → calls OnInteract() on whatever the trace hits

Setting bIsTarget and OwnerZone at spawn time rather than baking them into a Blueprint was a deliberate choice — it means the same NPC prefab can be a decoy in one zone and the target in another, decided entirely by which pool spawned it.

Forward declarations aren't optional style — they're the difference between a header that compiles and one that doesn't. AZoneMarkerActor needed a forward declaration in UInteractableComponent's header (just the pointer type) and a full #include in the .cpp (to actually call methods on it). Getting this backwards is one of the more common ways to either bloat compile times or hit circular include errors.

Null pointers don't apologize. GetOwner() returning null and getting passed straight into AddIgnoredActor() caused a CDO cast crash — not a caught exception with a stack trace pointing at the problem, just a crash. Null-checking before use isn't defensive-programming paranoia in Unreal, it's the baseline.

SpawnActor's overload resolution is picky about value vs. pointer. A SpawnActor call that looked correct failed to compile until location and rotation were passed by value instead of by pointer — a small signature mismatch, but the kind that costs real time when you're still building intuition for the API surface.

This is proof I can hold an unfamiliar engine's actual constraints — hard crashes on null access, manual memory patterns, forward-declaration discipline — rather than translating Unity patterns into Unreal syntax and hoping the differences don't matter. It's also the reason I stopped skipping Unreal-only listings.