Case Closed
URP was the right call here since the game doesn't need the heavy lighting features of HDRP, and URP keeps performance reasonable on a wider range of hardware, which matters when you're also running an LLM locally.
Overview
Case Closed is a first-person detective game I built over two years, starting with planning in 2022 and development in 2023. Every citizen in the city is driven by a Large Language Model. You talk to them by typing whatever you want, and they respond in character. No dialogue trees, no scripted lines. Because of this, no two playthroughs or murder cases play out the same way.
The LLM System
The backbone for the LLM integration was LLMUnity by undream ai. It gave me a solid starting point for running models inside Unity, but I had to modify it fairly heavily. The biggest issue was running multiple LLM instances simultaneously. In a city full of NPCs, you can't have one model blocking everything else. I reworked the system to handle concurrent inference through caching and GPU offloading so the game would actually run on hardware that isn't a datacenter. Saving and loading conversations also required modifications to the underlying system, since the original implementation wasn't built with persistent game sessions in mind.
All models run locally on the player's machine. I used Meta's LLaMA models, which gave the best results for this use case, but technically any model works since the game leans heavily on prompt instructions rather than relying on a specific model's strengths.
Dynamic Prompts and NPC Awareness
A static prompt doesn't cut it when your NPCs need to react to a living world. Each citizen needs to know where they are, what's around them, who they're friends with, how they feel, and what the player has done recently. I built a dynamic prompt system that injects all of this context (location, nearby objects, relationships, emotional state, player reputation) into the NPC's instructions at runtime. The trick was figuring out when to update these prompts. Doing it mid-conversation would cause noticeable lag, so I calculated specific safe moments, like transitions between areas and natural pauses in gameplay, to refresh the context without the player ever noticing a hitch.
JSON ended up being central to how I managed all of this. NPC data, conversation history, dynamic prompt state, save files: everything serialized through JSON. It kept things readable and easy to debug, which mattered a lot when I was manually testing dozens of NPCs with different configurations.
Prompt Engineering and Anti-Jailbreak
Getting the prompts right took a long time. LLMs hallucinate. They'll invent facts, contradict themselves, or go completely off-script if you let them. I spent a lot of hours on manual testing, tweaking instructions, and iterating on prompt structure to keep NPCs consistent and grounded in their assigned roles and backstories.
Then there's jailbreaking. Players will absolutely try to trick NPCs into breaking character, whether by asking them to ignore their instructions, pretending to be the developer, or fishing for system prompt details. I implemented keyword filters that catch common jailbreak patterns and redirect the conversation back to the NPC's character. It's not glamorous work, but it's necessary if you want the illusion to hold up.
Streaming and Emotion Injection
To avoid the player staring at a blank screen while the model generates a full response, I used streaming. Tokens appear as they're generated, so the conversation feels responsive even on slower hardware.
NPCs also express emotions and perform actions through their responses. The model outputs inline tags
like [emotion:happy] or [action:look_away] embedded in the text. These get
parsed out before the dialogue reaches the player, but the NPC reacts to them: changing facial
expressions, playing animations, shifting body language. From the player's side, the NPC just
feels alive. They don't see the markup, only the result.
NPC Navigation
For pathfinding, I used Unity's NavMesh system with dynamic baking. NavMesh was a natural fit here because it handles the complexity of an open city (sidewalks, crosswalks, building interiors, elevation changes) without me having to hand-author paths for every NPC. The navigation mesh updates at runtime, so if something in the environment changes, NPCs adapt their routes accordingly.
What I didn't want was NPCs wandering randomly like they do in most games. Citizens stick to sidewalks, wait at crosswalks, take logical routes between locations, and generally move like people who actually know where they're going. The result is a city that feels inhabited rather than populated with random walkers.
Investigation Tools
With fully open-ended AI dialogue, players need a way to keep track of what they've learned. The in-game notebook lets you jot down clues, map out relationships, and cross-reference alibis. When every NPC can say something different every time, having a reliable way to organize information becomes essential.
Making an Accusation
Once you think you've found the killer, you head to the police station and name your suspect. Get it wrong, and the real killer walks free. You're back on the streets digging for more evidence. There's no hand-holding here.