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!