Service-Based Architecture

πŸ—οΈ
GridPlacement 6.0 vs GridBuilding 5.x
This page documents the C# service + adapter architecture introduced in GridPlacement 6.0 (formerly GridBuilding). The legacy 5.x GDScript plugin is still available but treated as legacy / maintenance–only.
Canonical version: 6.0 Β· C#
Legacy line: 5.x Β· GDScript
For 5.x behavior or migration notes, see the legacy sections and 5.x release notes. New work should target the 6.0 C# architecture.

🎯 Overview

This document describes the refined service-based architecture that separates state data from business logic and event handling. This creates a cleaner separation of concerns and improves testability.

πŸ—οΈ Architecture Principles

1. State = Pure Data Only

  • State classes contain ONLY data (no logic, no events)
  • Serializable and testable in isolation
  • No engine dependencies
  • No business logic or validation

2. Services = Business Logic + Events

  • Services contain all business logic
  • Services handle state validation and transitions
  • Services dispatch events and signals
  • Services coordinate between multiple state objects

3. Clear Data Flow

  • Godot ↔ Services ↔ State
  • Godot never directly accesses State
  • All communication goes through Services
  • Events flow from Services to Godot

πŸ“Š New Folder Structure

 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
49
50
51
52
53
54
55
Core/
β”œβ”€β”€ State/                    # PURE DATA ONLY
β”‚   β”œβ”€β”€ Building/
β”‚   β”‚   └── BuildingState.cs
β”‚   β”œβ”€β”€ Manipulation/
β”‚   β”‚   └── ManipulationState.cs
β”‚   β”œβ”€β”€ Targeting/
β”‚   β”‚   └── TargetingState.cs
β”‚   β”œβ”€β”€ User/
β”‚   β”‚   └── UserState.cs
β”‚   β”œβ”€β”€ Mode/
β”‚   β”‚   └── ModeState.cs
β”‚   └── IState.cs            # Base state interface
β”‚
β”œβ”€β”€ Services/                   # SERVICES: Business logic + events
β”‚   β”œβ”€β”€ Building/              # Building operations and events
β”‚   β”‚   β”œβ”€β”€ IBuildingService.cs
β”‚   β”‚   β”œβ”€β”€ BuildingService.cs
β”‚   β”‚   └── BuildingEvents.cs
β”‚   β”œβ”€β”€ Manipulation/          # Manipulation operations and events
β”‚   β”‚   β”œβ”€β”€ IManipulationService.cs
β”‚   β”‚   β”œβ”€β”€ ManipulationService.cs
β”‚   β”‚   └── ManipulationEvents.cs
β”‚   β”œβ”€β”€ Targeting/             # Targeting operations and events
β”‚   β”‚   β”œβ”€β”€ ITargetingService.cs
β”‚   β”‚   β”œβ”€β”€ TargetingService.cs
β”‚   β”‚   └── TargetingEvents.cs
β”‚   β”œβ”€β”€ Input/                 # Input processing and events
β”‚   β”‚   β”œβ”€β”€ IInputService.cs
β”‚   β”‚   β”œβ”€β”€ InputService.cs
β”‚   β”‚   └── InputEvents.cs
β”‚   β”œβ”€β”€ EventDispatcher.cs     # Central event coordination
β”‚   β”œβ”€β”€ ICalculator.cs         # Calculator interfaces
β”‚   └── IGeometryCalculator.cs # Geometry calculation interfaces
β”‚
β”œβ”€β”€ Systems/                  # CORE SYSTEMS (unchanged)
β”‚   β”œβ”€β”€ Building/
β”‚   β”œβ”€β”€ Collision/
β”‚   β”œβ”€β”€ Components/
β”‚   β”œβ”€β”€ Configuration/
β”‚   β”œβ”€β”€ Data/
β”‚   β”œβ”€β”€ Geometry/
β”‚   β”œβ”€β”€ Grid/
β”‚   β”œβ”€β”€ Input/
β”‚   β”œβ”€β”€ InputManager.cs        # Input management system
β”‚   β”œβ”€β”€ Logging/
β”‚   β”œβ”€β”€ Manipulation/
β”‚   β”œβ”€β”€ Placement/
β”‚   β”œβ”€β”€ State/
β”‚   β”œβ”€β”€ Targeting/
β”‚   └── Validation/
β”‚
β”œβ”€β”€ Grid/                     # GRID INTEGRATION (unchanged)
β”œβ”€β”€ Interfaces/               # INTERFACES (unchanged)
└── Common/                   # UTILITIES (unchanged)

πŸ”„ Data Flow Architecture

Godot β†’ Core β†’ Godot Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Godot Layer   β”‚    β”‚  Service Layer  β”‚    β”‚   State Layer   β”‚
β”‚                 β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚ User Input      │───▢│ InputService   │───▢│ UserState       β”‚
β”‚ Mouse Click     β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚ Key Press       β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚                 β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚ UI Updates      │◀───│ BuildingEvents │◀───│ BuildingState   β”‚
β”‚ Visual Changes  β”‚    β”‚ Manipulation   β”‚    β”‚                 β”‚
β”‚ Audio Feedback  β”‚    β”‚ Events         β”‚    β”‚                 β”‚
β”‚                 β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚ Godot Nodes     │───▢│ BuildingService│───▢│ BuildingState   β”‚
β”‚ Scene Objects   β”‚    β”‚ Manipulation   β”‚    β”‚                 β”‚
β”‚ Components      β”‚    β”‚ Service        β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Detailed Flow Examples

1. Building Placement Flow

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
// Godot Layer
public class GridBuildingNode : Node
{
    private IBuildingService _buildingService;
    
    public override void _Ready()
    {
        // Connect to SERVICE events, not state
        _buildingService.BuildingPlaced += OnBuildingPlaced;
        _buildingService.BuildingFailed += OnBuildingFailed;
    }
    
    public void OnUserClick(Vector2I gridPos)
    {
        // Call SERVICE method
        _buildingService.PlaceBuilding(gridPos, "house");
    }
    
    private void OnBuildingPlaced(BuildingPlacedEvent args)
    {
        // Update visuals based on event data
        SpawnBuildingVisual(args.BuildingState);
    }
}

// Service Layer
public class BuildingService : IBuildingService
{
    private readonly BuildingState _state;
    private readonly IEventDispatcher _events;
    
    public event EventHandler<BuildingPlacedEvent> BuildingPlaced;
    public event EventHandler<BuildingFailedEvent> BuildingFailed;
    
    public void PlaceBuilding(Vector2I position, string buildingType)
    {
        // Business logic here
        if (!CanPlaceBuilding(position, buildingType))
        {
            BuildingFailed?.Invoke(this, new BuildingFailedEvent(...));
            return;
        }
        
        // Update STATE (pure data)
        _state.Position = position;
        _state.BuildingType = buildingType;
        _state.Status = BuildingStatus.Placed;
        
        // Dispatch EVENT
        BuildingPlaced?.Invoke(this, new BuildingPlacedEvent(_state));
    }
}

// State Layer (PURE DATA)
public class BuildingState
{
    public Vector2I Position { get; set; }
    public string BuildingType { get; set; }
    public BuildingStatus Status { get; set; }
    // NO EVENTS, NO LOGIC
}

2. Manipulation Flow

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// Godot Layer
public class ManipulationController : Node
{
    private IManipulationService _manipulationService;
    
    public override void _Ready()
    {
        // Connect to service events
        _manipulationService.ManipulationStarted += OnManipulationStarted;
        _manipulationService.ManipulationUpdated += OnManipulationUpdated;
        _manipulationService.ManipulationCompleted += OnManipulationCompleted;
    }
    
    public void StartManipulation(Vector2I startPos)
    {
        _manipulationService.StartManipulation(ManipulationMode.Move, startPos);
    }
}

// Service Layer
public class ManipulationService : IManipulationService
{
    private readonly ManipulationState _state;
    private readonly ITargetingService _targetingService;
    
    public event EventHandler<ManipulationStartedEvent> ManipulationStarted;
    public event EventHandler<ManipulationUpdatedEvent> ManipulationUpdated;
    public event EventHandler<ManipulationCompletedEvent> ManipulationCompleted;
    
    public void StartManipulation(ManipulationMode mode, Vector2I origin)
    {
        // Business logic
        _state.CurrentMode = mode;
        _state.GridOrigin = origin;
        _state.Phase = ManipulationPhase.Active;
        _state.IsActive = true;
        
        // Event dispatch
        ManipulationStarted?.Invoke(this, new ManipulationStartedEvent(_state));
    }
    
    public void UpdateManipulation(Vector2I targetPosition)
    {
        // Update state
        _state.GridTarget = targetPosition;
        _state.AffectedTiles = CalculateAffectedTiles(origin, targetPosition);
        
        // Event dispatch
        ManipulationUpdated?.Invoke(this, new ManipulationUpdatedEvent(_state));
    }
}

// State Layer (PURE DATA)
public class ManipulationState
{
    public ManipulationMode CurrentMode { get; set; }
    public Vector2I GridOrigin { get; set; }
    public Vector2I GridTarget { get; set; }
    public List<Vector2I> AffectedTiles { get; set; }
    public ManipulationPhase Phase { get; set; }
    public bool IsActive { get; set; }
    // NO EVENTS, NO LOGIC
}

πŸ”Œ Service Interface Design

Standard Service Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public interface IBuildingService
{
    // Events
    event EventHandler<BuildingPlacedEvent> BuildingPlaced;
    event EventHandler<BuildingFailedEvent> BuildingFailed;
    event EventHandler<BuildingRemovedEvent> BuildingRemoved;
    
    // Commands (state-changing operations)
    void PlaceBuilding(Vector2I position, string buildingType);
    void RemoveBuilding(string buildingId);
    void MoveBuilding(string buildingId, Vector2I newPosition);
    
    // Queries (read-only operations)
    bool CanPlaceBuilding(Vector2I position, string buildingType);
    BuildingState GetBuilding(string buildingId);
    IEnumerable<BuildingState> GetAllBuildings();
}

Event Design 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
// Base event class
public abstract class GameEvent
{
    public DateTime Timestamp { get; }
    public string Source { get; }
    
    protected GameEvent(string source)
    {
        Timestamp = DateTime.Now;
        Source = source;
    }
}

// Specific event classes
public class BuildingPlacedEvent : GameEvent
{
    public BuildingState BuildingState { get; }
    
    public BuildingPlacedEvent(BuildingState buildingState) 
        : base("BuildingService")
    {
        BuildingState = buildingState;
    }
}

public class ManipulationUpdatedEvent : GameEvent
{
    public ManipulationState ManipulationState { get; }
    public Vector2I CurrentPosition { get; }
    
    public ManipulationUpdatedEvent(ManipulationState state) 
        : base("ManipulationService")
    {
        ManipulationState = state;
        CurrentPosition = state.GridTarget;
    }
}

🎯 Godot Implementation Changes

Before (State-Based)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ Direct state access
public class GridBuildingNode : Node
{
    private BuildingState _buildingState;
    
    public override void _Ready()
    {
        // Connect directly to state events
        _buildingState.PropertyChanged += OnBuildingStateChanged;
    }
}

After (Service-Based)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// βœ… Service-based access
public class GridBuildingNode : Node
{
    private IBuildingService _buildingService;
    private IManipulationService _manipulationService;
    private IInputService _inputService;
    
    public override void _Ready()
    {
        // Connect to service events
        _buildingService.BuildingPlaced += OnBuildingPlaced;
        _manipulationService.ManipulationUpdated += OnManipulationUpdated;
        _inputService.InputReceived += OnInputReceived;
    }
    
    public void OnUserInput(InputEvent inputEvent)
    {
        // Send to service, not state
        _inputService.ProcessInput(inputEvent);
    }
}

πŸ§ͺ Testing Benefits

State Testing (Pure Data)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[Test]
public void BuildingState_ShouldStorePosition()
{
    // Arrange
    var state = new BuildingState();
    
    // Act
    state.Position = new Vector2I(5, 3);
    
    // Assert
    Assert.AreEqual(new Vector2I(5, 3), state.Position);
}

Service Testing (Business Logic)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Test]
public void BuildingService_ShouldFailPlacement_WhenInvalidPosition()
{
    // Arrange
    var mockValidator = new Mock<IPlacementValidator>();
    mockValidator.Setup(x => x.CanPlace(It.IsAny<Vector2I>())).Returns(false);
    
    var service = new BuildingService(mockValidator.Object);
    
    // Act & Assert
    Assert.Throws<PlacementException>(() => 
        service.PlaceBuilding(new Vector2I(5, 3), "house"));
}

πŸ“‹ Migration Steps

Phase 1: Create Service Interfaces

  1. Define IBuildingService, IManipulationService, etc.
  2. Define event classes
  3. Create service interfaces in Services/ folder

Phase 2: Implement Services

  1. Move business logic from state classes to service classes
  2. Remove events from state classes
  3. Add event dispatching to service classes

Phase 3: Update Godot Layer

  1. Change Godot classes to use services instead of state
  2. Update event subscriptions to use service events
  3. Update method calls to use service methods

Phase 4: Clean Up

  1. Remove old event code from state classes
  2. Remove context objects that are now services
  3. Update documentation

🎯 Benefits Summary

Before (Mixed State/Services)

  • ❌ State contains business logic
  • ❌ State dispatches events
  • ❌ Hard to test (mixed concerns)
  • ❌ Godot directly accesses state
  • ❌ Confusing data flow

After (Separated)

  • βœ… State is pure data (easily testable)
  • βœ… Services handle business logic
  • βœ… Services dispatch events
  • βœ… Clear data flow: Godot ↔ Services ↔ State
  • βœ… Better separation of concerns
  • βœ… Easier unit testing
  • βœ… More maintainable code

πŸ”„ Event Flow Diagram

User Input (Godot)
       ↓
InputService.ProcessInput()
       ↓
[Business Logic in Services]
       ↓
State Objects Updated (Pure Data)
       ↓
Events Dispatched (Services)
       ↓
Godot Updates Visuals

This architecture provides a clean separation of concerns while maintaining clear data flow and excellent testability.