A tester sends you a clip: "the fire sword does nothing now." You open the project and find SwordFire_02. You open the balance sheet and find item_sword_fire. You open the design doc and find "Ember Brand." The localization file has WPN_FIREBLADE_NAME. Four names, one object, and twenty minutes of grep before you can even start looking at the bug.
Naming is the least glamorous thing in a design document and the one that quietly costs the most. Every cross-reference in your project — a loot table pointing at an item, a dialogue node pointing at a character — is a name someone typed from memory. Get the convention right in week two and it holds for the whole project. Get it wrong and you pay a tax on every lookup for three years.
Two names per thing, and only one of them is yours to change
The single most useful rule: separate the stable ID from the display name.
| Role | Example | Who changes it | When |
|---|---|---|---|
| Stable ID | wpn_sword_ember |
Nobody, after creation | Never |
| Display name | "Ember Brand" | Writers, marketing | Any time |
| Loc key | wpn_sword_ember.name |
Derived from the ID | Never |
| Engine asset | SM_wpn_sword_ember |
Artists, with the ID intact | On re-export |
| File on disk | wpn_sword_ember.json |
Nobody | Never |
The ID is a machine handle. It exists so that a loot table, a shop inventory, an achievement condition and a save file can all point at the same row without caring that your sword got renamed from "Flame Tongue" to "Ember Brand" to "Cinderbrand" over eighteen months. The display name is content: it should be free to change, and it should live in exactly one place, ideally the localization table, keyed off the ID.
The mistake almost everyone makes once is building the ID out of the display name and then renaming the display name. Now wpn_sword_flametongue shows up in your logs as "Cinderbrand", and every new hire loses an hour to that.
A grammar you can type from memory
A convention nobody can recall is a convention nobody follows. Use one pattern, three or four segments, lowercase snake_case:
<domain>_<subtype>_<subject>[_<variant>]
| ID | Reads as |
|---|---|
wpn_sword_ember |
weapon, sword, Ember Brand |
enm_grunt_spearman_elite |
enemy, grunt class, spearman, elite variant |
lvl_mine_02 |
level, mine biome, second map |
npc_elara |
character, Elara |
qst_mine_rescue |
quest, in the mine, the rescue one |
sfx_ui_confirm |
sound effect, UI, confirm |
vo_elara_ch03_012 |
voice line, Elara, chapter 3, line 12 |
Rules that are worth being strict about:
- Lowercase, underscores, ASCII only. No spaces, no hyphens, no accents, no apostrophes. These strings end up in file paths, URLs, shell commands and JSON keys. "Zoë's Café" is a display name, never an ID.
- Zero-pad numbers, and pick the width once.
lvl_mine_02sorts correctly;lvl_mine_2sorts afterlvl_mine_11. Two digits caps you at 99 — fine for levels, not for voice lines. Use three for anything you expect to exceed 50 (dialogue lines, items, localization strings). - No status words.
_final,_new,_v2,_temp,_fix,_realall become lies within a week. Status belongs in version control and your build-tracking notes, not in the name. - No initials, no dates.
sword_mk_octtells you who made it and loses you the ability to sort. Attribution is in the commit log. - Prefix by domain, not by project. Nobody needs
mygame_on every row.
Engine-side, keep the ID inside the asset name and let the engine's own prefix convention sit in front of it. Unreal projects commonly use type prefixes such as SM_ for static meshes, T_ for textures and M_ for materials; T_wpn_sword_ember_D is readable, sortable and still greppable by the design ID.
Keeping IDs stable when the design is not
Design changes. IDs should not:
| Design change | What happens to the ID |
|---|---|
| Renamed in fiction | Nothing. Change the display string only. |
| Stats rebalanced | Nothing. It is the same object. |
| Cut from the game | Mark status: retired. Do not delete the row and never reuse the ID. |
| Split into two | Keep the ID for the closer descendant, create one new ID. |
| Two merged into one | Keep one ID, retire the other, record the mapping. |
| Changed category (item becomes a spell) | Keep the ID even though the prefix is now wrong, or do a real migration. Do not half-rename. |
Retired-not-deleted matters more than it sounds. Old save files, old playtest telemetry and old bug reports all reference IDs that no longer exist. If you keep retired rows with a status flag, a save from build 0.4.2 still loads and your telemetry from three months ago still joins against your item list. If you delete them, you get null-reference crashes in the oldest saves — exactly the ones from your most patient testers.
Two places where names become genuinely permanent: save-game keys and store-platform API names. Steamworks achievement and stat API names are effectively frozen once players have unlocked them — change one and you have orphaned people's progress. Decide those names in a dedicated pass before your first public build, not in a hurry during Steamworks prep, and write them down in the same table as everything else.
A rename is a migration, not an edit
When you genuinely must rename an ID — and it does happen, usually because the first one was wrong — treat it as a code change:
- Confirm nothing shipped with the old ID in a save file or platform API.
- Grep the entire project, including data tables, dialogue, loc files and scripts.
- Rename in the canonical source first (your design data), not in the engine.
- Add a mapping row:
old_id → new_id, with the date. - Update loc keys and ship a loc-key alias if translators are mid-pass.
- Run the game and load an old save.
- Note it in your decision log so nobody re-litigates it in four months.
If that feels heavy for one sword, that is the point: it is cheaper to argue about the name for ten minutes up front.
Catch drift with a ten-minute audit
Conventions rot silently. One person adds Enemy_Spider_Boss, nobody notices, and a month later there are nine of them. Make the audit mechanical and run it at every milestone:
- Export the canonical ID list from your design data. If your docs live in GameDesignerX, the items, characters and locations modules hold the canonical rows, and the REST API or MCP server will dump them as JSON so you can diff them against the project without copying anything by hand.
- Export the asset and data-table names from the engine.
- Diff the two lists. Three buckets: in docs but not in build (unimplemented or retired), in build but not in docs (undocumented content), name mismatches (drift).
- Regex the combined list against your pattern and print the violations.
A twenty-line script that prints "7 IDs do not match the pattern" is worth more than a style guide nobody reads.
The one-page convention doc
Keep it to a single page and lead with examples rather than rules — people copy the nearest example. It needs the pattern, the domain prefix list, the number-padding rule, the forbidden-words list, the ID-vs-display-name rule and the rename procedure. Put it where new contributors trip over it: next to the data schema, not buried in a wiki.
Checklist before you name 400 things
- Domain prefix list agreed and written down (
wpn,enm,lvl,npc,qst,sfx,vo,ach) - Number width chosen per domain (2 for levels, 3 for dialogue and items)
- Display names live only in the localization table
- Retired rows keep their IDs with a status flag
- Achievement and save keys decided in a single pass before first public build
- Rename procedure documented, with a mapping table
- Audit script runs at every milestone
None of this makes your game better on its own. It makes every other improvement cheaper to find, cheaper to reference and cheaper to hand to someone else — which, on a three-person team shipping over two years, is roughly the same thing.