Code Organization

Abstract

Problem: How should game code be structured, particularly for a system like inventory that touches mechanics, UI, and multiple game objects?

Approach: Tim Cain walks through his preferred architecture using inventory as a concrete example, demonstrating a clean separation between system mechanics, components, and user interface layers.

Findings: By keeping mechanics completely ignorant of UI, using a component-based architecture, and communicating state changes through events, you get code that is clean, modular, debuggable, and naturally maps to how design documents are written.

Key insight: Mechanics should never know about UI. The UI knows about mechanics and calls into them, but mechanics only return results and fire events — they never reach up into the display layer.

Source: https://www.youtube.com/watch?v=Zzo5JTY8zjg

The Golden Rule: Separate Mechanics from UI

Tim's number one principle: keep system mechanics separate from UI. No matter what system you're building, the mechanics layer should have no idea how it's being displayed on screen. Making mechanics care about display only makes your code more complicated.

He structures every system in three layers:

  1. Mechanics — the pure game logic
  2. Components — how mechanics attach to game objects
  3. UI — how the player sees and interacts with the system

Inventory Mechanics

The mechanics layer for inventory is deliberately simple. It contains:

Core Data

A list of items. The storage implementation doesn't matter — linked list, array, resizable array — that's an implementation detail.

Core Functions

  • Add Item / Remove Item — These always succeed (they're void-returning). You pass an item, it goes on or comes off the list.
  • Can Add / Can Remove — These are the gatekeepers. They return true/false and, on failure, return a failure code explaining why (no space, too heavy, quest item can't be removed, cursed item can't be removed, etc.).
  • Equip / Unequip (to a slot) — Same pattern: the action functions always succeed, and companion "can equip" / "can unequip" functions handle validation with failure codes (cursed item can't be unequipped, class restriction, etc.).

Utility Functions

Convenience queries that live in the mechanics layer:

  • Total weight of equipped items
  • Total weight of all carried items
  • Total value of all items
  • Total value of items by type (weapons, armor, etc.)

What About UI Data on Items?

Items may carry UI-related data (like grid position in a grid inventory), but the mechanics layer never accesses or understands that data. It sits on the item alongside weight and flags, but mechanics ignores it completely.

The Component Pattern

Inventory becomes a component that can be attached to different game objects:

  • Containers — shelves, chests, loot drops. The container handles lockability and keys; inventory just handles the items inside.
  • Creatures — the player character, NPCs you can pickpocket, vendors you trade with.
  • Vendors — may use separate inventory components for "items for sale" vs "items being carried" (which is why you sometimes find a hidden chest containing a shopkeeper's entire sellable inventory).

When an object receives an inventory component, it initializes it with configuration: weight limits, item count limits, and crucially, which equipment slots are active. A container tells its inventory "I have no equip slots," so canEquip always returns false. This keeps the inventory system general while letting each object customize its behavior.

The UI Layer

The UI knows about mechanics; mechanics knows nothing about UI. The UI programmer (often a specialist on larger teams) works against a clean API.

UI-Specific Logic

Things the UI handles that mechanics doesn't care about:

  • Grid placement — Can the item be dropped on this empty slot?
  • Drag-and-drop behavior — Should dropping on an occupied slot swap items, displace them, or reject the drop?
  • Auto-sorting — Rearranging items by type, which updates grid positions (a UI-only concept)
  • Visual feedback — Everything the player sees

The UI calls canAdd first, and only if mechanics approves does it proceed with add and update the visual state.

Events: The Communication Bridge

The trickiest architectural question: how does the UI learn about things that happen in the mechanics layer without mechanics knowing about UI?

Tim's answer: events.

The Encumbrance Example

When an item is added and the player's weight exceeds the encumbrance limit, the mechanics layer fires an event: "encumbrance limit exceeded on player." It doesn't know or care who's listening.

  • The inventory UI (if open) might listen and show a visual indicator
  • The HUD might listen and display "You are now encumbered"
  • Events can be consumed — if the inventory UI handles it, it can eat the event so the HUD doesn't also display a redundant message

Why Events Are Powerful

Without events, every place in the code that calls add — inventory UI, a "pick up all" button, a script, a dialogue that gives you an item — would need to manually check encumbrance and display a message. With events, you handle it once at the listener level.

Debugging becomes straightforward too. Bug report: "I picked up items that were too heavy and didn't see a message." Fix: the HUD wasn't registered as a listener for the encumbrance event. Add it as a fallback listener, done.

Why This Architecture Works

  • Clean mechanics that closely mirror design documents
  • Clean UI that handles only display and interaction concerns
  • Easy debugging because responsibilities are clearly separated
  • Flexibility — different objects customize behavior through component initialization, not by forking the inventory code
  • Team-friendly — UI specialists and systems programmers work on separate layers with a clean API boundary

References