chore: define dir structure

This commit is contained in:
2026-08-20 20:48:10 +02:00
parent c79aaa873c
commit be04f92260
14 changed files with 281 additions and 17 deletions
+258
View File
@@ -0,0 +1,258 @@
# 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);
}
}
```
---
### 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`.
+8
View File
@@ -0,0 +1,8 @@
using Godot;
namespace Game.Bootstrap;
public partial class GameBootstrap : Node
{
}
@@ -1,8 +0,0 @@
using Game.Features.Player.Godot;
using Godot;
using System;
public partial class GameWorld : Node
{
}
@@ -1,7 +1,7 @@
[gd_scene format=3 uid="uid://c44pljehe0qun"] [gd_scene format=3 uid="uid://c44pljehe0qun"]
[ext_resource type="Texture2D" uid="uid://b5i827vmueyei" path="res://assets/textures/skyboxes/skybox_emx_19.png" id="1_g4n7o"] [ext_resource type="Texture2D" uid="uid://b5i827vmueyei" path="res://assets/textures/skyboxes/skybox_emx_19.png" id="1_g4n7o"]
[ext_resource type="PackedScene" uid="uid://curuqmbndymi6" path="res://src/Infrastructure/Floor/FloorCeilingHr42.tscn" id="2_1kpcd"] [ext_resource type="PackedScene" uid="uid://curuqmbndymi6" path="res://src/Scenes/FloorCeilingHr42.tscn" id="2_1kpcd"]
[ext_resource type="PackedScene" uid="uid://bka1dmgdi5btd" path="res://src/Features/Player/Godot/PlayerNode.tscn" id="2_mvtit"] [ext_resource type="PackedScene" uid="uid://bka1dmgdi5btd" path="res://src/Features/Player/Godot/PlayerNode.tscn" id="2_mvtit"]
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"] [sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_epykj"]
+5
View File
@@ -0,0 +1,5 @@
using System;
namespace Game.Shared.Domain;
public readonly record struct EntityId(Guid Value);
+6
View File
@@ -0,0 +1,6 @@
namespace Game.Shared.Events.Contracts;
public interface IEvent
{
}
@@ -1,6 +1,6 @@
using System; using System;
namespace Game.Shared.Events; namespace Game.Shared.Events.Contracts;
public interface IEventBus public interface IEventBus
{ {
-6
View File
@@ -1,6 +0,0 @@
namespace Game.Shared.Events;
public interface IEvent
{
}
@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Game.Shared.Events.Contracts;
namespace Game.Shared.Events; namespace Game.Shared.Events.Infrastructure;
public sealed class InMemoryEventBus : IEventBus public sealed class InMemoryEventBus : IEventBus
{ {