Development ⚠️ GridPlacement 6.0 documentation is in active development. APIs and content may change, and the site may be temporarily unstable.

Architecture Evolution: GDScript to POCS

The Transformation

GridBuilding has undergone a major architectural evolution from GDScript-heavy, Godot-dependent code to a clean POCS (Pure C# Core Services) architecture with thin Godot wrappers.

Before: Pure GDScript Architecture

What It Looked Like

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Old: Everything in GDScript, Godot-dependent
extends Node
class_name BuildingSystem

var buildings = {}
var grid_data = {}
var collision_map = {}

func _ready():
    setup_grid()
    load_buildings()

func place_building(building_type, position):
    # All logic in GDScript - 50+ lines
    if not validate_placement(building_type, position):
        return false
    
    # Manual scene management
    var scene = load("res://buildings/" + building_type + ".tscn")
    var building = scene.instantiate()
    building.position = position
    add_child(building)
    
    # Manual state tracking
    var building_id = generate_id()
    buildings[building_id] = {
        "node": building,
        "type": building_type,
        "position": position,
        "health": 100
    }
    
    # Manual grid updates
    update_grid_data(position, building_type)
    update_collision_map(position, building_type)
    
    return true

func validate_placement(building_type, position):
    # Complex validation logic in GDScript
    if not grid_data.has(position):
        return false
    
    if collision_map.has(position):
        return false
    
    # 20+ lines of validation...
    return true

Problems with Old Architecture

Note: The GDScript version performed fine for typical use cases. Performance wasn’t noticeably problematic, but the architecture had other challenges:

1. Testing Challenges

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Old: Hard to test GDScript code
func test_building_placement():
    # Need full Godot environment
    var scene = preload("res://tests/test_scene.tscn").instantiate()
    add_child(scene)
    
    var building_system = scene.get_node("BuildingSystem")
    var result = building_system.place_building("house", Vector2(5, 5))
    
    # Complex setup and teardown
    scene.queue_free()

2. Code Maintenance

  • Mixed Concerns: Building logic, UI, and state all mixed together
  • Godot Dependencies: Everything tied to Godot Node system
  • Hard to Extend: Adding new features required touching many files

3. Limited Reusability

  • Godot Lock-in: Code only worked in Godot
  • No Engine Portability: Couldn’t reuse for Unity or other engines
  • Monolithic Design: Large, interconnected classes

After: POCS Architecture

What It Looks Like Now

Core Services (Pure C# - No Godot Dependencies)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// New: Pure C# business logic
public class PlacementService : IPlacementService
{
    private readonly IPlacementValidator _validator;
    private readonly ICollisionCalculator _collisionCalculator;
    
    public PlacementResult ExecutePlacement(Placeable placeable, Vector2 position, GBOwner placer)
    {
        // Fast C# validation
        var validationResult = _validator.ValidatePlacement(placeable, position, placer);
        if (!validationResult.IsValid)
        {
            return PlacementResult.Failed(validationResult.ErrorMessage);
        }
        
        // Optimized collision detection
        var collisionResult = _collisionCalculator.CheckCollisions(placeable, position);
        if (collisionResult.HasCollision)
        {
            return PlacementResult.Failed("Position occupied");
        }
        
        return PlacementResult.Success(position, placeable.Type);
    }
}

State Objects (Pure Data)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// New: Clean data objects
public class BuildingState : IRuntimeState
{
    public string BuildingId { get; set; }
    public string BuildingType { get; set; }
    public Vector2I GridPosition { get; set; }
    public BuildingStatus Status { get; set; }
    public int Health { get; set; }
    
    // No business logic - just data
    public void UpdateTimestamp() { }
    public void SetCustomData(string key, object value) { }
}

Godot Wrappers (Thin Adapters)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# New: Thin Godot wrapper
extends Node
class_name PlacementSystem

# Just forwards to C# service
@onready var placement_service = ServiceRegistry.get_placement_service()

func place_building(building_type, position):
    var result = placement_service.execute_placement(
        Placeable.new(building_type), 
        position, 
        GBOwner.new("player")
    )
    
    if result.success:
        # Handle Godot-specific stuff
        spawn_building_visual(result)
    
    return result.success

Key Improvements

1. Better Architecture & Testing

Before (GDScript)

1
2
3
4
5
6
7
8
# GDScript validation
func validate_placement(building_type, position):
    for x in range(building_size.x):
        for y in range(building_size.y):
            var check_pos = position + Vector2i(x, y)
            if collision_map.has(check_pos):
                return false
    return true

After (C#)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// C# validation with clean architecture
public ValidationResult ValidatePlacement(Placeable placeable, Vector2 position, GBOwner placer)
{
    // Clean separation of concerns
    var occupiedTiles = _collisionCalculator.GetOccupiedTiles(position, placeable.Size);
    
    if (occupiedTiles.Count > 0)
    {
        return ValidationResult.Failed($"Position occupied: {string.Join(", ", occupiedTiles)}");
    }
    
    return ValidationResult.Success();
}

Architecture Improvements:

  • Clean separation of concerns with dedicated services
  • Better testability with pure C# unit tests
  • Improved maintainability with focused, single-purpose classes

2. Testing Revolution

Before (Complex GDScript Tests)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Old: Required full Godot environment
extends "res://addons/gdunit/test.gd"

func test_building_placement():
    # Complex setup
    var test_scene = preload("res://tests/test_scenes/building_test.tscn").instantiate()
    add_child(test_scene)
    
    var building_system = test_scene.get_node("BuildingSystem")
    var result = building_system.place_building("house", Vector2(5, 5))
    
    # Manual assertions
    assert(result == true, "Building should place")
    
    # Complex teardown
    test_scene.queue_free()
    await get_tree().process_frame

After (Simple C# Tests)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// New: Pure C# unit tests
[Fact]
public void PlacementService_ValidPlaceable_ReturnsSuccess()
{
    // Simple setup - no Godot needed
    var validator = new Mock<IPlacementValidator>();
    validator.Setup(v => v.ValidatePlacement(It.IsAny<Placeable>(), It.IsAny<Vector2>(), It.IsAny<GBOwner>()))
        .Returns(ValidationResult.Success());
    
    var service = new PlacementService(validator.Object, ...);
    
    // Act
    var result = service.ExecutePlacement(placeable, position, placer);
    
    // Assert
    Assert.True(result.IsSuccess);
}

Testing Benefits:

  • Faster test execution without Godot dependencies
  • Simple setup - no complex scene preparation
  • Easy mocking of dependencies
  • CI/CD compatible - can run on any system

3. Code Organization

Before (Mixed Concerns)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Old: Everything mixed together
extends Node
class_name BuildingManager

# Building logic
func place_building(type, pos): # ...

# UI logic  
func update_building_ui(): # ...

# Save/load logic
func save_buildings(): # ...

# Network logic
func sync_buildings(): # ...

# Sound effects
func play_building_sound(): # ...

After (Clean Separation)

1
2
3
4
5
6
// New: Single responsibility services
public class PlacementService     // Building placement only
public class ManipulationService  // Building removal only  
public class SaveService          // Save/load only
public class NetworkService       // Network sync only
public class AudioService         // Sound effects only

4. Engine Portability

Before (Godot Lock-in)

1
2
3
4
5
6
7
# Old: Only works in Godot
extends Node

func place_building(type, pos):
    var scene = load("res://buildings/" + type + ".tscn")  # Godot-specific
    var building = scene.instantiate()                     # Godot-specific
    add_child(building)                                     # Godot-specific

After (Engine Agnostic)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// New: Works with any engine
public class PlacementService
{
    public PlacementResult ExecutePlacement(Placeable placeable, Vector2 position, GBOwner placer)
    {
        // Pure C# - works in Godot, Unity, or any engine
        return ValidateAndPlace(placeable, position, placer);
    }
}

// Engine-specific adapters
public class GodotPlacementAdapter : Node
{
    public void HandlePlacementResult(PlacementResult result)
    {
        // Godot-specific implementation
        var scene = GD.Load<PackedScene>(result.ScenePath);
        var node = scene.Instantiate();
        AddChild(node);
    }
}

public class UnityPlacementAdapter : MonoBehaviour
{
    public void HandlePlacementResult(PlacementResult result)
    {
        // Unity-specific implementation
        var prefab = Resources.Load<GameObject>(result.ScenePath);
        var instance = Instantiate(prefab);
        instance.transform.position = result.Position.ToUnity();
    }
}

Migration Benefits for Developers

1. Simpler GDScript Code

Your game code becomes much cleaner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Before: Complex GDScript
extends Node

func place_building(type, pos):
    # 50+ lines of complex logic
    if not validate_grid(pos):
        return false
    if not check_resources(type):
        return false
    if not check_collision(pos):
        return false
    # ... more logic
    var scene = load("res://buildings/" + type + ".tscn")
    var building = scene.instantiate()
    add_child(building)

# After: Clean service calls
extends Node

func place_building(type, pos):
    var result = $PlacementSystem.place_building(type, pos)
    if result.success:
        show_success_effect()

2. Better Error Handling

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Before: Manual error checking
func place_building(type, pos):
    var result = complex_placement_logic(type, pos)
    if result == null:
        print("Something went wrong")  # Vague error
    elif not result.success:
        print("Failed")  # Still vague

# After: Clear error messages
func place_building(type, pos):
    var result = $PlacementSystem.place_building(type, pos)
    if not result.success:
        match result.error_type:
            "POSITION_OCCUPIED":
                show_error("This position is already occupied!")
            "INSUFFICIENT_RESOURCES":
                show_error("Need " + str(result.required_resources) " more resources")
            "INVALID_POSITION":
                show_error("Cannot build outside the grid!")

3. Easier Debugging

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Before: Hard to debug GDScript
func place_building(type, pos):
    # Which part failed? Who knows!
    var complex_result = do_complex_stuff(type, pos)
    return complex_result

# After: Clear debugging info
func place_building(type, pos):
    var result = $PlacementSystem.place_building(type, pos)
    
    # Rich debugging information
    print("Placement attempt: ", result.debug_info)
    print("Validation time: ", result.performance.validation_time_ms, "ms")
    print("Collision checks: ", result.performance.collision_checks_count)

Real-World Performance Comparison

Building 100 Buildings

Before (GDScript)

Performance: Generally fine for normal use cases
Code Organization: Complex mixed concerns in single classes
Testing: Difficult to test without full Godot environment

After (POCS)

Performance: Maintains good performance with cleaner code
Code Organization: Clean separation of services and responsibilities
Testing: Easy unit testing without Godot dependencies

Startup Time

Before (GDScript)

Initialization: Standard Godot node setup
Code Organization: Everything in large monolithic classes
Development: Complex debugging in mixed-concern code

After (POCS)

Initialization: Service registration and setup
Code Organization: Clean, focused service classes
Development: Easier debugging with separated concerns

Future Benefits

1. Easy Feature Addition

Adding new building features is now simple:

1
2
3
4
5
6
7
8
// Add new service for new feature
public class BuildingUpgradeService : IBuildingUpgradeService
{
    public UpgradeResult UpgradeBuilding(string buildingId, UpgradeType upgrade)
    {
        // Clean, isolated logic
    }
}

2. Multi-Engine Support

Same core services can power different engines:

1
2
3
4
5
6
7
8
// Core logic shared
var placementService = new PlacementService(...);

// Godot adapter
var godotAdapter = new GodotPlacementAdapter(placementService);

// Unity adapter  
var unityAdapter = new UnityPlacementAdapter(placementService);

3. Advanced Testing

Complex scenarios are easy to test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Fact]
public void StressTest_1000Placements_CompletesUnder1Second()
{
    var service = new PlacementService(...);
    var stopwatch = Stopwatch.StartNew();
    
    for (int i = 0; i < 1000; i++)
    {
        var result = service.ExecutePlacement(placeable, position, placer);
        Assert.True(result.IsSuccess);
    }
    
    stopwatch.Stop();
    Assert.True(stopwatch.ElapsedMilliseconds < 1000);
}

Migration Summary

AspectBefore (GDScript)After (POCS)Improvement
PerformanceGDScript execution speedOptimized C# servicesImproved performance with better algorithms
TestingComplex GDUnit setupSimple C# unit testsEasier and faster testing
Code QualityMixed concerns in large classesClean service separationBetter maintainability
Engine SupportGodot-only implementationEngine-agnostic coreMulti-engine compatibility
Error HandlingManual error checkingRich error informationBetter debugging experience
Memory UsageGDScript memory managementOptimized C# collectionsMore efficient memory usage
Development SpeedComplex GDScript debuggingSimplified service callsFaster development workflow

What This Means for You

Immediate Benefits

  • Faster Development: Your GDScript code is simpler and cleaner
  • Better Performance: Improved algorithms and optimized data structures
  • Easier Debugging: Clear error messages and better error information
  • Reliable Building: Cleaner architecture reduces bugs and edge cases

Long-term Benefits

  • Future-Proof: Easy to add new features and engines
  • Professional Quality: Industry-standard architecture
  • Team Ready: Easy for other developers to understand
  • Scalable: Handles large projects without performance issues

The evolution from GDScript-heavy to POCS architecture represents a fundamental improvement in how building systems are developed. You get the power and performance of C# while keeping your Godot code simple and focused on game logic.

This isn’t just an architectural change - it’s a complete transformation in development experience and code quality!

State Management Migration: Owner Context to Service Instance

The Problem with Owner Context Pattern

Before: Owner Context Dependencies

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Old: State with problematic owner context
public class GridTargetingState : IGridTargetingState
{
    private IOwnerContext? _ownerContext;  // ❌ Non-existent interface
    private object? _collider;
    private object? _target;
    
    // Constructor required owner context
    public GridTargetingState(IOwnerContext ownerContext)
    {
        _ownerContext = ownerContext;  // ❌ Creates tight coupling
    }
    
    // Methods depended on owner context
    public bool ValidateState()
    {
        return _ownerContext != null && _targetMap != null;  // ❌ brittle
    }
}

Problems with Owner Context:

  • Tight Coupling: State objects bound to specific owner implementations
  • Testing Complexity: Need to mock or create owner contexts for tests
  • Engine Dependencies: Owner contexts often contain engine-specific data
  • Scope Confusion: Unclear which “owner” applies in complex scenarios

The Solution: Service Instance Scoping

After: Service Instance Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// New: Clean state without owner dependencies
public class GridTargetingState : IGridTargetingState
{
    // ✅ Pure data - no owner context needed
    private object? _collider;
    private object? _target;
    private Vector2I _currentTargetTile = Vector2I.Zero;
    private bool _isManualTargetingActive = false;
    
    // ✅ Simple constructor
    public GridTargetingState()
    {
        _lastUpdated = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    }
    
    // ✅ Clean validation without owner dependencies
    public bool ValidateState()
    {
        return _targetMap != null;  // Simple, testable
    }
}

// Service manages scope through instance isolation
public class GridTargetingService : IGridTargetingService
{
    // ✅ Service instance = scope boundary
    private readonly Dictionary<object, Vector2I> _componentStates = new();
    private Vector2I _currentTargetTile = Vector2I.Zero;
    
    // ✅ Component-based tracking instead of owner context
    public void SetTargetTile(Vector2I position)
    {
        var oldTarget = _currentTargetTile;
        _currentTargetTile = position;
        
        // ✅ Event-driven state changes
        TargetingStateChanged?.Invoke(this, new TargetingStateChangedEventArgs(...));
    }
}

Migration Benefits

1. Cleaner Architecture

1
2
3
4
5
6
7
// Before: Complex owner relationships
var state = new GridTargetingState(someComplexOwnerContext);
state.SetTarget(someTarget); // Owner context affects behavior

// After: Simple service scoping
var targetingService = new GridTargetingService(); // Service = scope
targetingService.SetTargetTile(position); // Clear, predictable behavior

2. Better Testing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Before: Complex test setup
[Fact]
public void TestTargetingState()
{
    var mockOwner = new Mock<IOwnerContext>(); // Complex mocking
    mockOwner.Setup(o => o.IsValid).Returns(true);
    var state = new GridTargetingState(mockOwner.Object);
    // ... complex setup continues
}

// After: Simple test setup
[Fact]
public void TestTargetingState()
{
    var state = new GridTargetingState(); // ✅ Simple construction
    var result = state.SetTarget(target);  // ✅ Direct testing
    Assert.Equal(target, result);
}

3. Engine Agnostic Design

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Before: Owner context tied to engine
public interface IOwnerContext
{
    Godot.Node GetGodotNode();  // ❌ Engine-specific
    Unity.MonoBehaviour GetUnityComponent(); // ❌ Engine-specific
}

// After: Service pattern works with any engine
public class GridTargetingService
{
    private readonly Dictionary<object, Vector2I> _componentStates;
    
    // ✅ Works with Godot, Unity, or any engine
    public void RegisterComponent(object component, Vector2I position)
    {
        _componentStates[component] = position;
    }
}

4. Clear Scope Management

1
2
3
4
5
6
7
8
9
// Before: Unclear scope boundaries
var owner1 = new PlayerOwnerContext();
var owner2 = new AIOwnerContext();
var state = new GridTargetingState(owner1); // Which scope applies?

// After: Clear service instance boundaries
var playerTargeting = new GridTargetingService(); // Player scope
var aiTargeting = new GridTargetingService();     // AI scope
// Each service manages its own state independently

Implementation Patterns

1. Service Instance = Scope

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Each service instance provides natural isolation
public class GridTargetingService
{
    // Service-level state management
    private readonly Dictionary<object, Vector2I> _componentStates = new();
    
    // Multiple services = multiple scopes
    public static GridTargetingService CreatePlayerScope() => new();
    public static GridTargetingService CreateAIScope() => new();
}

2. Component-Based Tracking

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Track components by object reference, not owner hierarchy
private readonly Dictionary<object, Vector2I> _componentStates = new();

// Works with any component type
public void RegisterComponent(object component, Vector2I position)
{
    _componentStates[component] = position;
}

// Engine-agnostic component identification
public Vector2I GetComponentPosition(object component)
{
    return _componentStates.TryGetValue(component, out var pos) ? pos : Vector2I.Zero;
}

3. Event-Driven Updates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// State changes communicated through events
public event EventHandler<TargetingStateChangedEventArgs>? TargetingStateChanged;

public void SetTargetTile(Vector2I position)
{
    var oldTarget = _currentTargetTile;
    _currentTargetTile = position;
    
    // Notify interested parties without owner coupling
    TargetingStateChanged?.Invoke(this, new TargetingStateChangedEventArgs(oldTarget, _currentTargetTile));
}

Migration Checklist

What We Fixed

  • Removed IOwnerContext dependency from GridTargetingState
  • Implemented explicit interface methods for property conflicts
  • Added proper event implementations
  • Simplified validation logic

Architecture Consistency

  • All Core states now follow same pattern: pure data containers
  • Service instances provide scope boundaries
  • Component-based tracking replaces owner hierarchies
  • Event-driven communication replaces direct owner calls

Testing Improvements

  • Simple construction without complex dependencies
  • Direct method testing without mocking
  • Clear isolation between test scenarios
  • Engine-agnostic test setup

Real-World Impact

Before Migration

1
2
3
4
// Complex, tightly coupled code
var ownerContext = new ComplexOwnerContext(player, grid, ui, network);
var state = new GridTargetingState(ownerContext);
var result = state.ValidateState(); // Depends on multiple systems

After Migration

1
2
3
4
// Simple, decoupled code
var targetingService = new GridTargetingService(); // Service = scope
targetingService.UpdateGridNavigation(gridData);   // Clear dependencies
var result = targetingService.ValidatePosition(position); // Predictable behavior

Key Insight: Service instance boundaries provide cleaner scope management than owner context hierarchies.

This migration represents a fundamental improvement in state management architecture - moving from complex owner relationships to simple, predictable service instance scoping that works across any engine or framework.