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
- Define
IBuildingService, IManipulationService, etc. - Define event classes
- Create service interfaces in
Services/ folder
Phase 2: Implement Services
- Move business logic from state classes to service classes
- Remove events from state classes
- Add event dispatching to service classes
Phase 3: Update Godot Layer
- Change Godot classes to use services instead of state
- Update event subscriptions to use service events
- Update method calls to use service methods
Phase 4: Clean Up
- Remove old event code from state classes
- Remove context objects that are now services
- 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.