260 lines
6.5 KiB
Markdown
260 lines
6.5 KiB
Markdown
# Architektura projektu
|
|
|
|
## 1. Cel i zasada zależności
|
|
|
|
Architektura ma być prosta do rozwijania, modularna, testowalna i zorganizowana wokół feature'ów, a nie globalnych warstw technicznych.
|
|
|
|
Kierunek zależności jest zawsze taki sam:
|
|
|
|
```text
|
|
Godot -> Application -> Domain
|
|
```
|
|
|
|
```text
|
|
Domain -> Godot DOBRZE (domena nigdy nie zna silnika)
|
|
Godot -> Domain ŹLE (silnik wywołuje domenę)
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Struktura katalogów
|
|
|
|
```text
|
|
src/
|
|
├── Features/
|
|
│ ├── Player/
|
|
│ │ ├── Contracts/
|
|
│ │ ├── Domain/
|
|
│ │ ├── Application/
|
|
│ │ └── Godot/
|
|
│ └── ...
|
|
│
|
|
├── Shared/
|
|
│ ├── Domain/
|
|
│ └── Events/
|
|
│ ├── Contracts/
|
|
│ └── Infrastructure/
|
|
│
|
|
├── Bootstrap/
|
|
│
|
|
└── Scenes/
|
|
```
|
|
|
|
Nie każdy feature potrzebuje wszystkich czterech folderów. Mały feature może mieć tylko np. `Domain/` + `Godot/`. Foldery dokłada się dopiero, gdy pojawia się realna potrzeba.
|
|
|
|
---
|
|
|
|
## 3. Warstwy architektury
|
|
|
|
### 3.1 `Domain`
|
|
|
|
Właściwa logika i reguły gry. Zwykłe klasy C#, bez żadnej wiedzy o silniku.
|
|
|
|
Wrzucamy tam encje, value obiekty, reguły, inwarianty, obliczenia (`Health`, `PlayerData`, `Norma`, `Zgloszenie`).
|
|
|
|
Unikamy `Node`, sceny, input, UI, zapis do pliku - `Domain` o tym nie wie.
|
|
|
|
Wyjątkiem są matematyczne value type'y Godota (`Vector2`, `Vector3`, `Mathf`) - one są dozwolone.
|
|
|
|
```csharp
|
|
namespace Game.Features.Player.Domain;
|
|
|
|
public sealed class Health
|
|
{
|
|
public int Current { get; private set; }
|
|
public int Max { get; }
|
|
|
|
public bool IsDead => Current <= 0;
|
|
|
|
public Health(int max)
|
|
{
|
|
Max = max;
|
|
Current = max;
|
|
}
|
|
|
|
public void TakeDamage(int amount)
|
|
{
|
|
Current = Math.Max(0, Current - amount);
|
|
}
|
|
}
|
|
```
|
|
|
|
Nazewnictwo feature to zazwyczaj encja w liczbie mnogiej (`Players`, `Enemies` itd.). Gdy liczba mnoga nie brzmi naturalnie, nadajemy klasie nazwę opisującą jej rolę zamiast powtarzać nazwę feature'a - np. feature `Terminal` trzyma klasę `Report`, (zamiast `Terminal.Terminal`).
|
|
|
|
---
|
|
|
|
### 3.2 `Application`
|
|
|
|
Koordynacja przypadków użycia (use case'ów) - logika, która dotyka kilku obiektów domenowych, korzysta z portów albo publikuje eventy.
|
|
|
|
Wrzucamy klasy typu `SpawnEnemy`, `FindNearestEnemy` - jedna klasa = jeden przypadek użycia.
|
|
|
|
```csharp
|
|
namespace Game.Features.Combat.Application;
|
|
|
|
public sealed class Attack
|
|
{
|
|
public void Execute(Character attacker, IDamageable target)
|
|
{
|
|
if (!attacker.CanAttack) return;
|
|
|
|
target.TakeDamage(attacker.CalculateDamage());
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.3 `Contracts`
|
|
|
|
Publiczne API feature'a. Jedyna część feature'a, którą wolno importować z zewnątrz.
|
|
|
|
Wrzucamy identyfikatory, DTO/read-modele, eventy, małe interfejsy (porty publiczne).
|
|
|
|
Unikamy logiki. Kontrakt to tylko kształt danych i sygnatury - nie implementacja.
|
|
|
|
```csharp
|
|
namespace Game.Features.Enemies.Contracts;
|
|
|
|
public readonly record struct EnemyId(Guid Value);
|
|
|
|
public sealed record EnemyInfo(EnemyId Id, Vector2 Position, bool IsAlive);
|
|
|
|
public interface IEnemyQuery
|
|
{
|
|
IReadOnlyCollection<EnemyInfo> GetAliveEnemies();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.4 `Godot`
|
|
|
|
Adapter między silnikiem a grą. Cienka warstwa łącząca `Node`y z logiką w `Domain`/`Application`.
|
|
|
|
Wrzucamy wszystko co związane z silnikiem gry: `Node`, `CharacterBody3D`, `Area3D`, `AnimationPlayer`, obsługę inputu, sygnały, sceny.
|
|
|
|
Unikamy logiki gry. `Node` tylko woła `Domain`/`Application` i mapuje wyniki z powrotem na silnik.
|
|
|
|
```csharp
|
|
namespace Game.Features.Player.Godot;
|
|
|
|
public partial class PlayerController : Node3D
|
|
{
|
|
[Export] public CharacterBody3D Body;
|
|
|
|
private Health _health = null!;
|
|
|
|
public void Initialize(Health health)
|
|
{
|
|
_health = health;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.5 `Shared`
|
|
|
|
Wspólne, stabilne koncepty używane przez wiele feature'ów.
|
|
|
|
Proste value obiekty lądują płasko w `Shared/Domain`:
|
|
|
|
```csharp
|
|
namespace Game.Shared.Domain;
|
|
|
|
public readonly record struct EntityId(Guid Value);
|
|
```
|
|
|
|
Jeśli współdzielony koncept ma też port + implementację (np. event bus, z którego korzysta wiele feature'ów), dostaje własny podfolder z tym samym podziałem `Contracts`/`Infrastructure`, co niżej:
|
|
|
|
```text
|
|
Shared/
|
|
└── Events/
|
|
├── Contracts/
|
|
│ ├── IEvent.cs
|
|
│ └── IEventBus.cs
|
|
└── Infrastructure/
|
|
└── InMemoryEventBus.cs
|
|
```
|
|
|
|
---
|
|
|
|
### 3.6 `Infrastructure`
|
|
|
|
Implementacja portu z `Contracts`.
|
|
|
|
Wrzucamy konkretne, techniczne implementacje interfejsów zdefiniowanych w `Contracts`.
|
|
|
|
Unikamy logiki domenowej i unikamy trzymania w tym folderze samego portu (interfejs zawsze zostaje w `Contracts`).
|
|
|
|
```csharp
|
|
namespace Game.Shared.Events.Infrastructure;
|
|
|
|
public sealed class InMemoryEventBus : IEventBus
|
|
{
|
|
// implementacja portu
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.7 `Bootstrap`
|
|
|
|
Wiring implementacji portów w całość.
|
|
|
|
Wrzucamy skrypt(y) spinające całą grę: tworzenie konkretnych implementacji portów, `GetNode<T>()` po dzieci sceny głównej, wywołania `Initialize(...)`.
|
|
|
|
Unikamy logiki feature'a (to należy do `Godot/` danego feature'a) i lokalnych, jedno-portowych implementacji (to należy do `Infrastructure/` danego modułu).
|
|
|
|
```csharp
|
|
namespace Game.Bootstrap;
|
|
|
|
public partial class GameBootstrap : Node
|
|
{
|
|
public override void _Ready()
|
|
{
|
|
var eventBus = new InMemoryEventBus();
|
|
|
|
var player = GetNode<PlayerController>("Player");
|
|
player.Initialize(eventBus);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.8 `Scenes`
|
|
|
|
Sceny i geometria świata, które same w sobie nie niosą logiki gry - poziomy, propy, statyczne elementy środowiska.
|
|
|
|
Wrzucamy pliki `.tscn`, niepowiązane bezpośrednio z jednym feature'em.
|
|
|
|
Unikamy logiki - jeśli scena zaczyna mieć skrypt z regułami gry, ten skrypt trafia do odpowiedniego feature'a (`Godot/`), nie tutaj.
|
|
|
|
---
|
|
|
|
## 4. Zależności między feature'ami
|
|
|
|
Zasada: **feature A może importować tylko `FeatureB.Contracts`.**
|
|
|
|
```text
|
|
Targeting.Application -> Enemies.Contracts DOBRZE
|
|
Loot.Application -> Enemies.Contracts DOBRZE
|
|
|
|
Targeting -> Enemies.Domain ŹLE
|
|
Loot -> Enemies.Application ŹLE
|
|
Combat -> Enemies.Godot ŹLE
|
|
```
|
|
|
|
### Eventy między feature'ami
|
|
|
|
Gdy jedno zdarzenie interesuje wiele feature'ów, feature źródłowy publikuje event i nie wie, kto go słucha:
|
|
|
|
```text
|
|
Enemies -> EnemyKilled -> Loot
|
|
-> Quests
|
|
-> Score
|
|
```
|
|
|
|
`Enemies` nie zna `Loot`, `Quests` ani `Score`. |