item definitions vs item instances — where do you actually draw the line

123 views 11 replies

Working on an RPG-adjacent thing in Godot 4 and I keep second-guessing my item system architecture. The basic split makes sense conceptually: ItemDefinition is a Resource with static data (name, icon, base damage, description, max stack size), and ItemInstance is what lives in inventory slots and carries mutable stuff (current durability, quantity, randomly rolled modifiers).

Where it gets messy is deciding what actually belongs where. Base durability: definition or instance? I put it in definition as a reference value and store current durability on the instance. Fine. But then I added quality tiers, and suddenly an Iron Sword can be Common, Rare, or Epic with different base stats. Do I make three separate definition resources? Add a quality field to the instance? A lookup table on the definition?

class_name ItemInstance
extends Resource

@export var definition: ItemDefinition
@export var quantity: int = 1
@export var durability: float = -1.0  # -1 = use definition max
@export var quality: ItemDefinition.Quality = ItemDefinition.Quality.COMMON
@export var modifiers: Array[StatModifier] = []

func get_effective_stat(stat: StringName) -> float:
    var base := definition.get_base_stat(stat, quality)
    for mod in modifiers:
        if mod.stat == stat:
            base = mod.apply(base)
    return base

It works but it feels like papering over a design problem. The quality field on the instance means I need both objects together to reconstruct what an item actually is, and they're coupled in a way that's starting to feel fragile. And modifiers as an array of Resources serializes fine but debugging an item with 4 stacked modifiers in the editor is already not fun, and I haven't added crafting yet.

Curious how others handle this split. Fully data-driven with JSON or a proper database? Sticking with Resources? Something else entirely? And does the definition/instance model even hold up once you layer in crafting or item evolution mechanics where the definition itself kind of mutates?

The thing that finally made this architecture click for me was naming a third layer explicitly: the template. ItemDefinition holds the stat ranges and flavor data. The template is config for how to instantiate: which stats get rolled, what modifier tables to pull from, whether the item gets a prefix or suffix. The instance is just the result of running a template against a definition.

Once I had all three named and separated, questions like 'where does rolled base damage live?' stopped being contentious. It lives on the instance because the template rolled it. The definition only holds the range.

Replying to HexRunner: the stacking question is what breaks this whole clean mental model for me tbh. q...

The stack cap gets thornier once equipment or buff interactions enter the picture. 'Stackable up to 99' is simple until a perk increases that limit, or a crafting recipe requires exactly 50 of something regardless of how they happen to be stacked. At that point max_stack wants to be a computed property on the instance rather than a fixed definition value, which means you're essentially writing a mini-buff system just to manage item quantity. The clean line between definition and instance erodes fast once progression systems start touching inventory.

the thing that unstuck me on this whole architecture was separating 'item type' from 'item instance' from 'inventory slot'. the slot holds the instance, the instance references the definition, and the slot handles stack quantity. feels like an extra layer at first but it means quantity lives somewhere that actually makes sense, on the slot, and neither definition nor instance has to care about it. might be overkill for a simple inventory but it cleaned up a lot of the edge cases for me.

The line I draw: if two copies of the same item can have different values for a field, it belongs on the instance. If it's always identical for a given item type, it's definition data.

Durability, enchantments, stack count, and per-drop randomized stats live on instances. Base stats, icon, display name, and weight are definition-only. The useful question is "could I have two Rusty Swords in my inventory that are meaningfully different from each other?" If yes, those differences are instance data.

The tricky case I've hit is condition-dependent display names, like "Cracked Iron Shield" vs "Iron Shield", where I ended up putting a get_display_name() method on the instance that reads from the definition but can override based on state. Keeps the logic close to the data without duplicating strings into the definition.

Replying to GlitchFox: The line I draw: if two copies of the same item can have different values for a ...

This is basically the rule I use too, though it gets complicated with procedurally generated items. If your system rolls randomized base damage at item creation time, is that base damage still a definition property? It's 'always identical for a given item type' until the whole point of your system is that it isn't.

I ended up with a 'rolled definition' concept, still immutable after creation but generated rather than hand-authored. Felt like bending the pattern, but the key insight for me was that immutability after creation matters more than where the data originated. The rule holds, you just have to be looser about what counts as a definition.

Replying to GlitchFox: The line I draw: if two copies of the same item can have different values for a ...

Works for most cases. The edge case I kept tripping over was derived/computed properties, things like effective damage that depends on base damage (definition) plus an enchantment bonus (instance). I wasn't sure whether to cache that computed value on the instance or always calculate it on the fly.

Ended up going with a method on the instance that reads from its own fields and calls into the definition as needed. No cached value, just computed when asked. Slightly more overhead, but it eliminates an entire class of "stale cached value" bugs that I really didn't want to chase down later.

Replying to RiftGale: Works for most cases. The edge case I kept tripping over was derived/computed pr...

derived properties are a trap and i just stopped caching them entirely. get_effective_damage() recomputes from definition base + instance modifiers on every call. tiny overhead, zero stale cache bugs.

the moment you start writing cache invalidation logic you've basically built a mini reactive system, and i refuse to do that in a game project. signals are right there if you actually need reactivity, just emit on the instance when a modifier changes and let the UI figure it out.

Replying to DriftCrow: This is basically the rule I use too, though it gets complicated with procedural...

The way I landed on this: the definition holds the stat ranges (min_damage, max_damage), and the instance stores the actual rolled value as a plain float. So the definition stays fully static: it describes what the item can be, not what any particular copy is. Bonus: you can display damage ranges in shop tooltips without needing an instance at all. Serialization stays clean because rolled stats are just plain numbers sitting on the instance.

Replying to RoguePulse: derived properties are a trap and i just stopped caching them entirely. get_effe...

recomputing every call is fine until you add a sort-by-stat feature to your inventory. same approach here, worked great right up until i was calling get_effective_damage() on 200 items in a single frame to sort a full inventory by power level. tiny overhead times 200 is not tiny anymore.

dirty flag on the instance is the fix: whenever modifiers change the flag goes up, next call to the getter recomputes and caches. zero stale values but also not recalculating when nothing changed. felt like overthinking it until the sort hitch made it obviously necessary.

the stacking question is what breaks this whole clean mental model for me tbh. quantity is obviously instance-level until you have "stackable up to 99". is that 99 a definition property? probably. but then you get "blessed arrows" vs "regular arrows", same item type, different instance modifier, and now can they stack together? suddenly you need stack group logic and it falls apart fast.

i ended up with a can_stack_with(other: ItemInstance) -> bool method on the instance that checks definition equality AND whether all instance-level modifiers match. ugly but it handles the edge cases without a bunch of special casing in the inventory layer.

the part that bit me hardest was serialization. if ItemDefinition is a Resource in Godot you can just store the resource path and reload it on load, clean. but the moment instances have mutable state that references a definition, you have to decide: serialize the whole instance, or just the delta from defaults?

i ended up with instances that only write fields that differ from their definition. durability at max? don't save it. enchantment bonus is zero? skip it. smaller save files, and if you update a definition's defaults they propagate to unmodified instances automatically on next load.

works great until you rename a definition field and then everything is on fire. still haven't written a migration layer for that.

Moonjump
Forum Search Shader Sandbox
Sign In Register