163 lines
16 KiB
Markdown
163 lines
16 KiB
Markdown
# Structure
|
|
|
|
## Modules
|
|
|
|
### `src/entity.rs`
|
|
Manages entity lifecycle with `EntityHandle` (u64 alias) and `EntityManager` tracking alive entities. Entities are opaque IDs; all data lives in component storages on `World`.
|
|
|
|
### `src/world.rs`
|
|
Core ECS container holding `EntityManager`, 20+ typed `Storage<T>` collections (one per component type), and intent queues (`follow_player_intents`, `stop_following_intents`, `camera_transition_intents`). One-frame intents are inserted and consumed within a single frame. World also holds singleton state: `snow_layer`, `debug_mode`, `gizmo_mesh`.
|
|
|
|
### `src/components/`
|
|
Component types define entity data. Key storages:
|
|
- **Transforms** (`Transform`): position, rotation, scale
|
|
- **Physics** (`PhysicsComponent`): rapier3d rigidbody/collider handles
|
|
- **Movement** (`MovementComponent`): walking speed, acceleration, damping state
|
|
- **Jump** (`JumpComponent`): jump height, air control, jump curve
|
|
- **State machines** (stored per-entity): `IdleState`, `WalkingState`, `JumpingState`, `FallingState`, `LeapingState`, `RollingState`
|
|
- **Input** (`InputComponent`): current move direction, jump/parry key states
|
|
- **Camera** (`CameraComponent`): FOV, aspect, yaw/pitch; `CameraTransition` for animated transitions
|
|
- **Dialog** (`DialogBubbleComponent`, `DialogProjectileComponent`, `DialogSourceComponent`): Ink story state, projectile tracking, outcome events
|
|
- **Rendering** (`MeshComponent`): mesh reference, pipeline, instance buffer, dissolve/snow-light flags
|
|
- **Misc**: `FollowComponent`, `RotateComponent`, `DissolveComponent`, `TriggerComponent`, `ParticleEmitterConfig`
|
|
|
|
### `src/states/state.rs`
|
|
Per-entity state machine: `StateMachine` holds current `TypeId`, registered state types, and transitions. `State` trait defines lifecycle (`on_enter`, `on_physics_update`, `on_exit`). Transitions are condition predicates checked each update. Player entity uses this for locomotion states.
|
|
|
|
### `src/systems/`
|
|
Flat list of update functions called in main loop. Systems receive specific storage parameters (e.g., `&mut Storage<Transform>`, `&Storage<PhysicsComponent>`) instead of `&World`/`&mut World`. This makes data dependencies explicit for both borrow checker and reader. No cross-system coupling; all communication via world state and intents.
|
|
- **Camera**: `camera_input_system(cameras, follows, input_state)` → generates intents; `camera_intent_system(follow_player_intents, stop_following_intents, camera_transition_intents, ...)` consumes them; `camera_follow_system(follows, cameras, transforms)`, `camera_noclip_system(cameras, follows, transforms, input_state, delta)`, `camera_transition_system(camera_transitions, transforms, cameras, delta)`, `camera_ground_clamp_system(cameras, follows, transforms)`
|
|
- **Input**: `player_input_system(cameras, follows, player_tags, inputs, input_state)` reads SDL3 `InputState`, writes `InputComponent`
|
|
- **Physics**: `state_machine_physics_system(&mut World, FIXED_TIMESTEP)` (fixed-step), `PhysicsManager::physics_step()`, `physics_sync_system(entities, physics, transforms)` copies rapier bodies back to transforms, `trigger_system(trigger_events, triggers, transforms, player_tags)` (AABB overlap → events)
|
|
- **Dialog**: `dialog_system(entities, trigger_events, dialog_sources, bubble_tags, dialog_bubbles, transforms, names, player_tags, projectile_tags, dialog_projectiles, dialog_outcomes, delta)` ticks story state; `dialog_projectile_system(player_tags, transforms, projectile_tags, dialog_projectiles, spawn_particle_intents, dialog_outcomes, leaping_states, rolling_states, input_state)` moves projectiles; `dialog_camera_system(cameras, transforms, bubble_tags, player_pos, delta)` focuses camera on speaker; `dialog_bubble_render_system(transforms, dialog_bubbles, bubble_tags, camera_pos, view_proj)` generates billboard/text draw calls
|
|
- **Rendering**: `render_system(entities, transforms, meshes, dissolves)` → `Vec<DrawCall>` from all meshes; snow layer adds clipmap draw calls; debug adds collider/gizmo calls; `spotlight_sync_system(spotlights, transforms)` syncs light positions to shader uniform
|
|
- **State machine**: `state_machine_system(&mut World, delta)` (per-frame), `state_machine_physics_system(&mut World, FIXED_TIMESTEP)` (fixed-step) tick state lifecycle
|
|
- **Trees**: `tree_occlusion_system(player_tags, transforms, cameras, tree_instances)` culls trees behind camera; `tree_dissolve_update_system(tree_instances, delta)` animates dissolve; `tree_instance_buffer_update_system(tree_instances)` writes GPU buffer
|
|
- **Snow**: `snow_system(cameras, transforms, player_tags, follows, snow_layer)` deforms snow layer at physics contacts; `particle_intent_system(particle_buffers, spawn_particle_intents)`/`particle_update_system(particle_buffers, delta)` manage particle emitters
|
|
- **Rotate**: `rotate_system(rotates, transforms, delta)` rotates entities by delta
|
|
|
|
### `src/bundles/`
|
|
Factory functions to spawn pre-configured entity groups:
|
|
- `PlayerBundle`: player character with all locomotion components
|
|
- `TestCharBundle`: test NPC
|
|
- `CameraBundle`: camera entity
|
|
- `TerrainBundle`: terrain mesh + collider
|
|
- `SpotlightBundle` + `spawn_spotlights()`: light entities from scene data
|
|
|
|
### `src/render/`
|
|
GPU rendering pipeline via wgpu. Singleton `Renderer` stored in thread-local (global.rs).
|
|
- **`Renderer`**: wgpu device/queue/surface, framebuffer, pipelines, bind groups, shadow map texture, spotlight data
|
|
- **`DrawCall`** (types.rs): vertex/index buffers, model matrix, pipeline enum, instance buffer, entity ref
|
|
- **Pipelines** (pipeline.rs): `create_main_pipeline()`, `create_snow_clipmap_pipeline()`, `create_wireframe_pipeline()`, `create_shadow_pipeline()`, `create_debug_lines_pipeline()`
|
|
- **`Uniforms`** (types.rs): model/view/projection, 4 spotlights, camera position, height scale, player position, time, tile scale, debug mode
|
|
- **Shadow**: `render_shadow_pass()` renders scene depth to shadow map from each spotlight
|
|
- **Snow**: `SnowLayer` deforms snow heightfield via compute; `ClipmapConfig` manages multi-level clipmap grid; `deform_at_position()` marks terrain changed
|
|
- **Snow light**: `SnowLightAccumulation` ping-pong texture accumulates light contributions from spotlights onto snow surface
|
|
- **Billboard pipeline** + **Text pipeline**: render dialog bubbles and text overlays
|
|
- **Particle pipeline**: billboarded particles with per-instance color/velocity
|
|
- **Font atlas**: pre-rasterized glyph texture from embedded font file
|
|
|
|
### `src/loaders/`
|
|
Load scene data from glTF files:
|
|
- **`scene.rs`** `Space::load_space()`: loads meshes, lights, player spawn, test char spawn from single glTF
|
|
- **`mesh.rs`** `Mesh::load_gltf_with_instances()`: parses glTF buffers, vertex/index data, multi-instance mesh batches; `InstanceData` (position/rotation/scale/dissolve); `Vertex` (position/normal/UV)
|
|
- **`lights.rs`**: extracts spotlight transforms/params from glTF nodes
|
|
- **`empty.rs`**: extracts named empty (spawn point) transforms from glTF
|
|
- **`heightmap.rs`**: loads EXR heightfield texture for terrain collision
|
|
- **`terrain.rs`**: builds rapier heightfield collider from EXR matrix
|
|
|
|
### `src/physics.rs`
|
|
Thread-local `PhysicsManager` wrapping rapier3d. `physics_step()` runs one integration step; `add_rigidbody()`, `add_collider()` register bodies; `raycast()` queries. `HeightfieldData` caches terrain height matrix.
|
|
|
|
### `src/utility/`
|
|
- **`transform.rs`** `Transform`: matrix conversions to/from nalgebra `Isometry3`; getters/setters for position/rotation/scale
|
|
- **`input.rs`** `InputState`: SDL3 key states (WASD, Space, Shift, Ctrl) and mouse delta; `handle_event()` updates state; `clear_just_pressed()` resets one-frame flags
|
|
- **`time.rs`**: `Time::get_time_elapsed()` returns seconds since init (static Instant)
|
|
|
|
### `src/debug/`
|
|
- **`mode.rs`** `DebugMode` enum: None, Normals, UV, Depth, Wireframe, Colliders, ShadowMap, SnowLight; `cycle()` steps through
|
|
- **`collider_debug.rs`**: renders rapier collider AABBs as line meshes
|
|
- **`gizmo.rs`**: renders 3D transform gizmo (position/rotation/scale) for editor
|
|
|
|
### `src/editor/`
|
|
- **`inspector.rs`** `Inspector`: wraps Dear ImGui context; `render()` draws frame to texture; `build_ui()` draws entity inspector panels
|
|
- **`mod.rs`** `EditorState`: manages editor active state, selected entity, mouse capture; `editor_loop()` calls inspector, handles picking
|
|
|
|
### `src/picking.rs`, `src/postprocess.rs`, `src/texture.rs`, `src/paths.rs`
|
|
Utility modules: ray casting for mouse pick, fullscreen blit/framebuffer downsampling, dither/flowmap texture loading, paths to asset files.
|
|
|
|
## Data Flow
|
|
|
|
1. **Initialization** (`main.rs` `init()`): SDL3 window → wgpu renderer (Vulkan) → World creation → load scene (Space from glTF) → spawn bundles (player, terrain, camera, lights) → initialize physics
|
|
2. **Main loop** (`main.rs` `main()` with 60 Hz fixed physics + variable-rate graphics):
|
|
- **Per-frame** (delta): Input events → `player_input_system(cameras, follows, player_tags, inputs, input_state)` (fills `InputComponent`) + `camera_input_system(cameras, follows, input_state)` (generates intents)
|
|
- **Intent processing**: `camera_intent_system(follow_player_intents, stop_following_intents, camera_transition_intents, follows, transforms, cameras, player_tags, camera_transitions)` consumes intents, updates `CameraComponent`/`CameraTransition`
|
|
- **Camera systems**: `camera_follow_system(follows, cameras, transforms)` follows player; `camera_noclip_system(cameras, follows, transforms, input_state, delta)` noclip mode; `camera_transition_system(camera_transitions, transforms, cameras, delta)` animated transitions; `camera_ground_clamp_system(cameras, follows, transforms)` clamps to ground
|
|
- **Fixed physics** (1/60s accumulator):
|
|
- `state_machine_physics_system(&mut World, FIXED_TIMESTEP)`: tick state's `on_physics_update()`
|
|
- `PhysicsManager::physics_step()`: rapier integration
|
|
- `physics_sync_system(entities, physics, transforms)`: copy rigidbody poses to `Transform`
|
|
- `trigger_system(trigger_events, triggers, transforms, player_tags)`: detect collisions, emit events
|
|
- `dialog_system(entities, trigger_events, dialog_sources, bubble_tags, dialog_bubbles, transforms, names, player_tags, projectile_tags, dialog_projectiles, dialog_outcomes, delta)` ticks story state; `dialog_projectile_system(player_tags, transforms, projectile_tags, dialog_projectiles, spawn_particle_intents, dialog_outcomes, leaping_states, rolling_states, input_state)` moves projectiles
|
|
- **Per-frame systems**: `state_machine_system(&mut World, delta)` ticks state lifecycle; `rotate_system(rotates, transforms, delta)` rotates entities; `particle_intent_system(particle_buffers, spawn_particle_intents)`/`particle_update_system(particle_buffers, delta)` manage particles; `tree_occlusion_system(player_tags, transforms, cameras, tree_instances)` culls; `tree_dissolve_update_system(tree_instances, delta)`/`tree_instance_buffer_update_system(tree_instances)` updates dissolve; `snow_system(cameras, transforms, player_tags, follows, snow_layer)` deforms snow; `spotlight_sync_system(spotlights, transforms)` syncs lights
|
|
- **Render collection**: `render_system(entities, transforms, meshes, dissolves)` → `Vec<DrawCall>` from all meshes; snow layer adds clipmap draw calls; debug adds collider/gizmo calls
|
|
- **Submission**: `submit_frame()` renders draw calls, dialog bubbles/text, particles to framebuffer; blit to screen; optional ImGui overlay
|
|
3. **Frame cleanup**: `InputState::clear_just_pressed()` resets one-frame flags
|
|
|
|
## Key Types
|
|
|
|
| Type | Module | Description |
|
|
|------|--------|-------------|
|
|
| `EntityHandle` | entity | u64 opaque entity ID |
|
|
| `Storage<T>` | world | HashMap storage for per-entity component data; exposes `.get()`, `.get_mut()`, `.with_mut()`, `.all()` methods |
|
|
| `World` | world | ECS container: entities, 20+ storages, intent queues, singleton state |
|
|
| `Transform` | utility/transform | Position (Vec3), rotation (Quat), scale (Vec3); matrix conversions |
|
|
| `StateMachine` | states/state | Per-entity state machine: current state TypeId, registered states, transitions |
|
|
| `State` trait | states/state | Lifecycle: `on_enter`, `on_physics_update`, `on_exit`, `on_update` |
|
|
| `PhysicsComponent` | components/physics | Rapier3d rigidbody + optional collider handles |
|
|
| `MovementComponent` | components/movement | Walking speed, acceleration, damping, context (floored, last floor time) |
|
|
| `JumpComponent` | components/jump | Jump height, duration, air control, context (in progress, origin height) |
|
|
| `CameraComponent` | components/camera | FOV, aspect, yaw/pitch angles, is_active flag |
|
|
| `InputComponent` | components/input | Current frame: move direction, jump/parry key states (flags) |
|
|
| `MeshComponent` | components/mesh | Mesh ref, pipeline enum, instance buffer, dissolve/snow-light flags |
|
|
| `DialogBubbleComponent` | components/dialog | Ink story, current text, dialog phase (displaying/projectile in flight), parry button |
|
|
| `DrawCall` | render/types | GPU command: vertex/index buffers, model matrix, pipeline, instance count, entity ID |
|
|
| `Uniforms` | render/types | Per-frame shader data: matrices, spotlight array, debug flags |
|
|
| `Renderer` | render/mod | GPU state: device, queue, surface, all pipelines, framebuffer, texture samplers |
|
|
| `Space` | loaders/scene | Loaded scene: mesh batches, spotlight data, spawn positions |
|
|
| `Mesh` | loaders/mesh | GPU vertex/index buffers, AABB, CPU vertex data for physics |
|
|
| `SnowLayer` | render/snow | Snow heightfield: deform bind groups, depth texture, clipmap grid levels |
|
|
| `SnowLightAccumulation` | render/snow_light | Ping-pong textures + pipeline accumulating spotlight contributions onto snow |
|
|
| `InputState` | utility/input | SDL3 event state: key flags, mouse delta, relative mode |
|
|
| `PhysicsManager` | physics | Rapier3d bodies/colliders/pipeline; thread-local singleton |
|
|
| `DebugMode` | debug/mode | Enum: None, Normals, UV, Depth, Wireframe, Colliders, ShadowMap, SnowLight |
|
|
|
|
## Entry Points
|
|
|
|
1. **`main()` in src/main.rs**
|
|
- Calls `init()` → initializes SDL3, wgpu, loads world from scene glTF, spawns entities
|
|
- Runs infinite loop: event poll → systems (camera/input/physics/render) → frame submit → sleep to 60 Hz
|
|
2. **`Game::init()` in src/main.rs**
|
|
- SDL3 window + Vulkan surface
|
|
- `Renderer::new()` initializes all GPU pipelines and textures
|
|
- `init_world()` loads space, spawns bundles, sets up terrain/snow/lights
|
|
3. **`World::new()` in src/world.rs**
|
|
- Creates empty storages for all 20+ component types and intent queues
|
|
|
|
## Dependencies
|
|
|
|
- **Engine**: wgpu (GPU), rapier3d (physics), glam (math), nalgebra (physics conversions), SDL3 (windowing/input)
|
|
- **Content**: bladeink (Ink story scripting), image crate (EXR heightmaps), gltf (scene loading)
|
|
- **Editor**: Dear ImGui via imgui crate + SDL3 integration
|
|
- **Internal**: All systems take explicit storage parameters from `World`; systems are called in fixed sequence from main loop; no circular dependencies
|
|
- **Thread-local singletons**: `Renderer` (render/global.rs), `PhysicsManager` (physics.rs), `GLOBAL_RENDERER`, `GLOBAL_PHYSICS`
|
|
|
|
## Initialization Order
|
|
|
|
1. SDL3 init → window creation → Vulkan adapter/device
|
|
2. `Renderer::new()` → wgpu device/queue, create all pipelines, load textures (dither, flowmap, font atlas, shadow map, blue noise)
|
|
3. Load scene from glTF → meshes, lights, spawns
|
|
4. Spawn bundles: player (with input/movement/jump/state machine), terrain (mesh + heightfield collider), camera (follows player), lights (spotlights)
|
|
5. Initialize snow layer (deform by tree positions)
|
|
6. Initialize snow light accumulation (bind to spotlight data)
|
|
7. Main loop: process events → run systems in sequence → submit frame |