Architecture
Stack Inventory uses a three-layer architecture designed for flexibility, performance, and testability.
The Three Layers
Section titled “The Three Layers”┌─────────────────────────────────────┐│ UI Layer (Godot Nodes) ││ Pickup2D, Pickup3D, StackView │└──────────────┬──────────────────────┘ │ uses┌──────────────▼──────────────────────┐│ Game Layer (Godot Resources) ││ ItemDefinition, ActionSettings │└──────────────┬──────────────────────┘ │ implements┌──────────────▼──────────────────────┐│ Core Layer (POCS Interfaces) ││ IItem, Item, ItemTag │└─────────────────────────────────────┘Layer 1: Core (POCS)
Section titled “Layer 1: Core (POCS)”Purpose: Pure C# serializable types with no Godot dependencies
Key Types:
IItem- Interface defining what an item isItem- POCS implementation of IItemItemTag- 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 JSONstring json = JsonSerializer.Serialize(sword);
// Deserialize from JSONItem loaded = JsonSerializer.Deserialize<Item>(json);When to use:
- Save/load systems
- Networked inventories
- Procedural item generation
- Testing item logic
Layer 2: Game (Godot Resources)
Section titled “Layer 2: Game (Godot Resources)”Purpose: Godot Resource implementations that can be edited in the Inspector
Key Types:
ItemDefinition- Godot Resource implementing IItemActionSettings- 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
Layer 3: UI (Godot Nodes)
Section titled “Layer 3: UI (Godot Nodes)”Purpose: Scene nodes that work with items in the game world and UI
Key Types:
Pickup2D/Pickup3D- World items that can be picked upStackView- 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 scenepublic 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 Interface Bridge: IItem
Section titled “The Interface Bridge: IItem”The magic that ties everything together is the IItem interface.
Why Interfaces?
Section titled “Why Interfaces?”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);}Zero-Overhead Pattern
Section titled “Zero-Overhead Pattern”The genius of this design is that converting between layers has zero cost:
// ItemDefinition (Resource) → IItemIItem item = itemDefinition; // Just a cast! No conversion!
// Item (POCS) → IItemIItem item2 = itemPOCS; // Also just a cast!
// Use them the same wayvoid 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
Data Flow Examples
Section titled “Data Flow Examples”Example 1: Editor → Game
Section titled “Example 1: Editor → Game”Designer creates ItemDefinition.tres ↓Pickup2D loads ItemDefinition ↓Pickup2D.Item returns IItem (just a cast) ↓Player collects IItem ↓Inventory stores IItem ↓UI displays IItem propertiesExample 2: Network → Game
Section titled “Example 2: Network → Game”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)Example 3: Procedural Generation
Section titled “Example 3: Procedural Generation”Code generates random stats ↓Create new Item(id, name, value, ...) ↓Cast to IItem ↓Add to inventory or spawn pickup ↓(Continues same as other examples)Testing Architecture
Section titled “Testing Architecture”Each layer can be tested independently:
Core Layer Tests (xUnit)
Section titled “Core Layer Tests (xUnit)”[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");}Game Layer Tests (GoDotTest)
Section titled “Game Layer Tests (GoDotTest)”[Test]public void ItemDefinition_ShouldImplementIItem(){ var itemDef = new ItemDefinition { Name = "Sword" }; IItem item = itemDef;
item.Name.ShouldBe("Sword");}UI Layer Tests (GoDotTest)
Section titled “UI Layer Tests (GoDotTest)”[Test]public void Pickup2D_ShouldReturnItemFromDefinition(){ var pickup = new Pickup2D { ItemDefinition = new ItemDefinition { Name = "Potion" } };
pickup.Item.Name.ShouldBe("Potion");}Design Benefits
Section titled “Design Benefits”1. Flexibility
Section titled “1. Flexibility”Use Resources OR POCS classes - your choice!
// Works the samevoid HandleItem(IItem item) { }
HandleItem(itemDefinition); // ResourceHandleItem(itemPOCS); // POCS2. Performance
Section titled “2. Performance”Zero-overhead interface casts:
// No conversion cost!IItem item = resource; // Cast onlyitem.Name; // Direct access3. Testability
Section titled “3. Testability”Core logic has no Godot dependencies:
// Test without Godotvar item = new Item(...);Assert.Equal("Sword", item.Name);4. Serialization
Section titled “4. Serialization”POCS classes serialize easily:
string json = JsonSerializer.Serialize(item);SaveToFile(json);5. Designer-Friendly
Section titled “5. Designer-Friendly”Resources editable in Inspector:
- Visual editing
- Asset references
- Validation
- No code required
Best Practices
Section titled “Best Practices”- 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 ❌
Section titled “DON’T ❌”- 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
Code Examples
Section titled “Code Examples”Inventory Manager (Uses All Layers)
Section titled “Inventory Manager (Uses All Layers)”using StackInventory.Core.Items; // Coreusing 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); }}Next Steps
Section titled “Next Steps”- Getting Started - Build your first inventory
- API Reference - Detailed class documentation
- Migration Guide - Upgrade from v0.x
Summary
Section titled “Summary”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!