Project Structure

File Layout

The project uses a two-folder structure separating engine code from game code:

managed/                        ← ENGINE (stable, user doesn't edit)
  SaFiEngine.csproj               .NET project file (IDE IntelliSense only, not used by build)
  core/
    World.cs                      ECS world: entities, components, systems, queries
    Components.cs                 Built-in: Transform, MeshComponent, Movable, Camera, Light, Rigidbody, Collider
    FreeCameraState.cs            Static state for the debug free camera
  rendering/
    NativeBridge.cs               P/Invoke declarations for C++ renderer
  physics/
    PhysicsBridge.cs              P/Invoke declarations for joltc physics library
    PhysicsWorld.cs               Jolt Physics lifecycle, body tracking, fixed timestep
  runtime/
    Viewer.cs                     Entry point (Main), main loop
    HotReload.cs                  File watcher + recompiler (dev mode only)

game_logic/                     ← GAME CODE (user edits these)
  Game.cs                         Scene setup + system registration (Game.Setup entry point)
  GameConstants.cs                Tunable config values (debug, sensitivity, speed)
  systems/                        Per-frame system logic (one file per system, partial class)
    InputMovementSystem.cs        WASD input handling
    TimerSystem.cs                Countdown/interval timers
    PhysicsSystem.cs              Jolt physics step + body creation + transform sync
    CameraSystems.cs              FreeCameraSystem + CameraFollowSystem
    LightSyncSystem.cs            Push light data to GPU
    HierarchyTransformSystem.cs   Parent-child world transforms
    DebugSystems.cs               Debug overlay toggle + collider wireframes
    RenderSyncSystem.cs           Push entity transforms to GPU (always last)

native/
  joltc/                          Jolt Physics C API (git submodule)
  renderer.h                      VulkanRenderer class declaration
  bridge.cpp                      extern "C" bridge functions
  renderer/                       Core Vulkan renderer
    renderer.cpp                  Init, cleanup, renderFrame, camera, input, time, lighting
    vulkan_init.cpp               Instance, device, swapchain, pipelines, sync objects
    vulkan_util.cpp               Buffer/image/shader utilities
  scene/                          Scene objects
    mesh.cpp                      glTF loading + procedural primitives
    entity.cpp                    Entity pool management + debug entities
    material.cpp                  Textures, materials, samplers
  ui/                             UI overlay
    ui.cpp                        UI pipeline, font atlas, text rendering
  shaders/
    shader.vert                   Vertex shader (UBO for view/proj, push constant for model)
    shader.frag                   Fragment shader (Blinn-Phong, up to 8 lights)
    ui.vert                       UI vertex shader (pixel-to-NDC via push constant)
    ui.frag                       UI fragment shader (R8 font atlas sampling + alpha)
  vendor/
    cgltf.h                       glTF 2.0 parsing library
    stb_truetype.h                Font rasterization library
    stb_image.h                   Image decoding library (PNG, JPEG)

assets/
  fonts/
    RobotoMono-Regular.ttf        Monospace font for debug overlay

models/                           glTF model files (.glb)

Solution & Project Files

SaFiEngine.sln (repo root) and managed/SaFiEngine.csproj provide C# IntelliSense in VS Code and other IDEs. They are not used by the Makefile build. After cloning, run dotnet restore once to enable autocomplete and go-to-definition.

Adding New .cs Files

The Makefile uses two separate file lists — one for engine code, one for game logic:

MANAGED_CS = managed/runtime/Viewer.cs managed/core/World.cs managed/core/Components.cs \
             managed/rendering/NativeBridge.cs managed/core/FreeCameraState.cs \
             managed/physics/PhysicsBridge.cs managed/physics/PhysicsWorld.cs
GAMELOGIC_CS_FILES = game_logic/Game.cs game_logic/GameConstants.cs \
    game_logic/systems/InputMovementSystem.cs \
    game_logic/systems/TimerSystem.cs \
    game_logic/systems/PhysicsSystem.cs \
    game_logic/systems/CameraSystems.cs \
    game_logic/systems/LightSyncSystem.cs \
    game_logic/systems/HierarchyTransformSystem.cs \
    game_logic/systems/DebugSystems.cs \
    game_logic/systems/RenderSyncSystem.cs
VIEWER_CS = $(MANAGED_CS) $(GAMELOGIC_CS_FILES)
  • Game files go in game_logic/ (or game_logic/systems/ for systems) — add the path to GAMELOGIC_CS_FILES in the Makefile
  • Engine files go in the appropriate managed/ subdirectory (core/, rendering/, physics/, or runtime/) — add the path to MANAGED_CS in the Makefile

The .csproj uses EnableDefaultCompileItems so it discovers new files automatically — no project file changes needed. Only the Makefile needs updating.

Adding New Components

  1. Create a class in managed/core/Components.cs (or a new file in the appropriate managed/ subdirectory)
  2. Add fields with default values
  3. If in a new file, add it to MANAGED_CS in the Makefile
public class Health
{
    public float Current = 100f;
    public float Max = 100f;
}

Adding New Systems

  1. Create a new file in game_logic/systems/ using the partial class Systems pattern (or add to an existing system file)
  2. Add the path to GAMELOGIC_CS_FILES in the Makefile
  3. Register it with world.AddSystem() in game_logic/Game.cs
  4. Place it before RenderSyncSystem in the registration order
public static void MySystem(World world)
{
    var entities = world.Query(typeof(MyComponent));
    foreach (int e in entities)
    {
        // ...
    }
}

Scene Setup

game_logic/Game.cs is the entry point for all game-specific setup. The engine calls Game.Setup(world) after initializing the renderer. This is where you spawn entities, configure components, and register systems:

public static class Game
{
    public static void Setup(World world)
    {
        // Spawn entities, add components, register systems here
    }
}

Hot Reload (Dev Mode)

make dev watches game_logic/ for .cs changes and live-reloads without restarting. On save, the engine recompiles all game logic files, resets the world (despawns all entities, clears lights), and re-runs Game.Setup(world) from the new assembly. This means any change to Game.cs, GameConstants.cs, or systems/*.cs takes effect immediately — including new entities, changed lights, updated constants, and re-ordered systems.

Limitations:

  • Only game_logic/ files are hot-reloadable. Engine changes (managed/) require a restart.
  • Old assemblies leak memory (Mono can't unload). Restart after many reloads.
  • Static fields in systems/*.cs and GameConstants.cs values reset on each reload.