Back to Blog
data-drivenworkflowbalancingproduction

Tune It in a Table: Designing a Content Schema Your Team Can Edit Without a Programmer

Your game's balance will only be as good as your iteration count. Here's how to design the data schema and pipeline that turns a two-day tuning round trip into a four-second one.

GameDesignerX TeamOctober 24, 20267 min read

A designer asks for a small change: the shotgun should fire 8 pellets instead of 6. On a well-built project that is one cell in a table, saved, and visible in the running game four seconds later. On a badly built one it is a Jira ticket, a programmer's afternoon, a recompile, a merge conflict in a .uasset, and a two-day round trip — for a change that will probably be reverted after the next playtest.

The difference is not engine choice or team size. It is whether someone sat down early and designed the content schema: the shape of the data that describes your items, enemies, abilities and levels, and the pipeline that gets it from a table into the build. This is one of the highest-leverage days of work in pre-production, and almost nobody schedules it.

What a content schema actually is

A content schema is the answer to three questions, written down before you author 300 items:

  1. What kinds of things does the game contain? (entity types: weapon, enemy, ability, consumable, room, dialogue node)
  2. What fields does each kind have, and what are the legal values? (types, ranges, required vs optional, references to other entities)
  3. Where does that data live, who edits it, and how does it reach the build?

Everything painful about content production — duplicated numbers, enemies that reference a deleted item, a balance pass that takes three weeks — traces back to one of these three being improvised.

The test for "is this content or is this code?"

A useful rule: if a non-programmer would ever want to change it during a balance pass, it is content. Damage, cooldowns, prices, drop weights, aggro radius, XP curves, text, portraits, sound IDs — content. The code that applies a damage-over-time tick — code.

The grey zone is behaviour. Resist the urge to build a scripting language in your spreadsheet. The pattern that holds up is: programmers implement a small set of named behaviours; designers compose them by ID.

ability_id    name          behaviour     power  duration_s  cost  cooldown_s
abl_firebolt  Firebolt      projectile    18     0           12    1.5
abl_ignite    Ignite        dot_apply     4      6           20    8.0
abl_dash      Combat Dash   blink_forward 0      0.18        0     4.0

behaviour is an enum with maybe twelve values in a shipped indie game. Twelve behaviours × tunable parameters gives you hundreds of abilities. Three hundred bespoke ability classes gives you a maintenance problem.

Designing the fields

Start with one entity type and write the columns out by hand before you build anything. For a weapon in a shooter you might land on:

Field Type Rule
id string immutable, lowercase, prefixed (wpn_)
display_name string localisation key, never shown raw
damage float 1–200
pellets int 1–12, default 1
rpm int 30–1200
mag_size int 1–200
reload_s float 0.3–6.0
ammo_type ref → ammo must exist
rarity enum common/rare/epic
unlock_level int 0–50

Four conventions in that table matter more than the specific fields.

IDs are permanent and human-readable

wpn_shotgun_pump beats 42 and beats Pump Shotgun. Numeric IDs make diffs unreadable and make it tempting to renumber. Display names change constantly — if your save files or quest data reference display names, renaming a weapon corrupts saves. Pick the prefix convention on day one: wpn_, enm_, abl_, loc_, itm_. When you grep the project for wpn_shotgun_pump you want every reference, including the one in a level script you forgot about.

Every number gets a legal range

Ranges are not bureaucracy; they are the difference between "the game feels off" and "someone typed 180 into reload_s". A validator that rejects out-of-range values catches the typo at save time instead of in a playtest two weeks later.

References are typed and checked

ammo_type pointing at a deleted ammo row should fail the build, loudly, with the row number. The single most common content bug on small teams is a dangling reference that silently resolves to null and produces an enemy that does no damage.

Defaults keep the table narrow

If 90% of weapons fire one pellet, pellets defaults to 1 and only the shotguns fill it in. A table where every cell is populated is a table nobody can scan.

Derived values belong in formulas, not columns

The trap that kills balance passes: storing dps as a column. Someone changes rpm, forgets dps, and now two numbers disagree and nobody knows which one the game uses.

Store inputs. Compute outputs. In a balance sheet, dps is a formula cell: damage * pellets * rpm / 60. In the engine, it is a function. It is never authored data.

The same applies to enemy "power level", item "score", and any rating you use for loot tables. The moment a derived value becomes editable, your data has two sources of truth.

The pipeline: table to build

A workable pipeline for a team of two to eight:

  1. Author in a sheet. Spreadsheets win because designers already know them, they diff as CSV, and sorting and filtering are free. GameDesignerX's balance and economy sheets fill this role if you want the data living next to the rest of your design docs rather than in a stray Google Sheet nobody can find.
  2. Export to a flat text format — CSV or JSON — committed to the repo. Text diffs in code review; binary engine assets do not.
  3. Validate on export. Types, ranges, required fields, reference integrity, duplicate IDs. Fail with file and row.
  4. Import into the engine as a data table or ScriptableObject set, generated — never hand-edited downstream.
  5. Hot-reload in play mode if you can afford a day to build it. The payoff is enormous: tuning a jump arc with the game running collapses a 40-second iteration loop to 2 seconds.

If you want the data readable by other tools — a wiki, a Discord bot, an LLM assistant doing a consistency pass — a REST endpoint over the same source is worth having. GameDesignerX exposes one, plus an MCP server, which is how you get an AI assistant to answer "which enemies drop an item no vendor sells?" without you writing a script.

A validation checklist to steal

  • No duplicate IDs across the whole content set
  • Every ref field resolves to an existing row
  • Every numeric field inside its declared range
  • Every required field non-empty
  • Every display_name has a localisation entry
  • No orphans: every item is reachable from a drop table, shop or quest reward
  • No derived columns stored as data
  • Export is deterministic — same input, byte-identical output (otherwise every export is a 400-line diff)

That last one costs an hour and saves your code reviews.

When to do this

Do it after your first prototype proves the core loop and before you author the second dozen of anything. Too early and you are schematising a game that does not exist yet. Too late — around item 80 — and migrating becomes a week of work nobody wants to approve, so the team limps on with the improvised version until ship.

A good trigger: the first time someone copy-pastes an existing enemy definition and edits three numbers. That copy-paste is the schema asking to be written down.

The real payoff

Teams talk about data-driven design as an engineering concern. It is not. It is a design concern, because it determines how many balance iterations you can afford before launch. A team with a four-second tuning loop will run hundreds of passes on their combat numbers. A team with a two-day loop will run maybe six, and will ship whatever the sixth one produced.

Your game's balance will be roughly as good as your iteration count. The schema is what buys you the iterations.