← Back to Portfolio

Project Dunwell

A neo-noir murder mystery. One suspect. Nine clues. Three endings. Built in Unity + Ink.

Engine Unity
Language C#
Timeline Solo Project
Status Shipped

Project Dunwell is my latest shipped title, designed as a neo-noir sci-fi murder mystery I built in Unity with Ink handling all the branching dialogue. You play Marcus Dunwell, not a detective, but an engineer who will find himself as the prime suspect. Summoned by the victim for Dunwell's history in infrastructure, each piece of evidence has his name and fingerprints on it, all captured with a surveillance system he designed.

Project Dunwell was a medley of new challenges for me. From truly designing the whole world, aesthetic, game feel, down to the minute details of writing the story. I think the true challenge was learning to fire the proper Ink knot with the correct timing, which meant if a clue was discovered, the script would play and I'd need to recognize where the story had to pick back up from. Then it becomes how do I avoid interruptions, how can I cut blank lines that arise from tagging knots in Ink, and how can I avoid repeating the same monologue script or line to avoid losing the linear story. Learning Ink was something in of itself too which included learning its language, the syntax and grammar of how to properly code what I wanted the story to tell.

Core Problem

Dunwell needed two systems to stay completely decoupled: Unity owning the world — collision, player state, clue tracking — and Ink owning all narrative output. The challenge was making them talk to each other without either one knowing too much about the other. Specifically: how do you fire a narrative knot from a collision trigger without interrupting story state? How do you pass game events into Ink without hardcoding them? And how do you suppress the blank lines Ink outputs between logic-only lines so nothing bleeds into the UI? These aren't theoretical problems. They blocked progress until the architecture was right.

The answer was building an InkBridge right into my GameManager.cs — a custom messenger layer sitting between Unity and Ink. Working with this new system for dialogue and narrative application, my natural instinct was to brush up on their documentation. However with this documentation, I found that the tests I was running in the compiler weren't one hundred percent accurate with what was supposed to be helping me understand it's features. Reiterative tests, gate building in C# with Unity to advance the dialogue along became my source of truth. If I fire this knot, based on this object's script variable, and only when I collide with this clue, should this line of the story appear.

Unity calls knots by name when triggers fire. Ink delivers text and tags. The game manager reads those tags and fires Unity events in response: updating UI, triggering fades, locking the case board during critical beats, enabling the building manager encounter. Neither system reaches into the other directly.

public void AdvanceDialogue()
{
    if (_inkStory.canContinue)
    {
        string line = _inkStory.Continue().Trim();

        // Skip blank lines automatically
        if (string.IsNullOrWhiteSpace(line))
        {
            AdvanceDialogue();
            return;
        }
        typeWriter._readyForNewText = true;
        typeWriter.PrepareForNewText(dialogue);
        dialogue.transform.GetChild(1).GetComponent().text = line;
        //_inkStory.Continue();
        dialogue.SetActive(true);

        List tags = _inkStory.currentTags;
        foreach (string tag in tags)
        {
            if (tag.StartsWith("SCENE_"))
            {
                string sceneName = tag.Substring(6); // Extract the scene name after "SCENE_"
                                                     // Load the scene using SceneManager.LoadScene(sceneName);
                Debug.Log("Scene change triggered: " + sceneName);
                AdvanceDialogue(); //UI will produce a blank line, so we need to advance the dialogue again to skip it
            }
            if (tag.StartsWith("CONVO_"))
            {
                AdvanceDialogue();
                dialogue.SetActive(false);
                // Load the scene using SceneManager.LoadScene(sceneName);
                manager.GetComponent().Restore();
                Debug.Log("Conversation is Done ");
                //ui will produce a blank line, so we need to advance the dialogue again to skip it
                continue;
            }
            if (tag.StartsWith("CLUE_"))
            {
                string clueName = tag.Substring(0); // Extract the clue number after "CLUE_"
                                                    // Load the scene using SceneManager.LoadScene(sceneName);
                Debug.Log("Clue found triggered: " + clueName);

            }
        }
    }
    else
    {
        dialogue.SetActive(false);
    }

}

Other challenges that arose included Clue collection, inspection, clue card creation, connection mapping and how the story reacts to each. Now during clue collection, the first thing that had to be decided was what's important to the case, what's just world design and lore, and what's just junk someone in this world might just glance at. Using booleans to flag case-relevant objects versus world dressing, and item titles to surface context helped track what to tell the player about Sificity, and what Dunwell's behavior could reveal. Once a relevant clue to the case was found though, it was up to the player to see what connections they could draw on the caseboard to help unveil more of what happened in this locked room mystery. For me, finding the clues, making the connection and further discovering the story made building the rest of the game a lot more engaging and fun to work on.

What worked, and what I'd do differently.

What Worked
  • Every physical clue in the scene is inspectable. Walking up to one and interacting switches from third-person exploration to a first-person inspection camera, you're looking through Marcus's eyes, rotating the object in world space with your mouse.
  • Dialogue box with type writer effect so the story always comes in like a transmission in that scientific style and design.
  • Connecting clues on the case board isn't cosmetic — it fires narrative. Each connection is backed by a dictionary keyed on a sorted clue-pair string (so "clue_2_clue_5" and "clue_5_clue_2" resolve to the same entry), which maps to a unique Ink knot. Draw the line, Marcus reacts.
  • The system saves the object's original transform on entry and restores it exactly on exit, so nothing stays displaced after Marcus puts it down. The camera swap is seamless: no loading, no UI transition, just a perspective shift that makes examination feel deliberate rather than mechanical. It's a small thing, but it does a lot for making Marcus feel like someone who actually looks at things.
What I'd Change
  • Could work on better input handling and game state management to look cleaner. Trying to debug was a nightmare of figuring out where something went wrong as I cleaned up endings and inspections.
  • Would love to add a population to the city to allow for better gamefeel instead of just empty streets. Could use better Nav Agent for building manager too, but he's a good start for things to come.
  • The bluff mechanic uses a single inverted boolean — it defaults to true so players who never encounter the manager don't accidentally trigger Ending B. It works, but it's fragile. The cleaner version is two explicit flags: bluffAttempted and bluffFailed. That's the first refactor on the post-ship list.
  • The case board spline is hardcoded to 1920px width. It holds up on standard displays but breaks at other resolutions. Anchoring the spline endpoints to the canvas or switching to a layout group fixes it properly.