Skip to content

Architecture

Stack Inventory uses a three-layer architecture designed for flexibility, performance, and testability.

┌─────────────────────────────────────┐
│ UI Layer (Godot Nodes) │
│ Pickup2D, Pickup3D, StackView │
└──────────────┬──────────────────────┘
│ uses
┌──────────────▼──────────────────────┐
│ Game Layer (Godot Resources) │
│ ItemDefinition, ActionSettings │
└──────────────┬──────────────────────┘
│ implements
┌──────────────▼──────────────────────┐
│ Core Layer (POCS Interfaces) │
│ IItem, Item, ItemTag │
└─────────────────────────────────────┘

Purpose: Pure C# serializable types with no Godot dependencies

Key Types:

  • IItem - Interface defining what an item is
  • Item - POCS implementation of IItem
  • ItemTag - Hierarchical tag for categorization

Characteristics:

  • ✅ No Godot dependencies
  • ✅ Easy to serialize (JSON, binary, etc.)
  • ✅ Easy to test (no mocking needed)
  • ✅ Can use in any C# code
  • ✅ Perfect for networking and save files

Example:

using StackInventory.Core.Items;
// Create an item in pure C#
var sword = new Item(
id: Guid.NewGuid(),
name: "Iron Sword",
value: 100f,
tags: new List<ItemTag> { new("Weapon"), new("Melee") },
stackMaximum: 1,
iconPath: "res://icons/sword.png",
tooltip: "A sturdy iron sword"
);
// Serialize to JSON
string json = JsonSerializer.Serialize(sword);
// Deserialize from JSON
Item loaded = JsonSerializer.Deserialize<Item>(json);

When to use:

  • Save/load systems
  • Networked inventories
  • Procedural item generation
  • Testing item logic

Purpose: Godot Resource implementations that can be edited in the Inspector

Key Types:

  • ItemDefinition - Godot Resource implementing IItem
  • ActionSettings - Configuration for pickup/drop behaviors

Characteristics:

  • ✅ Editable in Godot Inspector
  • ✅ Can be saved as .tres files
  • ✅ Implements IItem interface
  • ✅ Automatic validation
  • ✅ Visual workflow for designers

Example:

// Create in Godot Editor as .tres file
// Or in code:
var potion = new ItemDefinition
{
Name = "Health Potion",
Value = 50f,
StackMaximum = 99,
Icon = GD.Load<Texture2D>("res://icons/potion.png"),
Tooltip = "Restores 50 HP"
};
// Use as IItem (just a cast - zero overhead!)
IItem item = potion;

When to use:

  • Content creation in Godot Editor
  • Designer-friendly workflow
  • Items defined at compile time
  • Visual asset references

Purpose: Scene nodes that work with items in the game world and UI

Key Types:

  • Pickup2D / Pickup3D - World items that can be picked up
  • StackView - UI component for inventory display
  • (Your custom nodes)

Characteristics:

  • ✅ Inherits from Godot nodes
  • ✅ Has collision/physics/rendering
  • ✅ Works with IItem interface
  • ✅ Scene-friendly workflow

Example:

// Pickup2D in a scene
public partial class Pickup2D : Area2D
{
[Export] public ItemDefinition ItemDefinition { get; set; }
[Export] public int Amount { get; set; }
// Computed property - zero overhead!
public IItem Item => ItemDefinition;
public int TryPickup(ICollector collector)
{
int taken = collector.TryCollect(Item, Amount);
Amount -= taken;
return taken;
}
}

When to use:

  • World items players interact with
  • Inventory UI components
  • Visual feedback systems

The magic that ties everything together is the IItem interface.

Problem: How do we use both Resources and POCS classes interchangeably?

Solution: Both implement the same interface!

public interface IItem
{
Guid Id { get; }
string Name { get; }
float Value { get; }
List<ItemTag> Tags { get; }
int StackMaximum { get; }
string IconPath { get; }
string Tooltip { get; }
bool HasTag(ItemTag tag);
bool HasTag(string tagId);
}

The genius of this design is that converting between layers has zero cost:

// ItemDefinition (Resource) → IItem
IItem item = itemDefinition; // Just a cast! No conversion!
// Item (POCS) → IItem
IItem item2 = itemPOCS; // Also just a cast!
// Use them the same way
void ProcessItem(IItem item)
{
GD.Print(item.Name); // Works with both!
GD.Print(item.Value); // No performance difference!
}

Performance:

  • No allocations
  • No copying
  • No lookups
  • Direct property access
  • 10-100x faster than string-based systems
Designer creates ItemDefinition.tres
Pickup2D loads ItemDefinition
Pickup2D.Item returns IItem (just a cast)
Player collects IItem
Inventory stores IItem
UI displays IItem properties
Server sends Item JSON
Client deserializes to Item (POCS)
Client casts to IItem
Client spawns Pickup2D with Item
(Same flow as Example 1 from here)
Code generates random stats
Create new Item(id, name, value, ...)
Cast to IItem
Add to inventory or spawn pickup
(Continues same as other examples)

Each layer can be tested independently:

[Fact]
public void Item_ShouldSerializeToJson()
{
var item = new Item(Guid.NewGuid(), "Test", 10f, ...);
string json = JsonSerializer.Serialize(item);
var loaded = JsonSerializer.Deserialize<Item>(json);
loaded.Name.ShouldBe("Test");
}
[Test]
public void ItemDefinition_ShouldImplementIItem()
{
var itemDef = new ItemDefinition { Name = "Sword" };
IItem item = itemDef;
item.Name.ShouldBe("Sword");
}
[Test]
public void Pickup2D_ShouldReturnItemFromDefinition()
{
var pickup = new Pickup2D
{
ItemDefinition = new ItemDefinition { Name = "Potion" }
};
pickup.Item.Name.ShouldBe("Potion");
}

Use Resources OR POCS classes - your choice!

// Works the same
void HandleItem(IItem item) { }
HandleItem(itemDefinition); // Resource
HandleItem(itemPOCS); // POCS

Zero-overhead interface casts:

// No conversion cost!
IItem item = resource; // Cast only
item.Name; // Direct access

Core logic has no Godot dependencies:

// Test without Godot
var item = new Item(...);
Assert.Equal("Sword", item.Name);

POCS classes serialize easily:

string json = JsonSerializer.Serialize(item);
SaveToFile(json);

Resources editable in Inspector:

  • Visual editing
  • Asset references
  • Validation
  • No code required
  • Use ItemDefinition for editor-defined items
  • Use Item for runtime/network/save items
  • Use IItem in all APIs and interfaces
  • Keep Core layer pure C# (no Godot deps)
  • Test each layer independently
  • Don’t store IItem directly (store the concrete type)
  • Don’t convert between types unnecessarily
  • Don’t add Godot dependencies to Core layer
  • Don’t bypass IItem in public APIs
  • Don’t duplicate data between layers
using StackInventory.Core.Items; // Core
using StackInventory.Game.Definitions; // Game
public partial class InventoryManager : Node
{
// Storage: Can hold both Resources and POCS
private List<(IItem item, int amount)> _items = new();
// Add: Works with any IItem
public void AddItem(IItem item, int amount)
{
var existing = _items.FirstOrDefault(i => i.item.Id == item.Id);
if (existing.item != null)
{
int index = _items.IndexOf(existing);
_items[index] = (existing.item, existing.amount + amount);
}
else
{
_items.Add((item, amount));
}
}
// Save: Convert to POCS for serialization
public string SaveToJson()
{
var saveData = _items.Select(i => new
{
Item = i.item is Item pocs ? pocs : Item.FromIItem(i.item),
Amount = i.amount
}).ToList();
return JsonSerializer.Serialize(saveData);
}
}

Stack Inventory’s three-layer architecture provides:

  • Separation of concerns (Core/Game/UI)
  • Zero-overhead interfaces (IItem)
  • Flexible implementations (Resource or POCS)
  • Easy testing (pure C# core)
  • Great performance (direct access, no lookups)
  • Designer-friendly (Resource workflow)
  • Developer-friendly (POCS serialization)

The result: A fast, flexible, testable inventory system that works great in Godot!