Year 2, Term 4, 2025
Hellhound's Trail
A horror game built on Twente folklore, where the route through the level is generated at runtime and the map is an object you hold.
- Unity
- C#
- Team of 6
- 8 weeks
- Programmer, procedural paths, flashlight, map and player systems
What it was, and what I owned
An 8-week client project made by a team of six: a first-person horror game drawn from Twente folklore, where the player is hunted through the woods by the Witte Wieven. My work was four systems, attributed by git blame across a 485-commit repository.
The largest is procedural path generation. The level is not a fixed route. At runtime the generator walks a set of unvisited areas, and at each step chooses between a left and a right candidate waypoint, recording the choice so the shape of the route is known rather than emergent. Two constraints keep the result playable: the generator will not repeat the same turn direction twice in a row, and it rules out any candidate that would leave no further path forward, so the route neither zig-zags on repeat nor walks itself into a dead end. Once an area is consumed its entry waypoints are removed, so the path cannot double back into a section the player has already cleared. Sections are enabled progressively as the player advances instead of the whole route existing at once, and a generation failure raises an explicit event rather than dropping the player into a level with no way forward.
The flashlight is the player's only weapon and only liability. It is a raycast that damages the Witte Wieven within range, gated behind a cooldown that is exposed as normalized progress so the HUD can render it honestly, with a flashbang burst as the heavier, slower option.
The map is diegetic. It is an object the player raises and lowers, not a fullscreen overlay, so consulting it never pauses the game and never removes the threat from the screen. That is a horror design decision enforced in code.
Attribution
Team project, 6 contributors, 485 commits, 125 mine. Line attribution by git blame: procedural paths 583 of 586 lines, diegetic map 219 of 280, player systems 413 of 635, flashlight 167 of 372. No AI involvement.
The code that makes the claim checkable
void DecideNextWaypoint(KeyValuePair<PathWaypoint, bool> leftPair, KeyValuePair<PathWaypoint, bool> rightPair)
{
//Debug.LogFormat("Choosing next waypoint between {0} and {1}.", leftPair.Key.name, rightPair.Key.name);
if (!CheckValidity(leftPair) || CheckLastEntryWaypoint(rightPair.Key))
{
//Debug.LogFormat("Forcing direction right. Visited: {0} Path status: {1}", leftPair.Key.visited, leftPair.Value);
generateRight = true;
}
else if (!CheckValidity(rightPair) || CheckLastEntryWaypoint(leftPair.Key))
{
//Debug.LogFormat("Forcing direction left. Visited: {0} Path status: {1}", rightPair.Key.visited, rightPair.Value);
generateRight = false;
}
// If the direction isn't forced we randomise it.
else
{
generateRight = RandomisePathDirection();
//Debug.Log("Going right? " + generateRight);
}
// Add the direction to the list after any potential forcing, so the accurate version gets added.
pastDirections.Add(generateRight);
prevWaypoint = currentWaypoint;
if (generateRight)
{
currentWaypoint = rightPair.Key;
// Remove the waypoint that wasn't chosen from the list of entry waypoints, because we assume that we can't reach that waypoint anymore.
RemoveEntryWaypointFromArea(leftPair.Key);
}
else
{
currentWaypoint = leftPair.Key;
RemoveEntryWaypointFromArea(rightPair.Key);
}
}The branch point of the route generator. Each step picks between a left and a right candidate and records the choice, ruling out a repeat of the last direction and any candidate that would dead-end the route, so the generated path is a constrained decision history rather than a random walk nobody can reason about afterwards.
Systems deep-dive
The generator treats the level as a set of CapsuleCollider areas, each holding entry waypoints. It walks from area to area, and at each step evaluates a left and a right candidate waypoint before committing to one and appending the choice to a pastDirections list.
Two rules keep the walk from degenerating: it will not choose the same direction it just chose, and it discards any candidate that would leave no further unvisited area reachable. Together they keep the generated route readable as a path instead of a random walk, and keep it from softlocking on its own dead end.
Consumed areas are removed from the unvisited set and their entry waypoints are stripped, which is what prevents the route folding back onto itself. In a horror game a path that revisits a cleared section is worse than a bad path: the tension depends on the player believing they are moving forward.
Sections are enabled as the player reaches them rather than all at once, which keeps the active object count down in a wooded scene. Three static events, OnPathGenerated, OnPathObjectEnabled and OnPathFail, let the rest of the game react without holding a reference to the generator. The failure event is the important one: generation can fail, and a horror level with no route is a softlock, so failure is signalled rather than swallowed.
Unity does not serialize Dictionary<,>, and the generator's area-to-waypoint mapping needed to be visible in the inspector for level designers to check. That is why UDictionary.cs is in the project, a serializable dictionary adapted from a public implementation rather than written from scratch.