Exploration over combat
The player advances through observation, travel, conversation, and environmental problem-solving rather than fighting enemies.
University team project
An atmospheric 3D exploration game built in Unreal Engine 5 by a team of eight.

In motion
Selected moments from the team's gameplay recording, showing the systems and player-facing features in context.





Overview
A Little Lost is an atmospheric 3D exploration game about Lumi, a small creature who leaves home to search for a missing loved one. The player travels by boat through a flooded and mysterious world, visiting islands shaped by the memories and grief of their inhabitants.
The experience is built around calm exploration, environmental puzzles, crafting, and visual storytelling rather than combat. Warm lights and inhabited spaces contrast with cold water and fog, creating a world that feels inviting and unsettling at the same time.
Role
I worked as a gameplay and systems programmer in a multidisciplinary team of eight. My original responsibility covered the interaction and dialogue systems, but it grew into coordinating much of the game's narrative progression as the project became more story-driven.
I created a reusable Interactable Actor base that allowed the player to talk with NPCs, collect objects, and use world elements through a consistent interaction flow. I also developed the dialogue framework, including speaker names, multi-line conversations, short gameplay messages, and dialogue selected from the player's current progress.
To connect these features across the full game, I built the ProgressionManager and StoryFlowManager. These systems tracked completed events, selected objectives, unlocked routes, responded to inventory changes, and ensured that dialogue, puzzles, and level transitions occurred in the correct order.
Design focus
The player advances through observation, travel, conversation, and environmental problem-solving rather than fighting enemies.
NPC dialogue, found objects, puzzles, and new destinations work together to guide the player through Lumi's journey.
Fog, water, sound, and warm points of light support a quiet, melancholic world without interrupting exploration.
Implementation
Built a C++ InteractableActor base and specialized interactions for NPCs, pickups, docks, doors, the gramophone, puzzle panels, and the lighthouse engine.
Created dialogue assets with speaker data, multiple lines, progression requirements, blocked states, completion flags, and support for automatic cutscene dialogue.
Implemented flag-based state tracking and objective updates that connected conversations, collected items, crafting, puzzles, and island access.
Integrated the story flow with Blueprints, inventory events, boat travel, cutscenes, level transitions, interaction prompts, and systems owned by other programmers.
Process
NPCs evaluate required and blocked progression flags, then start the first valid dialogue entry. A conversation can set a new flag when it ends, allowing narrative events to drive the next objective.
The StoryFlowManager translates many small gameplay flags into readable story states for the home area, three islands, the second level, and the lighthouse finale.
The dialogue manager handles short messages and longer conversations, locks movement when necessary, supports input-driven or timed advancement, and restores the interaction outline when dialogue ends.
Code samples
Excerpts from systems I contributed to during the project, lightly trimmed for a focused portfolio presentation.
Selects the first dialogue entry whose required flags are present and whose blocked flags have not been set.
bool ANPCInteractable::DoesEntryMatch(
AProgressionManager* Progression,
const FDialogueEntry& Entry) const
{
if (!Progression &&
(!Entry.RequiredFlags.IsEmpty() ||
!Entry.BlockedFlags.IsEmpty()))
return false;
for (const FName& Flag : Entry.RequiredFlags)
if (!Flag.IsNone() && !Progression->HasFlag(Flag))
return false;
for (const FName& Flag : Entry.BlockedFlags)
if (!Flag.IsNone() && Progression->HasFlag(Flag))
return false;
return true;
}Keeps story state accessible to C++ and Blueprints through a focused set of flag operations.
void AProgressionManager::AddFlag(FName FlagName)
{
if (FlagName.IsNone())
return;
ProgressFlags.Add(FlagName);
UE_LOG(LogTemp, Warning,
TEXT("Added progression flag: %s"),
*FlagName.ToString());
}
bool AProgressionManager::HasFlag(FName FlagName) const
{
return ProgressFlags.Contains(FlagName);
}Responds to inventory pickups on the next frame, updates the narrative state, and refreshes player objectives.
void AStoryFlowManager::HandleItemPickedUp(
FName ItemID,
bool bFirstPickupEver)
{
GetWorldTimerManager().SetTimerForNextTick(
FTimerDelegate::CreateWeakLambda(this,
[this, ItemID]()
{
CacheRuntimeReferences();
AProgressionManager* Progression =
GetProgressionManager();
if (!Progression)
return;
TryShowAllItemsFoundDialogue(Progression, ItemID);
UpdateStoryFlow();
UpdateObjectiveSystems(CurrentStoryState);
}));
}Reflection
The largest technical challenge was that dialogue, inventory, puzzles, travel, and objectives all depended on the same evolving story state. As the project grew, a change in one system could easily affect several others.
The original design considered both single-player and local co-op. Removing co-op was an important scope decision because it would have affected cameras, input, interactions, dialogue, puzzle design, and testing across the entire game.
Learning
I learned that a dialogue system is not automatically a story system. Narrative gameplay also needs explicit ownership of state, objectives, dependencies, and the rules that decide what can happen next.
I also gained practical experience integrating systems written by several programmers, debugging dependencies, and keeping shared interfaces understandable enough for teammates to use safely.
Next steps
I would begin a future narrative project with a more data-driven quest structure and clearer boundaries between dialogue, progression, story flow, and individual interactables.
I would also rely more consistently on delegates and gameplay events so the story reacts to changes instead of checking the complete state every frame.
Explore