Year 3, 2026
Software Architecture V2
An event-driven Unity action RPG with 181 tests and 27 editor tools, built AI-assisted and disclosed per session.
- Unity
- C#
- Solo
- One semester
What it was, and what I owned
A single-player action RPG built to be argued about rather than just played: every system has a seam, and every seam has a test behind it. Combat, questing, inventory, skills, spawning and the boss fight all communicate through a generic event bus instead of direct references, so a system can be replaced without touching the ones that listen to it.
The part worth looking at is not the game. It is that time and randomness are constructor arguments. Four core systems, the boss chase state, the loot dropper, the skill manager and the enemy spawner, take Func<float> providers rather than reading Time.time or Random.value directly. That single decision is what makes boss phase timing, loot roll distribution and spawn cadence assertable in an EditMode test instead of something you watch for and hope.
On top of that sits a 27-script editor layer, 2,872 lines, that builds prefabs, scenes and ScriptableObjects through PrefabUtility, SerializedObject and AssetDatabase.CreateAsset. Every one is idempotent and menu-registered, so running a tool twice never duplicates an asset. The content in this project was largely generated by tooling I wrote, not placed by hand.
The code that makes the claim checkable
public static void Publish(T evt)
{
// Snapshot the list so a handler that unsubscribes itself (or another handler)
// mid-publish does not invalidate enumeration.
var snapshot = handlers.ToArray();
for (int i = 0; i < snapshot.Length; i++)
{
snapshot[i]?.Invoke(evt);
}
}Publishing iterates an array snapshot, so a handler that unsubscribes itself or another handler mid-publish cannot invalidate the enumeration. The reentrancy case has an actual test rather than a comment promising it is fine.
private readonly Func<float> now;
private readonly Func<float> random01;
private readonly INavMeshSampler sampler;
private readonly EnemyFactory factory;
private readonly int maxAttempts;
private readonly float sampleDistance;
public Core(Func<float> now, Func<float> random01,
INavMeshSampler sampler, EnemyFactory factory,
int maxAttempts, float sampleDistance)
{
this.now = now;
this.random01 = random01;
this.sampler = sampler;
this.factory = factory;
this.maxAttempts = Mathf.Max(1, maxAttempts);
this.sampleDistance = Mathf.Max(0.01f, sampleDistance);
}Time and randomness arrive as Func<float> providers, so a test drives the clock and the dice directly. This is the seam that makes spawn cadence and loot rolls assertable instead of observable.
Systems deep-dive
EventBus<T> is a static generic: each closed type, EventBus<PlayerHPChangedEvent> and so on, owns its own handler list, so there is no string keying and no runtime type lookup. Subscription happens in OnEnable and unsubscription in OnDisable, which is the Unity lifecycle pairing that survives scene reloads.
Publishing copies the handler list to an array before iterating. That is the difference between a bus that works and one that throws the first time a handler removes itself in response to the event it just received, which in an RPG happens constantly: a dying enemy unsubscribes from the damage event that killed it.
Systems talk through interfaces rather than concrete types, so the tests can substitute fakes. FakeBossContext, FakePhysicsLineCaster and FakeSkillContext are the three shared stubs the EditMode suite is built on.
AI provenance
This project was built AI-assisted. Here is the split, in full.
- What the tool did
The typing. Claude Code wrote most of the implementation, the test scaffolding and the editor scripts, directed slice by slice, with a per-session disclosure recording what the tool did and what it explicitly did not.
- What I did
Which cases were worth covering and why the seams exist at all. The architecture decisions, the design of how the boss should feel to fight, review of every assertion, the scene and editor work, the bug fixing, and running the suite. The 181 tests are not decoration: they are the verification layer that makes accepting generated code defensible. Without them I would be trusting a read-through, and generated code is specifically good at surviving a read-through.