Merge branch 'docs/arquitectura': document d'arquitectura per a nous companys
This commit is contained in:
@@ -0,0 +1,838 @@
|
||||
# Arquitectura de Orni Attack
|
||||
|
||||
> Documento de orientación para alguien que llega nuevo al proyecto. Cada
|
||||
> afirmación está anclada a código real (fichero/clase/función con su ruta).
|
||||
> Cuando algo no se ha podido verificar o no existe, se indica explícitamente.
|
||||
> El objetivo no es vender una arquitectura ideal, sino describir lo que **este**
|
||||
> proyecto hace, incluso donde es poco convencional.
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Visión general](#1-visión-general)
|
||||
2. [Punto de entrada y el Director](#2-punto-de-entrada-y-el-director)
|
||||
3. [Bucle principal](#3-bucle-principal)
|
||||
4. [Sistema de escenas](#4-sistema-de-escenas)
|
||||
5. [Renderizado: de la lógica al píxel](#5-renderizado-de-la-lógica-al-píxel)
|
||||
6. [Entrada](#6-entrada)
|
||||
7. [Audio](#7-audio)
|
||||
8. [Recursos](#8-recursos)
|
||||
9. [Comunicación entre módulos](#9-comunicación-entre-módulos)
|
||||
10. [Lógica del juego](#10-lógica-del-juego)
|
||||
11. [IA del modo demo (attract)](#11-ia-del-modo-demo-attract)
|
||||
12. [Efectos visuales](#12-efectos-visuales)
|
||||
13. [Configuración, constantes y convenciones](#13-configuración-constantes-y-convenciones)
|
||||
14. [Guía de navegación](#14-guía-de-navegación)
|
||||
|
||||
---
|
||||
|
||||
## 1. Visión general
|
||||
|
||||
Orni Attack es un arcade vectorial (estética CRT de líneas con bloom) construido
|
||||
sobre **SDL3**, usando la **GPU API de SDL3** (`SDL_gpu`) para el render — **no**
|
||||
`SDL_Renderer`. El código está partido en dos grandes mundos:
|
||||
|
||||
- **`source/core/`** — el "motor": ventana, GPU, audio, input, recursos, i18n,
|
||||
overlays de sistema. No conoce nada del juego concreto. Por ejemplo,
|
||||
[audio.hpp](source/core/audio/audio.hpp) recibe un struct de configuración y no
|
||||
lee YAML, e [input.hpp](source/core/input/input.hpp) no incluye nada de `game/`.
|
||||
- **`source/game/`** — la lógica concreta de Orni Attack: escenas, entidades
|
||||
(naves, enemigos, balas), sistemas (colisiones, IA), stages/oleadas y efectos.
|
||||
|
||||
El punto de indirección entre ambos mundos para el render es
|
||||
[render_context.hpp](source/core/rendering/render_context.hpp): el juego habla con
|
||||
un `Rendering::Renderer*` opaco que es un alias de `GPU::GpuFrameRenderer`. Esto
|
||||
permite cambiar de backend sin tocar las firmas del juego.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph entry["Punto de entrada"]
|
||||
MAIN["main.cpp<br/>SDL_MAIN_USE_CALLBACKS"]
|
||||
end
|
||||
MAIN -->|posee| DIR["Director<br/>(es el programa)"]
|
||||
|
||||
subgraph core["source/core (motor)"]
|
||||
SDLM["SDLManager<br/>ventana + GPU"]
|
||||
GE["GlobalEvents<br/>F1-F7/F12/ESC/hotplug"]
|
||||
INPUT["Input (singleton)"]
|
||||
AUDIO["Audio (singleton)"]
|
||||
RES["Resource::Loader / Pack"]
|
||||
LOC["Locale (i18n)"]
|
||||
OVL["Notifier · ServiceMenu<br/>DebugOverlay · DefineInputs"]
|
||||
end
|
||||
|
||||
subgraph game["source/game (juego)"]
|
||||
SCN["Scenes<br/>Logo · Title · Game"]
|
||||
ENT["Entities<br/>Ship · Enemy · Bullet"]
|
||||
SYS["Systems<br/>Collision · EnemyAi · DemoPilot"]
|
||||
STG["StageManager / WaveRunner"]
|
||||
FX["Effects<br/>debris · firework · score · trail"]
|
||||
end
|
||||
|
||||
DIR --> SDLM
|
||||
DIR --> GE
|
||||
DIR --> OVL
|
||||
DIR --> SCN
|
||||
SCN --> ENT
|
||||
SCN --> SYS
|
||||
SCN --> STG
|
||||
SCN --> FX
|
||||
GE --> INPUT
|
||||
SCN -.usa.-> AUDIO
|
||||
SCN -.usa.-> RES
|
||||
OVL -.usa.-> LOC
|
||||
```
|
||||
|
||||
**Patrón dominante de comunicación:** singletons globales (`Input::get()`,
|
||||
`Audio::get()`, `Locale::get()`, `Notifier`, `ServiceMenu`) más paso por
|
||||
referencia de un `Rendering::Renderer*` y un `SceneContext&`. **No hay** un bus de
|
||||
eventos genérico ni un ECS — las entidades viven en `std::array` de tamaño fijo
|
||||
dentro de `GameScene` y los sistemas operan sobre un struct `Context` de punteros
|
||||
(ver [§10](#10-lógica-del-juego)).
|
||||
|
||||
**Rasgo de diseño destacable:** gran parte de la lógica es *data-driven*. Los
|
||||
enemigos, balas y el jugador se describen en **YAML declarativo**
|
||||
(`data/entities/*/*.yaml`: physics/ai/animation/events), los stages en
|
||||
`data/stages/stages.yaml` (oleadas), y las figuras vectoriales en ficheros `.shp`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Punto de entrada y el Director
|
||||
|
||||
El `main` real está en [main.cpp](source/main.cpp) y usa el modo de callbacks de
|
||||
SDL3 (`#define SDL_MAIN_USE_CALLBACKS 1`). En lugar de un bucle `while` clásico,
|
||||
SDL llama a cuatro funciones, y todas son pura fontanería que delega en un
|
||||
`Director`:
|
||||
|
||||
```cpp
|
||||
// main.cpp
|
||||
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
|
||||
System::Relaunch::setArgv(argc, argv);
|
||||
auto director = std::make_unique<Director>(argc, argv);
|
||||
*appstate = director.release(); // SDL guarda el puntero
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
auto SDL_AppEvent(void* s, SDL_Event* e) { return ((Director*)s)->handleEvent(*e); }
|
||||
auto SDL_AppIterate(void* s) { return ((Director*)s)->iterate(); }
|
||||
void SDL_AppQuit(void* s, ...) { /* reabsorbe y destruye el Director */ }
|
||||
```
|
||||
|
||||
La filosofía está escrita en el propio comentario de cabecera de
|
||||
[director.hpp](source/core/system/director.hpp):
|
||||
|
||||
> *El Director és EL programa: posseeix la configuració, els subsistemes i
|
||||
> l'estat.*
|
||||
|
||||
Como con `SDL_MAIN_USE_CALLBACKS` no hay un `scope` que envuelva todo el bucle,
|
||||
el estado que antes vivía en un `run()` ahora es **miembro** del Director:
|
||||
`sdl_` (SDLManager), `context_` (SceneContext), `debug_overlay_` y
|
||||
`current_scene_` (todos `std::unique_ptr`, ver
|
||||
[director.hpp:45-48](source/core/system/director.hpp#L45-L48)).
|
||||
|
||||
### Orden de arranque (constructor)
|
||||
|
||||
El constructor [Director::Director](source/core/system/director.cpp#L46) ejecuta el
|
||||
bootstrap completo, en este orden:
|
||||
|
||||
1. `ConfigYaml::init()` — valores por defecto de configuración.
|
||||
2. Parseo de argumentos (`--console`, `--reset-config`) en
|
||||
[checkProgramArguments](source/core/system/director.cpp#L241).
|
||||
3. `Utils::initializePathSystem()` + sistema de recursos
|
||||
([§8](#8-recursos)): en *release* el `resources.pack` es obligatorio; en *dev*
|
||||
hay fallback a `data/`.
|
||||
4. Crea la carpeta de sistema (`~/.config/jailgames/<NAME>` en Linux) y carga/crea
|
||||
`config.yaml` ([createSystemFolder](source/core/system/director.cpp#L260)).
|
||||
5. Carga el `locale` ([§7](#7-audio) usa lo mismo: i18n).
|
||||
6. `Input::init()` con el `gamecontrollerdb.txt` (autoasigna mandos a P1/P2 la
|
||||
primera vez).
|
||||
7. Crea `SDLManager` (ventana + GPU), oculta el cursor, inicializa `Audio`.
|
||||
8. **Precarga bloqueante** de todos los recursos (música, sonidos, shapes) para
|
||||
evitar tirones de I/O en las transiciones
|
||||
([director.cpp:187-195](source/core/system/director.cpp#L187-L195)).
|
||||
9. Crea el `SceneContext` y fija la escena inicial: `TITLE` en `_DEBUG`, `LOGO`
|
||||
en el resto ([director.cpp:200-205](source/core/system/director.cpp#L200-L205)).
|
||||
10. Inicializa los overlays de sistema: `DebugOverlay`, `Notifier`, `ServiceMenu`,
|
||||
`DefineInputs`.
|
||||
|
||||
El destructor [Director::~Director](source/core/system/director.cpp#L218) guarda
|
||||
la config y destruye los subsistemas **en orden inverso** a la construcción (el
|
||||
`Notifier` referencia el renderer, así que debe morir antes que `sdl_`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Bucle principal
|
||||
|
||||
Cada frame, SDL llama a `SDL_AppIterate`, que delega en
|
||||
[Director::iterate()](source/core/system/director.cpp#L383). Su estructura es:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SDL
|
||||
participant Dir as Director::iterate()
|
||||
participant Scene
|
||||
participant SDLM as SDLManager
|
||||
participant GPU as GpuFrameRenderer
|
||||
|
||||
SDL->>Dir: iterate()
|
||||
Note over Dir: si wants_quit_ → SDL_APP_SUCCESS
|
||||
Dir->>Dir: si !scene o scene.isFinished() → advanceScene()
|
||||
Dir->>Dir: delta_time = (now - last) capeado a 50 ms
|
||||
Dir->>Dir: Input::update()
|
||||
Dir->>Scene: update(dt)
|
||||
Dir->>Dir: overlays.update(dt) + Audio::update()
|
||||
Dir->>SDLM: clear() (= GPU.beginFrame)
|
||||
alt swapchain no disponible
|
||||
SDLM-->>Dir: false → saltar draw+present
|
||||
end
|
||||
Dir->>SDLM: updateRenderingContext()
|
||||
Dir->>Scene: draw()
|
||||
Dir->>Dir: overlays.draw() (capas)
|
||||
Dir->>SDLM: present() (= GPU.endFrame → bloom + postfx)
|
||||
```
|
||||
|
||||
Puntos concretos a tener en cuenta:
|
||||
|
||||
- **Pivot de escena**: si no hay escena o la actual reporta `isFinished()`, se
|
||||
llama a [advanceScene()](source/core/system/director.cpp#L338), que destruye la
|
||||
actual y construye la siguiente con
|
||||
[buildScene()](source/core/system/director.cpp#L323) según
|
||||
`context_->nextScene()`.
|
||||
- **Delta time**: se mide con `SDL_GetTicks()` y se **capea a 50 ms** para evitar
|
||||
saltos grandes tras un stall ([director.cpp:397-400](source/core/system/director.cpp#L397-L400)).
|
||||
- **Orden de update**: `Input::update()` → `current_scene_->update(dt)` →
|
||||
`debug_overlay_` → `Notifier` → `ServiceMenu` → `DefineInputs` → `Audio::update()`.
|
||||
- **Render por capas** (de abajo arriba, entre `clear` y `present`):
|
||||
escena → `debug_overlay_` → `Notifier` (toasts) → `ServiceMenu` → `DefineInputs`
|
||||
(modal de rebinding). Si el overlay de rebinding está activo, el menú de servicio
|
||||
no se pinta ([director.cpp:432-439](source/core/system/director.cpp#L432-L439)).
|
||||
- **Salto de frame**: si `sdl_->clear()` devuelve `false` (swapchain no disponible,
|
||||
p. ej. ventana minimizada), se omiten `draw` y `present` ese frame.
|
||||
|
||||
El bucle de eventos vive aparte, en
|
||||
[Director::handleEvent()](source/core/system/director.cpp#L354), que enruta cada
|
||||
`SDL_Event` por la cadena: **ventana → GlobalEvents → F11 (debug overlay) →
|
||||
escena** (ver [§9](#9-comunicación-entre-módulos)).
|
||||
|
||||
---
|
||||
|
||||
## 4. Sistema de escenas
|
||||
|
||||
La interfaz base es [scene.hpp](source/core/system/scene.hpp). Como dice su
|
||||
cabecera, *el frame loop vive en el Director, no en cada escena*. Cada escena
|
||||
implementa cuatro métodos puros:
|
||||
|
||||
```cpp
|
||||
virtual void handleEvent(const SDL_Event&) = 0; // eventos no-globales
|
||||
virtual void update(float delta_time) = 0; // lógica
|
||||
virtual void draw() = 0; // pintado (entre clear y present)
|
||||
virtual auto isFinished() const -> bool = 0; // ¿transición pendiente?
|
||||
```
|
||||
|
||||
Una escena pide transición vía `context_.setNextScene(...)`; en el siguiente frame
|
||||
`isFinished()` devuelve `true` y el Director la destruye para construir la
|
||||
siguiente.
|
||||
|
||||
### SceneContext
|
||||
|
||||
[scene_context.hpp](source/core/system/scene_context.hpp) es el "buzón" de
|
||||
transición que el Director posee y va pasando a cada escena por referencia. Tiene:
|
||||
|
||||
- `SceneType` (enum): `LOGO`, `TITLE`, `GAME`, `EXIT`.
|
||||
- `Option` (p. ej. `JUMP_TO_TITLE_MAIN`) consumible con `consumeOption()`.
|
||||
- `MatchConfig` (jugadores activos, modo NORMAL/DEMO) para pasar a `GAME`.
|
||||
- El **índice del escenario de demo** (`demoScenarioIndex()` / `advanceDemoScenario()`),
|
||||
que persiste entre escenas para que cada entrada al attract mode muestre el
|
||||
siguiente escenario curado (ver [§11](#11-ia-del-modo-demo-attract)).
|
||||
|
||||
Existe además una variable global `SceneManager::actual` que el Director mantiene
|
||||
sincronizada con la escena en curso (compatibilidad hacia atrás).
|
||||
|
||||
### Las tres escenas (FSM jerárquica)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> LOGO
|
||||
LOGO --> TITLE
|
||||
TITLE --> GAME : START (1P/2P)
|
||||
TITLE --> GAME : idle timeout (DEMO)
|
||||
GAME --> TITLE : game over / fin demo (input)
|
||||
GAME --> LOGO : fin demo (timeout/muerte)
|
||||
TITLE --> [*] : EXIT
|
||||
```
|
||||
|
||||
Cada escena tiene además su **propia** máquina de estados interna:
|
||||
|
||||
- **[LogoScene](source/game/scenes/logo_scene.hpp)** — `AnimationState`:
|
||||
`PRE_ANIMATION → ANIMATION → POST_ANIMATION → EXPLOSION → POST_EXPLOSION`. Anima
|
||||
el logo JAILGAMES y lo hace explotar en fragmentos (debris).
|
||||
- **[TitleScene](source/game/scenes/title_scene.hpp)** — `TitleState`:
|
||||
`STARFIELD_FADE_IN → STARFIELD → MAIN → PLAYER_JOIN_PHASE → BLACK_SCREEN →
|
||||
DEMO_DIVE → DEMO_CURTAIN`. Naves 3D flotantes (vía
|
||||
[ShipAnimator](source/game/title/ship_animator.hpp)), selección 1P/2P, y un
|
||||
`idle_timer_` en el estado `MAIN` que dispara el attract mode por inactividad.
|
||||
- **[GameScene](source/game/scenes/game_scene.hpp)** — es el núcleo del juego y se
|
||||
detalla en [§10](#10-lógica-del-juego).
|
||||
|
||||
---
|
||||
|
||||
## 5. Renderizado: de la lógica al píxel
|
||||
|
||||
Este es el subsistema más denso. La idea central: **toda la geometría son líneas**
|
||||
(la estética es vectorial). El juego acumula líneas en CPU durante `draw()`, y al
|
||||
final del frame se envían a la GPU en un único batch, se rasterizan a una textura
|
||||
*offscreen*, y un par de pases de post-procesado (bloom + flicker/fondo) componen
|
||||
la imagen final sobre la swapchain.
|
||||
|
||||
### 5.1 Capas del subsistema
|
||||
|
||||
| Fichero | Rol |
|
||||
|---|---|
|
||||
| [sdl_manager.hpp/.cpp](source/core/rendering/sdl_manager.hpp) | Crea la ventana SDL, posee el `GpuFrameRenderer`, gestiona zoom/fullscreen/letterbox. Expone `clear()` / `present()` / `getRenderer()`. |
|
||||
| [gpu/gpu_frame_renderer.hpp/.cpp](source/core/rendering/gpu/gpu_frame_renderer.hpp) | Orquestador del frame GPU: `beginFrame` → `pushLine`/`pushRect` → `endFrame` (`flushBatch` + `bloomPass` + `compositePass`). |
|
||||
| [gpu/gpu_device](source/core/rendering/gpu/gpu_device.hpp) | Wrapper del `SDL_GPUDevice` (claim de ventana, formato de swapchain). |
|
||||
| [gpu/gpu_line_pipeline](source/core/rendering/gpu/gpu_line_pipeline.hpp) | Pipeline de líneas: dibuja cada línea como un quad (2 triángulos) con antialias geométrico. |
|
||||
| [gpu/gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp) | Blur gaussiano separable (pase H + pase V) sobre dos texturas ping-pong. |
|
||||
| [gpu/gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp) | Composite final: mezcla escena + bloom + flicker + fondo pulsante. |
|
||||
| [line_renderer.hpp/.cpp](source/core/rendering/line_renderer.hpp) | API que usa el juego: `Rendering::linea(...)` y `lineaGlow(...)`. |
|
||||
| [shape_renderer.hpp/.cpp](source/core/rendering/shape_renderer.hpp) | `renderShape(...)`: dibuja una `Shape` aplicando transformación y, opcionalmente, glow multipase. |
|
||||
|
||||
### 5.2 Una `Shape` y cómo se carga
|
||||
|
||||
Una "shape" es una figura vectorial: un conjunto de **polilíneas** y **líneas**
|
||||
([shape.hpp](source/core/graphics/shape.hpp)). Los ficheros viven en `data/shapes/`
|
||||
con extensión `.shp` y un formato de texto tipo clave:valor. Ejemplo real
|
||||
([data/shapes/ship/arrow.shp](data/shapes/ship/arrow.shp)):
|
||||
|
||||
```
|
||||
name: arrow
|
||||
scale: 1.0
|
||||
center: 0, 0
|
||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||
```
|
||||
|
||||
> Nota: el formato real usa directivas `name:`, `scale:`, `center:`,
|
||||
> `polyline:` y `line:` (Y negativo = arriba). No es la sintaxis
|
||||
> `POLYLINE: (x,y)` que podría suponerse de otros motores.
|
||||
|
||||
La carga la centraliza [shape_loader.hpp](source/core/graphics/shape_loader.hpp)
|
||||
(`Graphics::ShapeLoader::load(filename)`), con caché de `std::shared_ptr<Shape>`.
|
||||
Todas las shapes se precargan en el boot del Director.
|
||||
|
||||
### 5.3 El flujo de un frame de render
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Scene::draw()<br/>(acumula en CPU)"] --> B["Rendering::linea / renderShape"]
|
||||
B --> C["GpuFrameRenderer::pushLine()<br/>extruye quad → vertices_ / indices_"]
|
||||
C -.repetido N veces.-> C
|
||||
A --> D["SDLManager::present()<br/>= GpuFrameRenderer::endFrame()"]
|
||||
D --> E["flushBatch()<br/>sube VBO/IBO, dibuja sobre OFFSCREEN"]
|
||||
E --> F["bloomPass()<br/>H: high-pass+blur → bloom_a<br/>V: blur → bloom_b"]
|
||||
F --> G["compositePass()<br/>offscreen + bloom_b + flicker + fondo<br/>→ swapchain (letterbox)"]
|
||||
G --> H["SubmitGPUCommandBuffer + present"]
|
||||
```
|
||||
|
||||
Paso a paso, con anclas reales:
|
||||
|
||||
1. **Emisión (juego).** Durante `current_scene_->draw()`, el juego llama a
|
||||
[Rendering::linea()](source/core/rendering/line_renderer.hpp#L33) (y
|
||||
`renderShape`, `VectorText`, `Playfield`, etc.). Las coordenadas son **lógicas
|
||||
(1280×720)**. El color por defecto si `alpha==0` es el verde fósforo CRT
|
||||
`DEFAULT_LINE_COLOR = {100,255,100,255}`.
|
||||
2. **Acumulación (CPU).** `linea()` pre-multiplica el brillo y llama a
|
||||
[GpuFrameRenderer::pushLine()](source/core/rendering/gpu/gpu_frame_renderer.hpp#L88),
|
||||
que **extruye** la línea en un quad (4 vértices, 6 índices) y lo acumula en
|
||||
`vertices_` / `indices_`. Si el antialias está activo, añade ~0.5 px de padding y
|
||||
marca `edge_dist` para el fade del fragment shader.
|
||||
3. **Flush (GPU).** En `endFrame()`, `flushBatch()` sube el batch a un VBO/IBO,
|
||||
abre un render pass sobre el `offscreen_texture_` (R8G8B8A8, tamaño físico
|
||||
configurable, independiente del lógico) y dibuja con el `line_pipeline_`. El
|
||||
vertex shader transforma píxeles lógicos → NDC; el fragment shader aplica
|
||||
`smoothstep` sobre `edge_dist` para el suavizado.
|
||||
4. **Bloom.** `bloomPass()` hace un blur separable: pase H (high-pass por
|
||||
luminancia + blur horizontal → `bloom_texture_a_`) y pase V (blur vertical →
|
||||
`bloom_texture_b_`). Parámetros en `PostFxParams`
|
||||
([gpu_frame_renderer.hpp:33-51](source/core/rendering/gpu/gpu_frame_renderer.hpp#L33-L51)).
|
||||
5. **Composite.** `compositePass()` dibuja un triángulo *fullscreen* sobre la
|
||||
swapchain, muestreando offscreen + bloom, aplicando flicker temporal y un fondo
|
||||
verde pulsante. Aquí se aplica el **letterbox** vía el viewport físico
|
||||
(`setViewport`).
|
||||
|
||||
El interruptor maestro de post-proceso es **F6** (`setPostFxEnabled`): cuando está
|
||||
OFF, la escena offscreen sale tal cual (passthrough), útil para A/B testing.
|
||||
|
||||
### 5.4 Texto, 3D y elementos de escena
|
||||
|
||||
- **[VectorText](source/core/graphics/vector_text.hpp)** — renderiza texto donde
|
||||
cada carácter es una `Shape` precargada.
|
||||
- **[Camera3D](source/core/graphics/camera3d.hpp)** + **[Wireframe3D](source/core/graphics/wireframe3d.hpp)**
|
||||
— proyección perspectiva en CPU de mallas 3D (vértices + aristas) a líneas 2D.
|
||||
Lo usan el starfield 3D y las naves del título.
|
||||
- **[Starfield](source/core/graphics/starfield.hpp)** (campo de estrellas 3D que
|
||||
vienen hacia la cámara) y **[StarfieldParallax](source/core/graphics/starfield_parallax.hpp)**
|
||||
(capas 2D de fondo con parallax).
|
||||
- **[Playfield](source/core/graphics/playfield.hpp)** — rejilla de fondo con
|
||||
animación de construcción y *ripples* (ondas) que reaccionan a la nave y a las
|
||||
explosiones.
|
||||
- **[Border](source/core/graphics/border.hpp)** — marco de 4 lados que se desplaza
|
||||
al recibir impactos.
|
||||
- **[Curtain](source/core/graphics/curtain.hpp)** — cortinilla negra para
|
||||
transiciones; se pinta siempre la última.
|
||||
|
||||
### 5.5 Shaders: fuentes, compilación y selección
|
||||
|
||||
Las fuentes GLSL viven en [shaders/](shaders/): `line.vert.glsl`, `line.frag.glsl`,
|
||||
`postfx.vert.glsl`, `postfx.frag.glsl`, `bloom.frag.glsl`. **No se cargan de disco en
|
||||
runtime**: se embeben como arrays/strings en el binario.
|
||||
|
||||
**Pipeline de compilación (SPIR-V, Linux/Windows).** Lo orquesta
|
||||
[CMakeLists.txt:139-187](CMakeLists.txt#L139). La lógica clave:
|
||||
|
||||
- Para cada `.glsl` hay un header destino en
|
||||
[gpu/spv/](source/core/rendering/gpu/spv/) (p. ej. `line_vert_spv.h`).
|
||||
- CMake busca `glslc` (`find_program(GLSLC_EXE ...)`). Hay **tres caminos**:
|
||||
1. `glslc` presente → un `add_custom_command` regenera los headers SPV cuando
|
||||
cambian los `.glsl`, vía el target `shaders` del que depende el ejecutable.
|
||||
2. `glslc` ausente pero **los headers ya están commiteados** → se usan tal cual
|
||||
(los `.spv.h` están versionados en el repo).
|
||||
3. `glslc` ausente **y** faltan headers → `FATAL_ERROR` pidiendo instalar
|
||||
`shaderc`/`vulkan-sdk`.
|
||||
- La conversión binario→header la hace el script
|
||||
[tools/shaders/compile_spirv.cmake](tools/shaders/compile_spirv.cmake): invoca
|
||||
`glslc -O -fshader-stage=<vert|frag>` para producir el `.spv`, lee el binario como
|
||||
hex (`file(READ ... HEX)`) y escribe un header con
|
||||
`static const uint8_t LINE_VERT_SPV[] = { 0x.., ... };` y su `_SIZE`. Es
|
||||
multiplataforma puro CMake (no necesita `bash` ni `xxd`).
|
||||
|
||||
**MSL (macOS).** Los headers Metal en [gpu/msl/](source/core/rendering/gpu/msl/)
|
||||
(`line_vert.msl.h`, etc.) están **escritos a mano** (no los genera CMake), como
|
||||
strings literales C++.
|
||||
|
||||
**Selección SPV vs MSL: es _compile-time_, no runtime.** La hace
|
||||
[shader_factory.hpp](source/core/rendering/gpu/shader_factory.hpp) con `#ifdef __APPLE__`:
|
||||
en Apple expone `createShaderMSL(...)` (`SDL_GPU_SHADERFORMAT_MSL`), y en el resto
|
||||
`createShaderSPIRV(...)` (`SDL_GPU_SHADERFORMAT_SPIRV`). Cada pipeline llama al helper
|
||||
disponible con el header embebido correspondiente. (Es decir: no es `GpuDevice` quien
|
||||
elige el backend de shader, sino el preprocesador al compilar.)
|
||||
|
||||
---
|
||||
|
||||
## 6. Entrada
|
||||
|
||||
El subsistema de input ([core/input/](source/core/input/)) es un **singleton**
|
||||
(`Input::init()` / `Input::get()` / `Input::destroy()`) que unifica teclado,
|
||||
gamepads y ratón.
|
||||
|
||||
- **Acciones**: enum `InputAction` (`LEFT`, `RIGHT`, `THRUST`, `SHOOT`, `START`,
|
||||
`MENU`, ...) en [input_types.hpp](source/core/input/input_types.hpp).
|
||||
- **Bindings por jugador**: hay bindings separados de teclado y de gamepad para P1
|
||||
y P2, que se cargan de la config con `applyPlayer1Bindings()` /
|
||||
`applyPlayer2Bindings()` (llamados desde el constructor del Director).
|
||||
- **Captura por frame**: `Input::update()` lee `SDL_GetKeyboardState()` y los ejes
|
||||
y botones del gamepad, y hace *edge-detection* para distinguir `just_pressed` de
|
||||
`is_held`. La consulta es `checkAction(...)` / `checkActionPlayer1/2(...)`.
|
||||
- **Hotplug**: `Input::handleEvent()` procesa `SDL_EVENT_GAMEPAD_ADDED/REMOVED`
|
||||
(`addGamepad` / `removeGamepad`) y notifica con un toast vía `Notifier`.
|
||||
- **Ratón**: [mouse.hpp](source/core/input/mouse.hpp) auto-oculta el cursor.
|
||||
- **Rebinding en runtime**: [define_inputs.hpp](source/core/input/define_inputs.hpp)
|
||||
es un modal singleton que captura una secuencia de acciones, persiste en config y
|
||||
reaplica bindings sin reiniciar.
|
||||
|
||||
El enrutado de input ocurre en dos sitios: los eventos **globales** pasan por
|
||||
`GlobalEvents::handle()` (que primero deja a `Input` procesar el hotplug), y la
|
||||
lógica de juego consulta directamente `Input::get()->checkAction...` durante
|
||||
`update()` (p. ej. [Ship::processInput](source/game/entities/ship.hpp)).
|
||||
|
||||
---
|
||||
|
||||
## 7. Audio
|
||||
|
||||
[core/audio/](source/core/audio/) es otro singleton (`Audio::init/get/destroy`)
|
||||
con un motor de bajo nivel propio:
|
||||
|
||||
- **[Audio](source/core/audio/audio.hpp)** — capa lógica: `playMusic()`,
|
||||
`playSound()`, volúmenes por grupo (`GAME`, `INTERFACE`), `playSoundWithEcho/Reverb`.
|
||||
- **[jail_audio.hpp](source/core/audio/jail_audio.hpp)** (`Ja::Engine`) — motor
|
||||
sobre SDL3 audio: streaming de **OGG** (vía `stb_vorbis`) para música, **WAV**
|
||||
descomprimido para efectos, mezcla en N canales.
|
||||
- **[audio_adapter.hpp](source/core/audio/audio_adapter.hpp)** —
|
||||
`AudioResource::getMusic/getSound`: caché *lazy* que carga bytes vía
|
||||
`Resource::Helper` y los decodifica una sola vez.
|
||||
- **[audio_effects.hpp](source/core/audio/audio_effects.hpp)** — DSP de echo y
|
||||
reverb; presets en `data/config/sounds.yaml`
|
||||
([sound_effects_config.hpp](source/core/audio/sound_effects_config.hpp)).
|
||||
|
||||
El Director precarga toda la música y todos los sonidos en el boot, y llama a
|
||||
`Audio::update()` una vez por frame.
|
||||
|
||||
---
|
||||
|
||||
## 8. Recursos
|
||||
|
||||
[core/resources/](source/core/resources/) abstrae de dónde salen los bytes:
|
||||
|
||||
- **[resource_pack](source/core/resources/resource_pack.hpp)** (`Resource::Pack`)
|
||||
— lee un fichero empaquetado con cabecera *magic* `"ORNI"` y entradas con CRC32
|
||||
para validación de integridad.
|
||||
- **[resource_loader](source/core/resources/resource_loader.hpp)**
|
||||
(`Resource::Loader`, singleton Meyers) — `loadResource()`, `resourceExists()`,
|
||||
`listResources(prefix)`, `validatePack()`.
|
||||
- **[resource_helper](source/core/resources/resource_helper.hpp)** — wrappers de
|
||||
conveniencia (`initializeResourceSystem`, `listResources`, `loadFile`).
|
||||
|
||||
**Estrategia dual** (decidida en el constructor del Director,
|
||||
[director.cpp:64-93](source/core/system/director.cpp#L64-L93)):
|
||||
|
||||
- **Release** (`RELEASE_BUILD`): `resources.pack` es **obligatorio** y se valida su
|
||||
integridad; si falla, el juego aborta. No hay fallback (ver memoria de proyecto
|
||||
*"No fallback a SDL_Renderer"* — aquí es la política equivalente para recursos).
|
||||
- **Dev**: intenta el pack; si no está, hace **fallback al directorio `data/`** del
|
||||
filesystem, escaneándolo según prefijo (`music/`, `sounds/`, `shapes/`).
|
||||
|
||||
El formato de datos de juego:
|
||||
|
||||
- **Entidades** (`data/entities/<nombre>/<nombre>.yaml`) — YAML declarativo con
|
||||
`shape`, `physics`, `ai`, `animation`, `wounded`, `spawn`, `colors`, `score`,
|
||||
`events`. Ejemplo: [data/entities/square/square.yaml](data/entities/square/square.yaml).
|
||||
- **Stages** (`data/stages/stages.yaml`) — oleadas (`waves`) con `spawn`,
|
||||
`spawn_interval`, `next` y multiplicadores de dificultad por stage.
|
||||
- **Shapes** (`data/shapes/**/*.shp`) — figuras vectoriales (ver [§5.2](#52-una-shape-y-cómo-se-carga)).
|
||||
|
||||
El parser YAML usado es [fkyaml](source/external/fkyaml_node.hpp) (cabecera única),
|
||||
envuelto por [config_yaml](source/game/config_yaml.hpp).
|
||||
|
||||
---
|
||||
|
||||
## 9. Comunicación entre módulos
|
||||
|
||||
No hay un sistema de mensajería desacoplado. La comunicación es:
|
||||
|
||||
1. **Eventos SDL → cadena del Director.** Por cada `SDL_Event`,
|
||||
[Director::handleEvent](source/core/system/director.cpp#L354) intenta, en orden:
|
||||
`SDLManager::handleWindowEvent` → `GlobalEvents::handle` → F11 (debug overlay) →
|
||||
`current_scene_->handleEvent`.
|
||||
|
||||
2. **GlobalEvents** ([global_events.cpp](source/core/system/global_events.cpp)) es
|
||||
el orquestador de la entrada global. Su `handle()` hace, en orden:
|
||||
`Input::get()->handleEvent` (hotplug) → `consumeIfDefineActive` (si el modal de
|
||||
rebinding está activo, **engulle todo**) → `SDL_EVENT_QUIT` → ratón → botón MENU
|
||||
del mando → reenvío al `ServiceMenu` si está abierto → teclas de función:
|
||||
|
||||
| Tecla | Acción |
|
||||
|---|---|
|
||||
| F1 / F2 | reducir / aumentar tamaño de ventana |
|
||||
| F3 | fullscreen |
|
||||
| F4 | VSync |
|
||||
| F5 | antialias geométrico |
|
||||
| F6 | post-procesado (bloom/flicker/fondo) |
|
||||
| F7 | idioma ca ↔ en (hot-swap de `Locale`) |
|
||||
| F11 | debug overlay (gestionado en el Director, no en GlobalEvents) |
|
||||
| F12 | menú de servicio |
|
||||
| ESC | doble pulsación para salir (la 1ª muestra un toast de confirmación) |
|
||||
|
||||
3. **Singletons compartidos.** `Input`, `Audio`, `Locale`, `Notifier`,
|
||||
`ServiceMenu`, `DefineInputs` se acceden globalmente vía `::get()`. Muchos
|
||||
comprueban `nullptr` para degradar con elegancia (p. ej. el hotplug notifica
|
||||
solo si `Notifier::get() != nullptr`).
|
||||
|
||||
4. **Paso por referencia.** Las escenas reciben `SDLManager&` y `SceneContext&`; el
|
||||
render se propaga como `Rendering::Renderer*`. Los sistemas de juego reciben un
|
||||
struct `Context` con punteros a los pools (ver [§10](#10-lógica-del-juego)).
|
||||
|
||||
**Overlays de sistema** (todos singletons, todos por encima de la escena):
|
||||
|
||||
- **[Notifier](source/core/system/notifier.hpp)** — toasts deslizantes centrados
|
||||
(`notifyInfo/Warn/Exit`), con máquina de animación HIDDEN/ENTERING/HOLDING/EXITING.
|
||||
- **[ServiceMenu](source/core/system/service_menu.hpp)** — menú de configuración
|
||||
(F12) con pila de páginas (vídeo, audio, controles, sistema...).
|
||||
- **[DebugOverlay](source/core/system/debug_overlay.hpp)** — HUD de FPS/VSync (F11).
|
||||
- **[Relaunch](source/core/system/relaunch.hpp)** — reinicio en caliente vía
|
||||
`execv` (lo solicita el ServiceMenu, lo ejecuta `SDL_AppQuit`).
|
||||
|
||||
**Lo que NO existe** (verificado): no hay event bus genérico, ni cola de mensajes
|
||||
desacoplada, ni un FSM genérico reutilizable fuera de las máquinas de estado
|
||||
concretas de cada escena/sistema, ni un ECS.
|
||||
|
||||
---
|
||||
|
||||
## 10. Lógica del juego
|
||||
|
||||
Toda la partida vive en [GameScene](source/game/scenes/game_scene.hpp). Es la clase
|
||||
más grande del juego y actúa como orquestador. Posee:
|
||||
|
||||
- El mundo físico [Physics::PhysicsWorld](source/core/physics/physics_world.hpp)
|
||||
(integración cinemática + colisiones físicas).
|
||||
- Pools de tamaño **fijo**: `std::array<Ship, 2>`,
|
||||
`std::array<Enemy, MAX_ORNIS>` (15), `std::array<Bullet, MAX_BULLETS_TOTAL>` (6:
|
||||
P1=[0,1,2], P2=[3,4,5]).
|
||||
- Estado de partida: vidas, score y *death timers* por jugador, máquina de
|
||||
game over (`GameOverState`: `NONE/CONTINUE/GAME_OVER`), continues usados.
|
||||
- El stage system, los efectos visuales, y los `DemoPilot` (uno por nave).
|
||||
|
||||
### 10.1 Orquestación por frame
|
||||
|
||||
[GameScene::update()](source/game/scenes/game_scene.cpp) es un orquestador delgado;
|
||||
cada paso es una función privada (descompuesto para reducir complejidad cognitiva):
|
||||
|
||||
```cpp
|
||||
void GameScene::update(float dt) {
|
||||
if (ServiceMenu abierto) return; // pausa global (draw sí sigue)
|
||||
stepPhysics(dt);
|
||||
if (mode == DEMO) { if (stepDemo(dt)) return; }
|
||||
else if (game_over_state_ == NONE) { stepShootingInput(); stepMidGameJoin(); }
|
||||
if (stepContinueScreen(dt)) return;
|
||||
if (stepGameOver(dt)) return;
|
||||
stepDeathSequence(dt);
|
||||
stepStageStateMachine(dt);
|
||||
}
|
||||
```
|
||||
|
||||
El corazón del gameplay es
|
||||
[stepStageStateMachine](source/game/scenes/game_scene.hpp#L166), que despacha según
|
||||
el estado del stage; en `PLAYING`,
|
||||
[runStagePlaying](source/game/scenes/game_scene.hpp#L169) ejecuta: WaveRunner
|
||||
(spawns) → IA de cada enemigo → control de naves
|
||||
([updateShipsControl](source/game/scenes/game_scene.cpp), que en demo usa
|
||||
`applyMovement` con el control del pilot y fuera de demo usa `processInput`) →
|
||||
detección de colisiones ([runCollisionDetections](source/game/scenes/game_scene.hpp#L176)).
|
||||
|
||||
`draw()` despacha de forma análoga según `GameOverState` y el estado del stage, y
|
||||
siempre pinta la cortinilla al final.
|
||||
|
||||
### 10.2 Entidades
|
||||
|
||||
Las tres heredan de `Entities::Entity` ([entity.hpp](source/core/entities/entity.hpp)):
|
||||
|
||||
- **[Ship](source/game/entities/ship.hpp)** — nave del jugador. `processInput()`
|
||||
(humano) y `applyMovement()` (usado por la IA demo). Estados: activa,
|
||||
invulnerable (parpadeo tras spawn), herida (`hurt`). Al morir genera debris con
|
||||
la inercia heredada.
|
||||
- **[Enemy](source/game/entities/enemy.hpp)** — 5 tipos (`EnemyType`: `PENTAGON`,
|
||||
`SQUARE`, `PINWHEEL`, `STAR`, `ORB`). Toda su config (físicas, IA, animación,
|
||||
eventos) viene del **YAML** vía [EnemyRegistry](source/game/entities/enemy_registry.hpp).
|
||||
Tiene salud (la mayoría HP=1; `ORB` HP=10) y estado *wounded* (parpadeo).
|
||||
- **[Bullet](source/game/entities/bullet.hpp)** — con `owner_id` (0=P1, 1=P2,
|
||||
≥16=enemigo) y `prev_position` para colisión *swept* (la bala que cruza un enemigo
|
||||
entre dos frames). Config en [BulletRegistry](source/game/entities/bullet_registry.hpp).
|
||||
|
||||
### 10.3 IA de enemigos: declarativa
|
||||
|
||||
Los enemigos **no** tienen comportamiento hardcoded. El YAML describe:
|
||||
|
||||
- Una **primitiva de movimiento** (`MovementType` en
|
||||
[enemy_ai.hpp](source/game/entities/enemy_ai.hpp)): `ZIGZAG`, `TRACKING`,
|
||||
`RECTILINEAR_PROXIMITY`, `WANDER`, `CHASE`, `FLEE`.
|
||||
- **Acciones de tick** periódicas (p. ej. `SHOOT`).
|
||||
- **Eventos** (`on_hit`, `on_no_health`, `on_hurt_end`, `on_destroy`) con acciones
|
||||
(`APPLY_IMPULSE`, `DECREASE_HEALTH`, `CREATE_DEBRIS`, `ADD_SCORE`, `FLASH`,
|
||||
`FIRE_BULLET`, `DESTROY`, ...).
|
||||
|
||||
Dos sistemas los ejecutan:
|
||||
|
||||
- **[EnemyAiSystem](source/game/systems/enemy_ai_system.hpp)** — `move()` aplica la
|
||||
primitiva de movimiento; `tick()` añade las acciones periódicas. Helper
|
||||
`findNearestShipPosition()` para las primitivas que buscan al jugador.
|
||||
- **[EnemyEventDispatcher](source/game/systems/enemy_event_dispatcher.hpp)** —
|
||||
ejecuta las acciones declarativas cuando se dispara un evento.
|
||||
|
||||
### 10.4 Colisiones
|
||||
|
||||
[CollisionSystem](source/game/systems/collision_system.hpp) recibe un struct
|
||||
`Context` (punteros a ships/enemies/bullets, managers de efectos, timers, scores,
|
||||
vidas y un callback `on_player_hit`) que GameScene construye en
|
||||
[buildCollisionContext](source/game/scenes/game_scene.hpp#L174). Detecta:
|
||||
bala↔enemigo, nave↔enemigo, bala↔jugador (fuego amigo / autodisparo), bala
|
||||
enemiga↔nave, y balas fuera del área. Reglas observadas: el primer impacto deja al
|
||||
enemigo *wounded*; el segundo lo destruye y suma score. La nave entra en `hurt` al
|
||||
primer toque y muere al segundo durante ese estado.
|
||||
|
||||
### 10.5 Stages y oleadas
|
||||
|
||||
- **[StageManager](source/game/stage_system/stage_manager.hpp)** — FSM del stage
|
||||
(`EstatStage`): `INIT_HUD` (anima el HUD, 3 s) → `LEVEL_START` ("ENEMY INCOMING",
|
||||
3 s, arranca `game.ogg`) → `PLAYING` → `LEVEL_COMPLETED` ("GOOD JOB COMMANDER!",
|
||||
3 s) → siguiente stage. `initDemo(stage_id)` arranca directamente en `PLAYING`
|
||||
para el attract mode.
|
||||
- **[WaveRunner](source/game/stage_system/wave_runner.hpp)** — emite los enemigos de
|
||||
cada oleada según `spawn_interval` y avanza cuando se cumple `next` (`all_dead`,
|
||||
`timeout`, o ambos).
|
||||
- **[StageConfig](source/game/stage_system/stage_config.hpp)** /
|
||||
[StageLoader](source/game/stage_system/stage_loader.hpp) — modelo y carga del
|
||||
YAML de stages.
|
||||
|
||||
### 10.6 Dos capas de colisión: física vs gameplay
|
||||
|
||||
Conviene no confundirlas, porque conviven:
|
||||
|
||||
**1. Física** — [PhysicsWorld](source/core/physics/physics_world.hpp) /
|
||||
[physics_world.cpp](source/core/physics/physics_world.cpp). Es un mundo 2D
|
||||
minimalista de arcade. Cada frame, `update(dt)` hace tres pasos:
|
||||
|
||||
1. **Integración** semi-implícita de Euler con damping exponencial
|
||||
(`v += (F·invMass)·dt; v *= exp(-damping·dt); x += v·dt`) sobre cada
|
||||
[RigidBody](source/core/physics/rigid_body.hpp) no estático. Un cuerpo con
|
||||
`mass=0` (`inverse_mass=0`) es estático (masa infinita).
|
||||
2. **Rebote contra los bordes** del `PLAYAREA` (`resolveBoundsCollisions`): reposiciona
|
||||
el cuerpo dentro del rect y refleja la componente normal de la velocidad por su
|
||||
`restitution`. Antes de reflejar, invoca un `BoundsHitCallback` opcional con la
|
||||
velocidad de impacto entrante (lo usa GameScene para los efectos de borde).
|
||||
3. **Colisiones cuerpo-cuerpo** (`resolveBodyCollisions`): broadphase trivial
|
||||
**O(n²)** (suficiente para ~23 cuerpos), círculo-círculo, con corrección posicional
|
||||
de penetración + **impulso elástico** `j = -(1+e)(v_rel·n) / (1/mₐ + 1/m_b)`
|
||||
(referencia Box2D / Chris Hecker, en `resolveBodyPair`). Los cuerpos con `radius=0`
|
||||
(las balas, cinemáticas puras) **no** participan aquí.
|
||||
|
||||
Los `RigidBody` los poseen las entidades; el mundo solo guarda punteros no-owning
|
||||
(`addBody`/`removeBody`).
|
||||
|
||||
**2. Gameplay** — [collision_system.cpp](source/game/systems/collision_system.cpp)
|
||||
(ver [§10.4](#104-colisiones)), que decide *qué pasa* (daño, score, muerte). Usa los
|
||||
helpers de [collision.hpp](source/core/physics/collision.hpp): `checkCollision`
|
||||
(círculo-círculo discreto, distancia al cuadrado sin `sqrt`) y `checkCollisionSwept`
|
||||
(segment-círculo, para que una bala rápida no atraviese un enemigo entre frames —
|
||||
*anti-tunneling*). Estos checks usan el `collision_radius` de la **entidad**
|
||||
(con amplificador opcional de hitbox), no el `radius` del body.
|
||||
|
||||
En resumen: la **física** mueve y rebota los cuerpos; el **gameplay** detecta los
|
||||
contactos relevantes para las reglas. Una bala no rebota físicamente (radius 0) pero sí
|
||||
provoca daño vía el check *swept*.
|
||||
|
||||
---
|
||||
|
||||
## 11. IA del modo demo (attract)
|
||||
|
||||
El attract mode es una partida que se juega sola para atraer al jugador. Se activa
|
||||
desde [TitleScene](source/game/scenes/title_scene.hpp) cuando el `idle_timer_` en el
|
||||
estado `MAIN` supera el umbral de inactividad, y desde
|
||||
[GameScene](source/game/scenes/game_scene.hpp) cuando `match_config_.mode == DEMO`.
|
||||
|
||||
La IA vive en [DemoPilot](source/game/systems/demo_pilot.hpp) /
|
||||
[demo_pilot.cpp](source/game/systems/demo_pilot.cpp). Su diseño es explícito en la
|
||||
cabecera: busca **parecer humano, no ser óptimo**. Características clave:
|
||||
|
||||
- **Solo lectura**: `DemoPilot::compute(ship, enemies, bullets, play_area, dt)`
|
||||
devuelve un `Control{left,right,thrust,shoot}`. No lee `Input` ni muta entidades;
|
||||
GameScene aplica el resultado vía `Ship::applyMovement` + `fireBullet`.
|
||||
- **Escenarios curados**: hay 4 (`SCENARIOS` en
|
||||
[demo_pilot.hpp:36-42](source/game/systems/demo_pilot.hpp#L36-L42)): stages
|
||||
`{5,8,6,10}` con 1 o 2 naves IA. El `SceneContext` recuerda el índice y rota al
|
||||
siguiente en cada entrada al demo.
|
||||
|
||||
**Lógica de decisión por prioridad** (verificado en `demo_pilot.cpp`, con sus
|
||||
constantes):
|
||||
|
||||
1. **Esquiva de bala** — si una bala enemiga entrante está dentro de
|
||||
`DODGE_SCAN_RADIUS = 190 px` y viene hacia la nave (`DODGE_HEADING_MIN = 0.25`),
|
||||
maniobra perpendicular a la bala con sesgo al centro (`WALL_BIAS = 0.6`); no
|
||||
dispara mientras esquiva.
|
||||
2. **Sin enemigos** — deriva tranquila (giro lento).
|
||||
3. **Peligro cercano** — si el objetivo está a menos de `DANGER_RADIUS = 95 px`, se
|
||||
aleja con sesgo al centro.
|
||||
4. **Combate** — apuntado con *lead* (`LEAD_TIME = 0.30 s`) más un error humano
|
||||
(`AIM_JITTER_MAX = 0.10 rad`); dispara si el error es menor que
|
||||
`FIRE_TOLERANCE = 0.18 rad` y el cooldown (`FIRE_COOLDOWN = 0.32 s`) lo permite;
|
||||
se acerca si está más lejos que `APPROACH_RADIUS = 250 px`.
|
||||
|
||||
Temporización "humana": reevalúa el objetivo cada `RETARGET_INTERVAL = 0.15 s` y usa
|
||||
una zona muerta de rotación (`ROTATE_DEADZONE = 0.05 rad`) para no oscilar. La demo
|
||||
se rompe con cualquier input (vuelve a TITLE) o por timeout/muerte (vuelve a LOGO),
|
||||
gestionado en [stepDemo](source/game/scenes/game_scene.hpp#L157).
|
||||
|
||||
---
|
||||
|
||||
## 12. Efectos visuales
|
||||
|
||||
Viven en [game/effects/](source/game/effects/) y son managers con pools:
|
||||
|
||||
- **[DebrisManager](source/game/effects/debris_manager.hpp)** — rompe una shape en
|
||||
fragmentos que vuelan radialmente, heredando inercia del cuerpo y, opcionalmente,
|
||||
el impulso de la bala que causó la muerte. Notifica al `Border` (bump) y al
|
||||
`Playfield` (ripple). Lo usan muerte de nave/enemigo, balas fuera de área y las
|
||||
explosiones del logo.
|
||||
- **[FireworkManager](source/game/effects/firework_manager.hpp)** — bursts de fuegos
|
||||
artificiales.
|
||||
- **[FloatingScoreManager](source/game/effects/floating_score_manager.hpp)** —
|
||||
números de puntuación flotantes ("+150").
|
||||
- **[TrailManager](source/game/effects/trail_manager.hpp)** — estela tras las naves.
|
||||
|
||||
---
|
||||
|
||||
## 13. Configuración, constantes y convenciones
|
||||
|
||||
**Configuración:**
|
||||
|
||||
- **[EngineConfig](source/core/config/engine_config.hpp)** — struct POD con
|
||||
ventana, rendering, audio, bindings de jugadores, locale, console. Es la config
|
||||
persistente (`config.yaml`), gestionada por
|
||||
[config_yaml](source/game/config_yaml.hpp) (`ConfigYaml::engine_config`,
|
||||
`loadFromFile`/`saveToFile`).
|
||||
- **[PostFxConfig](source/core/config/postfx_config.hpp)** — carga los `PostFxParams`
|
||||
(bloom/flicker/fondo) desde YAML.
|
||||
- **[GameConfig::MatchConfig](source/core/system/game_config.hpp)** — config no
|
||||
persistente de la partida (jugadores activos, modo NORMAL/DEMO).
|
||||
|
||||
**Constantes y tipos:**
|
||||
|
||||
- **[core/types.hpp](source/core/types.hpp)** — `Vec2` / `Vec3` (agregados con
|
||||
operadores y helpers como `length()`, `normalized()`, `dot()`, `cross()`).
|
||||
- **[core/defaults/](source/core/defaults/)** — un fichero por dominio
|
||||
(`window.hpp`, `rendering.hpp`, `audio.hpp`, `entities.hpp`, `notifier.hpp`...)
|
||||
con todas las constantes por defecto. `game/constants.hpp` reexporta varias como
|
||||
alias (`MAX_ORNIS`, `MAX_BULLETS`, `PI`) y añade helpers de área de juego.
|
||||
|
||||
**Convenciones de código** (de `.clang-tidy`, confirmadas en memoria de proyecto):
|
||||
|
||||
- Métodos en `camelBack`, tipos en `CamelCase`, constantes en `UPPER_CASE`.
|
||||
- Comentarios mayormente en **catalán** (algunos en castellano); el código y los
|
||||
identificadores mezclan catalán/castellano/inglés.
|
||||
- Patrón recurrente: **singletons** con `init/get/destroy` y comprobación de
|
||||
`nullptr` para degradación elegante.
|
||||
- Patrón recurrente: descomposición de funciones grandes (`update`/`draw`) en
|
||||
sub-pasos privados (`stepX`/`runX`/`drawXState`) para mantener baja la complejidad
|
||||
cognitiva.
|
||||
- Análisis estático (cppcheck/clang-tidy) corre vía git hooks
|
||||
([.githooks/](.githooks/)); la política es **arreglar la causa**, no suprimir el
|
||||
diagnóstico.
|
||||
|
||||
---
|
||||
|
||||
## 14. Guía de navegación
|
||||
|
||||
| Si quieres tocar… | Mira… |
|
||||
|---|---|
|
||||
| El arranque, orden de init, o el bucle de frame | [director.cpp](source/core/system/director.cpp) (`Director::iterate` / `handleEvent`) |
|
||||
| Las callbacks de SDL | [main.cpp](source/main.cpp) |
|
||||
| Añadir/cambiar una escena o una transición | [scene.hpp](source/core/system/scene.hpp), [scene_context.hpp](source/core/system/scene_context.hpp), `Director::buildScene` |
|
||||
| Cómo se dibuja una línea / el frame de render | [line_renderer.cpp](source/core/rendering/line_renderer.cpp) → [gpu_frame_renderer.cpp](source/core/rendering/gpu/gpu_frame_renderer.cpp) |
|
||||
| Bloom / flicker / fondo (post-proceso) | [gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp), [gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp), shaders en [shaders/](shaders/) |
|
||||
| Crear/editar una figura vectorial | `data/shapes/**/*.shp` + [shape_loader.hpp](source/core/graphics/shape_loader.hpp) |
|
||||
| El texto en pantalla | [vector_text.hpp](source/core/graphics/vector_text.hpp) |
|
||||
| Eventos globales (teclas F, ESC, hotplug) | [global_events.cpp](source/core/system/global_events.cpp) |
|
||||
| Controles, bindings, rebinding | [input.cpp](source/core/input/input.cpp), [define_inputs.cpp](source/core/input/define_inputs.cpp) |
|
||||
| Reproducir música/efectos | [audio.hpp](source/core/audio/audio.hpp), [audio_adapter.hpp](source/core/audio/audio_adapter.hpp) |
|
||||
| Cómo se cargan los recursos / el pack | [resource_loader.cpp](source/core/resources/resource_loader.cpp), [resource_pack.cpp](source/core/resources/resource_pack.cpp) |
|
||||
| Reglas de la partida, vidas, game over | [game_scene.cpp](source/game/scenes/game_scene.cpp) |
|
||||
| Comportamiento de un enemigo | su YAML en `data/entities/<tipo>/` + [enemy_ai_system.cpp](source/game/systems/enemy_ai_system.cpp) |
|
||||
| Definir oleadas / dificultad de un nivel | [data/stages/stages.yaml](data/stages/stages.yaml) + [stage_manager.cpp](source/game/stage_system/stage_manager.cpp) |
|
||||
| Colisiones | [collision_system.cpp](source/game/systems/collision_system.cpp) |
|
||||
| La IA del modo demo | [demo_pilot.cpp](source/game/systems/demo_pilot.cpp) |
|
||||
| Explosiones / partículas | [debris_manager.cpp](source/game/effects/debris_manager.cpp) |
|
||||
| El menú de servicio (F12) | [service_menu.cpp](source/core/system/service_menu.cpp) |
|
||||
| Textos traducibles | `data/locale/*.yaml` + [locale.cpp](source/core/locale/locale.cpp) |
|
||||
| Constantes por defecto | [core/defaults/](source/core/defaults/) |
|
||||
|
||||
---
|
||||
|
||||
### Notas de honestidad sobre la cobertura
|
||||
|
||||
- Todas las secciones se verificaron leyendo directamente los ficheros y firmas
|
||||
citados, incluyendo el **pipeline de compilación de shaders**
|
||||
([§5.5](#55-shaders-fuentes-compilación-y-selección): `CMakeLists.txt` +
|
||||
`tools/shaders/compile_spirv.cmake` + `shader_factory.hpp`) y el interior de la
|
||||
**física** ([§10.6](#106-dos-capas-de-colisión-física-vs-gameplay):
|
||||
`physics_world.cpp` + `collision.hpp` + `rigid_body.hpp`).
|
||||
- Lo que **no** se ha trazado a fondo y queda como lectura directa del código si hace
|
||||
falta: los detalles finos de animación de cada overlay (curvas de easing del
|
||||
`Notifier`/`ServiceMenu`) y la coreografía interna completa de `LogoScene` y
|
||||
`TitleScene` (más allá de sus estados). Son descriptivos, no estructurales.
|
||||
</content>
|
||||
</invoke>
|
||||
Reference in New Issue
Block a user