Case study · Accessibility · Voice interaction · Game development

Dik-dik

A game about a rover that only acts when spoken to, built so that voice is never the only way to play.

Unity and C# · whisper.cpp, on-device · MIT licensed · Solo project

AccessibilityVoice interactionUnityC#

The game

You are the voice on a rover’s radio. It is fully capable and completely still, and it stays still until it is addressed. You drive it along a relay line and scan each section until you find the break.

Nothing can be failed. There are no lives, no timer and no score. The worst thing that can happen is that something takes longer.

Dik-dik source on GitHub

The argument

A game decides who gets to play the moment it decides how it must be played. That decision is usually invisible, made early by someone who assumed one kind of body and one kind of voice, and by the time anyone notices, the fix costs a rewrite.

So the rover is not broken. The disability is not in the machine. It is in the gap between what the machine can do and how it is allowed to be asked.

How it is built

Speech recognition runs on the player’s own machine, through whisper.cpp. There is no cloud dependency, no account, and no audio leaves the computer. Audio is transcribed and dropped inside the method that receives it. Nothing stores it or writes it to disk.

A nineteen-intent matching layer turns a transcribed sentence into a command. People say “okay go ahead now”, not “go”. The layer normalises, strips politeness, matches whole phrases, then falls back to edit distance. It has no Unity dependency, so it is tested with plain dotnet in about a second, independently of the engine.

Keyboard parity is enforced at the architecture level. Voice and keyboard both implement one ICommandProducer interface and both feed one command bus. Everything downstream receives an intent and cannot tell which input produced it. No gameplay rule may branch on the source. The working rule is that a command is not finished until every input can raise it, and both ship in the same commit or the feature is not done. Voice is an alternative input, never a requirement.

Scenes are generated by code, not placed by hand, and validated by build-time assertions. A hand-placed scene is a binary blob nobody can review in a diff, and the values that matter here, a probe distance, a checkpoint spacing, a transport delay, are exactly the ones that go wrong silently.

Accessibility

Built against the Game Accessibility Guidelines, with the guideline titles quoted rather than paraphrased, because a paraphrase is where an honest claim quietly becomes a flattering one.

Implemented: subtitles for all important speech, no essential information carried by colour alone, a high contrast option, remappable controls, settings that persist and are available from launch rather than unlocked by progress, adjustable game speed, separate volume for speech and world, speech input that is supplementary rather than required, and a small vocabulary rather than free dictation.

Two things are only partial and two are not implemented at all, including screen reader support. They are listed as not implemented, because claiming a partial implementation would be worse than admitting none.

The piece I care about most is voice remapping. A keyboard player rebinds a key. A voice player should be able to rebind a word, including a sound that is not a word.

Latency, which is the real problem

There is no such thing as zero-latency voice. Measured on my machine, roughly 1.9 seconds pass between the end of speech and a resolved command. Most approaches attack the delay itself, which is an arms race a local model cannot win and which never reaches zero anyway.

The approach here is the opposite: accept the latency and remove its cost. Speech carries two signals, not one. That someone is speaking arrives in about 100 milliseconds. What they said arrives in about 2000. Most systems discard the first and wait for the second. Here, the instant voice detection fires, before any transcription exists, the rover eases off. It does not guess what you said, because guessing wrong is expensive. The principle is to anticipate only with actions you cannot be wrong about. Slowing down costs nothing if the command turns out to be “left”.

I had the prior art checked before claiming anything, and the honest framing is transfer from network latency compensation rather than invention. One of the three design decisions here is one the literature has already named and partly argued against. It stays in the game as a decision rather than a contribution, and it is on the list to actually test.

Where it fell over

The repository is candid about this, and so is this page.

An accent-specific finding I did not hide. Whisper’s tiny model consistently transcribes my “turn” as “10”. Six occurrences in one sitting, in my own Malawian English. This is what the research predicts: evaluations find Whisper performs worse on African-accented English, and the tiny model is among the worst offenders. It is the documented reason the vocabulary here is nineteen intents rather than free dictation. I could have hidden this by seeding the model with my vocabulary and re-running until the number flattered me. That seemed a poor way to open a project about being heard.

The smoke test is a smoke test. Thirteen of sixteen utterances matched the intended command, against a threshold I set before running it. One speaker, one microphone, one room, one afternoon. It told me whether to keep going. It is not an evaluation of anything, and it took four sessions to get one honest number, because the first three were destroyed by my own bugs.

The weakest part of the stack is my code, not the model. In the session that diagnosed everything, the recogniser transcribed roughly fifteen of seventeen utterances correctly and my matcher then discarded six of them. “Start moving” resolved to stop, because it is three edit-distance steps from “stop moving”. A sentence landing on its own opposite, through a door I built myself.

A feature I built, played and deleted. The ending used to be your own voice, every command you had given, played back. It worked. Then I played it and it felt like being watched, because a microphone does not record commands, it records a room. So the retention went, not just the ending.

Four bugs shipped that compiled cleanly and looked right in screenshots, including one level that was mathematically inescapable by 31 centimetres. That is why there are now assertions inside the scene builders that fail the build, and why I verify build artefacts rather than exit codes.

AI assistance

I used Claude heavily on this project: architecture, C# I would have written more slowly and worse, the test suite, and the repository documentation. I am not going to hide that in a portfolio piece aimed at people who study interaction.

Getting help has always been part of this work. Reading Stack Overflow. Importing an open source library somebody else wrote with care. This is a difference of degree rather than of kind, and the interesting question is not whether to use the tools but what changes when everyone knows you did.

That question is also my thesis finding, and it landed here in practice: once AI use is openly acknowledged, attention moves off the fact of use and onto the dynamics it creates. Accountability, attribution, and verification of outputs. The speed is real. It moves the bottleneck to checking, and checking is not free. Almost every verification habit in this project exists because something plausible was produced quickly and turned out to be wrong in a way only measurement caught.

What I would do next

Non-verbal input is the one that matters: record any sound at all during remapping, a hum, a whistle, a click, and map it to a command, so the game becomes playable without a single word. After that, a latency study with a dial from 100 to 2000 milliseconds, to ask the question nobody has answered for voice-controlled vehicles: how much latency can a player tolerate, and which compensations help at which point on that curve.

← Back to all work