MineRPG uses a multi-assembly C# architecture where pure business logic lives in engine-agnostic class libraries, and Godot-specific code lives in separate bridge projects. This enables unit testing without Godot, enforces strict boundaries, and makes the logic portable.
┌─────────────────────────────────────────────────────────┐
│ MineRPG.Game │
│ (Godot project — entry point) │
│ References ALL projects below │
│ Contains: Scenes, bootstrapper, composition root │
└──────────┬──────────┬──────────┬──────────┬─────────────┘
│ │ │ │
┌─────▼───┐ ┌────▼────┐ ┌──▼───┐ ┌───▼────────┐
│ .Godot │ │ .Godot │ │.Godot│ │ .Godot │
│ .World │ │.Entities│ │ .UI │ │ .Network │
│ Bridge │ │ Bridge │ │Bridge│ │ Bridge │
└────┬────┘ └────┬────┘ └──┬───┘ └─────┬──────┘
│ │ │ │
┌──────▼──┐ ┌─────▼───┐ │ ┌──────▼──────┐
│ .World │ │.Entities│ │ │ .Network │
│ (pure) │ │ (pure) │ │ │ (pure) │
└────┬────┘ └────┬────┘ │ └──────┬──────┘
│ │ │ │
┌────▼────────────▼─────────▼────────────▼──┐
│ .RPG (pure) │
│ Stats, Combat, Skills, Buffs, Loot │
└─────────────────┬─────────────────────────┘
│
┌─────────────────▼─────────────────────────┐
│ .Core (pure) │
│ EventBus, Interfaces, Registry<T>, │
│ ObjectPool, DataLoader, Math, Extensions │
└───────────────────────────────────────────┘
Golden rule: Arrows point downward only. A project must NEVER reference a project above it.
| Project | Can Reference | CANNOT Reference |
|---|---|---|
MineRPG.Core |
Nothing | Everything else |
MineRPG.RPG |
Core |
World, Entities, Godot.*, Game |
MineRPG.World |
Core |
RPG, Entities, Godot.*, Game |
MineRPG.Entities |
Core, RPG |
World, Godot.*, Game |
MineRPG.Network |
Core |
RPG, World, Entities, Godot.*, Game |
MineRPG.Godot.World |
Core, World |
RPG, Entities, other Godot.* |
MineRPG.Godot.Entities |
Core, RPG, Entities |
World, other Godot.* |
MineRPG.Godot.UI |
Core, RPG |
World, Entities, other Godot.* |
MineRPG.Godot.Network |
Core, Network |
RPG, World, Entities, other Godot.* |
MineRPG.Game |
All Godot.* projects directly (pure projects come in transitively) |
— |
MineRPG.Tests |
All pure projects | Godot.*, Game |
- If
MineRPG.RPGtries to referenceMineRPG.World, the compiler will reject it — the.csprojdoes not have thatProjectReference - Godot bridge projects never reference each other — they communicate through the pure layer via
IEventBus MineRPG.Testsonly references pure projects — all tests run without Godot
The shared foundation used by every other project. No external dependencies, not even Godot.
src/MineRPG.Core/
├── Events/ # IEventBus, EventBus, GameEvents (struct catalog)
├── Registry/ # IRegistry<TKey, TValue>, Registry<TKey, TValue>
├── DataLoading/ # IDataLoader, JsonDataLoader, DataPath
├── Pooling/ # IObjectPool<T>, ObjectPool<T>
├── StateMachine/ # IState, IStateMachine, StateMachine
├── Command/ # ICommand, CommandQueue
├── Math/ # FastNoise, VoxelMath, ChunkCoord
├── DI/ # IServiceLocator, ServiceLocator
├── Interfaces/ # ITickable, ISaveable, IIdentifiable
└── Extensions/ # CollectionExtensions, MathExtensions
Key design decisions:
EventBususes typed struct events — no boxing, no string-based signalsRegistry<TKey, TValue>is the backbone for all data-driven registries (blocks, items, mobs, etc.)ObjectPool<T>provides reusable object pools to avoid GC pressureStateMachinesupports pushdown automata for nested states (e.g., player in combat while in a dialogue)
All RPG mechanics: stats, combat, crafting, items, quests, dialogues. No rendering, no voxels.
src/MineRPG.RPG/
├── Stats/ # StatDefinition, StatContainer, StatModifier, AttributeSet
├── Leveling/ # ExperienceCurve, LevelSystem, SkillPointAllocator
├── Classes/ # ClassDefinition, ClassRegistry, TalentTree
├── Combat/ # DamageType, DamageCalculator, IDamageFormula, HitResult, StatusEffect, ThreatTable
├── Skills/ # SkillDefinition, SkillRegistry, Cooldown, SkillExecutor
├── Items/ # ItemDefinition, ItemRegistry, ItemInstance, Affix, LootTable
├── Inventory/ # IInventory, Inventory, InventorySlot, SlotFilter
├── Crafting/ # RecipeDefinition, RecipeRegistry, CraftingQueue, CraftingValidator
├── Quests/ # QuestDefinition, QuestObjective, QuestState, QuestJournal
├── Dialogue/ # DialogueTree, DialogueNode, DialogueCondition, DialogueEffect
├── Reputation/ # FactionDefinition, ReputationTracker, FactionRegistry
└── Buffs/ # BuffDefinition, BuffInstance, BuffContainer
Key design decisions:
StatContainersupports flat, percent-add, and percent-multiply modifiers with dirty-flag cachingDamageCalculatorusesIDamageFormulastrategy pattern — formulas are swappable via dataLootTableuses weighted random with conditions — fully data-drivenInventoryis generic — same class for player, chests, and merchant NPCs
Everything about the voxel world: chunk data, generation, meshing, biomes, lighting. No Godot dependency — meshes are raw vertex/index/UV arrays.
src/MineRPG.World/
├── Blocks/ # BlockDefinition, BlockFlags, BlockRegistry, BlockInteraction
├── Chunks/ # ChunkData (ushort[] flat array), ChunkState, ChunkManager, ChunkSerializer
├── Generation/ # IWorldGenerator, WorldGenerator, BiomeDefinition, HeightmapGenerator, CaveCarver
├── Structures/ # StructureTemplate, StructureRegistry, StructureRule
├── Meshing/ # IMeshBuilder, GreedyMeshBuilder, MeshData, MeshUtils
├── Lighting/ # LightingEngine, LightData, LightPropagator
├── Liquids/ # LiquidSimulator, LiquidData
└── Spatial/ # WorldPosition, ChunkPosition, LocalPosition
Key design decisions:
ChunkDatauses a flatushort[]array, not a 3D array or dictionary — cache-friendly and compactGreedyMeshBuilderproducesMeshData(raw arrays) that the Godot bridge converts toArrayMeshAllowUnsafeBlocks = truein this project only — forSpan<T>andstackallocin meshing hot paths- All generation runs on background threads —
CancellationTokenfor cancelling stale jobs
Entity logic (player, mobs, NPCs) without any Godot dependency. Defines logical components and AI systems.
src/MineRPG.Entities/
├── Components/ # HealthComponent, StaminaComponent, ManaComponent, CombatComponent, etc.
├── AI/
│ ├── BehaviorTree/ # IBTNode, BTSelector, BTSequence, BTCondition, BTAction, BTStatus
│ ├── Actions/ # PatrolAction, ChaseAction, FleeAction, AttackAction, WanderAction
│ ├── Perception/ # PerceptionData, PerceptionResult
│ └── Spawning/ # SpawnRule, SpawnTable
├── Definitions/ # MobDefinition, MobRegistry, NPCDefinition, NPCSchedule
└── Player/ # PlayerData, InputAction
Key design decisions:
- Behavior Tree implemented in pure C# — no GDScript plugins
- Components are plain C# classes, not Godot nodes — they wrap RPG layer types
PlayerDataaggregates all components — it's the "entity" without being a God class- AI perception runs at reduced frequency (every 0.2-0.5s), not every frame
Network abstraction layer. Interfaces and protocol only — no transport implementation.
src/MineRPG.Network/
├── INetworkTransport.cs # Send, Receive, Connect, Disconnect
├── IPacket.cs
├── PacketRegistry.cs
├── Packets/ # ChunkDataPacket, BlockChangePacket, EntityMovePacket, etc.
├── Serialization/ # PacketReader (binary), PacketWriter (binary)
├── Authority/ # IServerAuthority, ClientPrediction
└── Sync/ # DeltaCompressor, InterpolationBuffer
Key design decisions:
INetworkTransportabstracts the transport — swap ENet, WebSocket, or Steam Networking- Solo mode runs a local server using the same code path — no special-casing
- Only deltas are synchronized (modified blocks, not full chunks) to minimize bandwidth
These projects translate pure logic into Godot nodes. They are the only projects that reference GodotSharp.
| Bridge | Converts | Into |
|---|---|---|
Godot.World |
MeshData |
ArrayMesh + MeshInstance3D + ConcavePolygonShape3D |
Godot.Entities |
PlayerData, MobDefinition |
CharacterBody3D + AnimationTree + Area3D |
Godot.UI |
RPG data (inventory, stats, quests) | Godot Control nodes (MVVM / Observer) |
Godot.Network |
INetworkTransport |
ENet/WebSocket via Godot APIs |
Rules for bridge projects:
- Bridge nodes are thin — they delegate to the pure layer, they do not contain business logic
- UI observes data, it never modifies it directly (MVVM pattern)
- Bridge projects never reference each other — inter-bridge communication goes through
IEventBus
The Godot project root. Contains scenes, bootstrapper, assets, and data files.
(project root — next to project.godot and MineRPG.Game.csproj)
├── Bootstrap/ # GameBootstrapper (autoload), CompositionRoot, GameConfig
├── Scenes/ # Main.tscn, World/, UI/, Entities/
├── Data/ # JSON files for all data-driven content
├── Resources/ # Materials, Shaders, Themes
├── Assets/ # Textures, Models, Audio, Fonts
└── src/ # All sub-projects (never edit from Game directly)
Key design decisions:
GameBootstrapperis the single autoload — it initializes all systems and wires DICompositionRootconnects interfaces to implementations (whichIDataLoader, whichINetworkTransport, etc.)- All data files live in
Data/subdirectories, organized by type
src/MineRPG.Tests/
├── Core/ # EventBusTests, RegistryTests, StateMachineTests, ObjectPoolTests
├── RPG/ # DamageCalculatorTests, InventoryTests, CraftingValidatorTests, LootTableTests
├── World/ # ChunkDataTests, GreedyMeshBuilderTests, BiomeSelectionTests, LightingEngineTests
├── Entities/ # BehaviorTreeTests, PerceptionTests, SpawnRuleTests
└── Network/ # PacketSerializationTests, DeltaCompressorTests
Testing stack: xUnit + FluentAssertions + NSubstitute. All tests run without Godot.
1. Player presses "attack" input
2. PlayerController (Godot.Entities) detects input via InputActions.Attack
3. VoxelRaycast (Godot.World) determines which block the player is looking at
4. MiningSystem checks BlockDefinition.Hardness vs equipped tool
5. ChunkData.SetBlock(x, y, z, 0) removes the block
6. EventBus publishes BlockMinedEvent { Position, BlockId, PlayerId }
7. Subscribers react independently:
- ChunkMeshApplier rebuilds the chunk mesh
- LootSystem generates drops from BlockDefinition.LootTableRef
- QuestJournal checks for "mine X blocks" objectives
- LightingEngine re-propagates light
- AudioManager plays mining sound
1. Create a JSON file in Data/Items/ (e.g., iron_sword.json)
2. The file follows ItemDefinition schema: id, name, type, rarity, stats, etc.
3. At startup, GameBootstrapper loads all Data/Items/*.json via IDataLoader
4. Each definition is registered in ItemRegistry
5. No code changes needed — the item is available via ItemRegistry.Get(id)
6. If the item needs special behavior, create an ItemEffect in MineRPG.RPG/Items/
1. HitboxNode (Godot.Entities) detects Area3D collision
2. Extracts AttackData from attacker's CombatComponent
3. Extracts DefenseData from defender's StatsComponent
4. DamageCalculator.Calculate(attack, defense) returns HitResult
5. HealthComponent.TakeDamage(hitResult.Damage)
6. BuffComponent checks for damage modifiers (e.g., fire resistance)
7. EventBus publishes DamageTakenEvent
8. UI updates health bar, shows damage number
9. If health <= 0: DeathEvent -> loot drop, XP reward, respawn timer
Godot 4 .NET expects a single .csproj next to project.godot. For multi-project support:
- Only
MineRPG.Game.csprojlives at the project root (next toproject.godot) - All other
.csprojfiles live insrc/subdirectories MineRPG.Game.csprojhas<ProjectReference>to all Godot bridge projects<DefaultItemExcludes>$(DefaultItemExcludes);src/**</DefaultItemExcludes>prevents Godot SDK from including sub-project source files- Godot compiles the full solution via the
.sln - The
Directory.Build.propsat the root applies shared settings (Nullable, TreatWarningsAsErrors, LangVersion) to all projects - The
GodotSharpVersionis pinned centrally inDirectory.Build.props
| System | Target Metric | Key Technique |
|---|---|---|
| Chunk meshing | < 5ms per chunk | Greedy meshing + background thread |
| Chunk loading | 0 stutter on main thread | Async + frame budget |
| Terrain rendering | < 500 draw calls | Atlas + 1 mesh/chunk + LOD |
| Terrain physics | < 1ms/frame | Custom raycast, not 1 collider/block |
| Mob AI | < 2ms total/frame | AI budget, sleep distant mobs, reduced tick |
| Inventory UI | 0 allocations during navigation | UI element pooling |
| Lighting | < 3ms propagation | BFS on background thread |
| Chunk save | < 10ms per chunk | Binary serialization + compression |
| Anti-Pattern | Why It's Bad | What to Do Instead |
|---|---|---|
| God class | Single class with too many responsibilities | Split into focused components |
| Deep inheritance | Rigid, fragile, hard to test | Use composition |
| Singleton everywhere | Hidden dependencies, hard to test, thread-unsafe | Use DI, pass dependencies explicitly |
| String-based signals | No compile-time safety, allocation per emit | Use typed C# signals or IEventBus |
| GD.Print for logging | No levels, no filtering, no disabling | Use centralized logging system |
| Hardcoded data | Can't modify without recompiling | Load from data files |
| Logic in Godot nodes | Untestable, coupled to engine | Put logic in pure projects |
GetNode with paths |
Breaks on scene restructuring | Use [Export] references |
| LINQ in hot paths | Creates allocations (enumerators, closures) | Use for loops with pre-allocated lists |
new in _Process |
GC pressure, stutters | Use object pooling |