Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated server with client-side prediction. Clients send input only; the server resolves every hit for both players and enemies (no PvP). - SimWorld: whole simulation as plain RefCounted objects (no nodes, no physics server), ~0.24ms/tick at peak load -- runs headless for free and drives 78 tests in under a second - BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn events rather than per-tick state - Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash enemies and bosses -- a new boss is data in src/content/content.gd, no simulation changes - The Warden of the Fold: stationary 4-phase boss built entirely on that format - Lobby hub with a portal into on-demand dungeon instances; one process hosts the hub plus every concurrent dungeon - Emergency escape: 3s server-owned channel, cancelled by damage - tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over ENet), bench.gd; git hooks wired to the same scripts - docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# Architecture
|
||||
|
||||
## The shape
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ src/sim/ -- plain RefCounted objects │
|
||||
authoritative │ SimWorld, SimPlayer, SimEnemy, SimBoss, │ replica
|
||||
(server) │ BulletPool, emitters │ (client)
|
||||
│ no nodes · no physics · no rendering │
|
||||
└───────────────┬──────────────────────────┘
|
||||
│
|
||||
┌────────────────────────┼────────────────────────┐
|
||||
│ │ │
|
||||
┌───────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
|
||||
│ src/instances/ │ │ src/net/ │ │ src/view/ ui/ │
|
||||
│ lobby, dungeon │ │ codec, server & │ │ read-only draw │
|
||||
│ progression │ │ client runtimes │ │ │
|
||||
└────────────────┘ └────────┬────────┘ └─────────────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ src/autoload/ │
|
||||
│ net.gd (RPC) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
Dependencies point inwards only. `src/sim/` imports nothing from `net`, `view`,
|
||||
`ui` or `instances`, and never touches the `Net` autoload. That is what makes
|
||||
the simulation runnable in a unit test, in a headless server and inside a client
|
||||
without a single conditional.
|
||||
|
||||
## Why the simulation is not made of nodes
|
||||
|
||||
Godot's instinct is `CharacterBody2D` + `Area2D` + `MultiplayerSynchronizer`.
|
||||
This project deliberately does not, for four reasons:
|
||||
|
||||
1. **Bullet count.** Hundreds of live bullets. A node and an `Area2D` each would
|
||||
cost more than the entire simulation does. `BulletPool` is parallel packed
|
||||
arrays; a tick over ~350 bullets costs 0.24 ms including AI and hit checks.
|
||||
2. **Headless cost.** The dedicated server allocates no nodes and touches no
|
||||
physics server.
|
||||
3. **Testability.** A `SimWorld` is `SimWorld.new()`. The suite drives thousands
|
||||
of ticks in ~2 s with no SceneTree, no awaits and no frame timing.
|
||||
4. **Replication control.** `MultiplayerSynchronizer` is excellent for
|
||||
prototyping and wrong for this shape: it would replicate per-bullet state and
|
||||
saturate the link. See [NETCODE.md](NETCODE.md).
|
||||
|
||||
The trade is that there is no physics engine — no arbitrary collision shapes, no
|
||||
tilemap collision. The arena is a rectangle and every hit is circle-vs-circle.
|
||||
For a bullet hell that is not a limitation; hitboxes in the genre are circles by
|
||||
convention anyway. A dungeon with real geometry would need a static collision
|
||||
representation added to `SimWorld`, not a switch back to nodes.
|
||||
|
||||
## Tick
|
||||
|
||||
Everything runs on Godot's physics tick, pinned to 60 Hz in `src/main.gd`
|
||||
(asserted in code, because `project.godot` drops settings equal to the engine
|
||||
default). `SimConfig` holds every constant both sides must agree on; per-machine
|
||||
options live in `GameOpts` and must never affect the simulation.
|
||||
|
||||
## Content pipeline
|
||||
|
||||
```
|
||||
BulletEmitter (Resource, abstract)
|
||||
├── RingEmitter count, arc, spin per shot -> rings and spirals
|
||||
├── AimedSpreadEmitter fan at nearest player -> punishes standing still
|
||||
├── WallGapEmitter curtain with a sliding gap -> forces a committed dodge
|
||||
└── ArcSweepEmitter rotating arms -> forces tracking
|
||||
|
||||
EnemyDef = stats + movement enum + emitters
|
||||
BossPhase = hp threshold + telegraph + looping timeline of emitters
|
||||
BossDef = stats + ordered phases
|
||||
```
|
||||
|
||||
`SimWorld._run_emitters()` is shared by enemies and bosses, so any pattern can
|
||||
be dropped on either. A boss is four phases layering one idea at a time; a new
|
||||
boss is a new function in `src/content/content.gd` and zero simulation changes.
|
||||
|
||||
## Entry point
|
||||
|
||||
One executable, one `src/main.tscn`. `GameOpts.parse()` reads the command line
|
||||
after `--`:
|
||||
|
||||
- `--server` → `ServerRuntime`, no view, `max_fps` pinned to the tick rate.
|
||||
- `--join` / `--bot` → connect immediately, skipping the menu.
|
||||
- otherwise → the connect menu, which can also start a listen server.
|
||||
|
||||
## File map
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `src/core/sim_config.gd` | Every constant server and client must agree on. |
|
||||
| `src/core/movement.gd` | Pure movement + overlap helpers. Shared by prediction. |
|
||||
| `src/core/game_log.gd`, `game_opts.gd` | Static; usable from tools and tests. |
|
||||
| `src/sim/sim_world.gd` | The simulation. Authority flag decides what runs. |
|
||||
| `src/sim/bullet_pool.gd` | Struct-of-arrays bullet storage and integration. |
|
||||
| `src/sim/input_frame.gd` | The only thing a client may assert about itself. |
|
||||
| `src/net/net_codec.gd` | Snapshot / event / input binary codecs. |
|
||||
| `src/net/server_runtime.gd` | Instances, ticking, transfers, broadcast. |
|
||||
| `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. |
|
||||
| `src/instances/instance.gd` | Lobby hub and dungeon progression. |
|
||||
| `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. |
|
||||
| `src/view/bullet_renderer.gd` | The whole bullet field in one MultiMesh draw call. |
|
||||
Reference in New Issue
Block a user