Skip to content

What a build needs from a project

The authoring decisions that reach a compiler: codes that become C# names, what an unimplemented command does, option sets, and the start nothing will guess.

All documentation — Authoring for a game
On this page

Most of what an author does in the editor never reaches your build as anything but data. A short list of decisions does — and each of them has a consequence a writer cannot see from inside the editor.

Codes become C# names

Almost every code an author types becomes an identifier: node types, their fields, entities, variables, commands, tags, boards, option sets.

So the guidance to a writer is short: codes that look like identifiers produce game code somebody can read. Nothing judges whether a code reads well, so nothing will tell them otherwise.

Three consequences worth knowing precisely:

Two codes that differ only in case are one nameEvery generated name is made unique ignoring case, because a file name compares that way on Windows: of Ab and AB, the later generates as AB2
Node type codesThe generated class comes from a sanitised, uniquified name — never from the raw code
Field codes are unique per namespace, not per typeA type can carry the same code at its top level and inside a repeater. Both survive; they end up nested.

NodeTypes.Branch.Condition and NodeTypes.Branch.Conditions.Condition are two different fields — see the generated constants.

Entity codes

Two entities sharing a code resolve first-wins, with one duplicate-entity-code diagnostic at load. Nothing crashes and nothing is dropped; one of them is simply unreachable by code.

An entity’s id survives a rename and its code does not, which is why the payload and every save key on the id and carry the code alongside.

Input action codes

A record of a definition whose mentions draw as a keycap — the Puzzles & world pack’s Input action — is an input action, and its code is the one your game maps to its own action: Crouch to Player/Crouch in your Input Actions asset. EntityPalette.ResolveMention hands that code to your mention hook as MentionLook.ActionCode, and EntityDisplay.IsAction says a record is one. Renaming the record changes only the label a player reads; changing its code breaks that mapping, silently, in every prompt that names it. Its key name, pad control and touch gesture fields are previews the editor draws — the binding a player sees is your game’s. See an input action’s glyph.

Commands: optional to implement, never silent

Declaring a command in the editor creates a method in the programmer’s project, which they override to answer it. The build never breaks over a command nobody has implemented yet, and a command that runs unanswered always says so:

Declared asGenerated asLeft unimplemented
Ordinarypublic virtual void On<Command>(…)Runs, and warns once — command-not-implemented, naming the method
Awaitedpublic virtual void On<Command>Async(…, CommandCompletion completion)Fails the wait with an error naming the method, and the story carries on

An awaited command is failed rather than left waiting because an unanswered wait is not a no-op but an infinite hang — the story would suspend and nothing would ever settle the token.

Marking a command as awaited is a request to a programmer, not a setting — worth saying out loud before a project has fifty of them. Receiving one is implementing commands.

Option sets versus inline options

A select field can draw its values from a reusable option set, or carry its own options inline. Only the first produces a C# enum.

Inline optionsA reusable option set
Integer valuesNoneYes
Generated C#A plain stringA real enum
A variable of that typestringThe enum type — Mood.Calm

The payload carries the option set’s id and not its option list, which is why the mapping cannot be built for an inline select at all.

An option the project no longer declares misses to -1 for a plain enum — deliberately not any declared member. On a bitmask it misses to 0, because -1 there would be every flag set at once.

The start the engine will not guess

A project names a start board and, optionally, a start node.

Set the game start explicitly. It is one field, and it is the difference between a build that plays and a build that opens on whatever node happened to sort first.

Gates and requirements do not treat blankness the same way

Blank means
A gate on an exit the story takes by itselfFails. A gate that is present and empty is an unfinished edit on a path meant to be conditional.
A gate on an option the player picksPasses. An option has no else to protect, so an empty gate is no condition.
A requirement row on a nodePasses. An author who has not written the test yet has not blocked anything.

Both are deliberate and neither is configurable.

Two more, worth telling a writer once:

  • A node’s unmet requirements are hoisted onto every choice that leads to it, so the requirement is written once on the destination and every route in shows locked.
  • A jump bypasses them, because a jump is already an authored override of the graph.

Media filenames are addresses

A synced media file’s address is its path under the media root, without the extension. Renaming the file in the editor changes the address, and an entity prefab’s address is its filename.

Nothing breaks silently here — a missing address is reported — but it does mean the file names an author chooses are a public surface. Where synced media lands has the rest.

Rich text and plain text are different fields

A rich-text field and a plain-text field carry different things, and the conversion to something drawable is different for each. That is a programmer’s problem — FieldMarkup decides, see drawing a line of text — but it is worth knowing that the choice is made in the editor, and that a stage direction typed into a plain field can contain angle brackets safely while the same characters in a rich field are formatting.

Languages

The master language’s values live in the fields themselves; every other language lives in a translation bag. There is no runtime fallback chain — the generator fills a missing translation with the master language’s string at sync time.

So a partly-translated project ships and plays. It just plays with some lines in the master language, which is the right failure and is not reported as an error.

Written for
Unity developers · Advanced integrators
Last updated