3DEngine 4.0.1

dotnet add package 3DEngine --version 4.0.1
                    
NuGet\Install-Package 3DEngine -Version 4.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="3DEngine" Version="4.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="3DEngine" Version="4.0.1" />
                    
Directory.Packages.props
<PackageReference Include="3DEngine" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add 3DEngine --version 4.0.1
                    
#r "nuget: 3DEngine, 4.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package 3DEngine@4.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=3DEngine&version=4.0.1
                    
Install as a Cake Addin
#tool nuget:?package=3DEngine&version=4.0.1
                    
Install as a Cake Tool

<p align="center" style="text-align:center"> <img src="Images/3DEngineIcon.png" alt="3D Engine Icon" width="256"/> </p>

<h1 align="center" style="text-align:center">3D Engine</h1> <h4 align="center" style="text-align:center">C# 3D Game Engine (Vulkan + SDL3).</h4> <p align="center" style="text-align:center">Editor planned with Avalonia UI.</p>

<p align="center" style="text-align:center"> <img alt=".NET" src="https://img.shields.io/badge/.NET-10.0-512BD4"> <img alt="Graphics API" src="https://img.shields.io/badge/Graphics-Vulkan-AC162C"> <img alt="Windowing" src="https://img.shields.io/badge/Windowing-SDL3-0B7BB2"> </p>

<p align="center" style="text-align:center"> <img alt="Status" src="https://img.shields.io/badge/Status-Early%20Preview-yellow"> <img alt="Platforms" src="https://img.shields.io/badge/Platforms-Linux%20%7C%20Windows%20%7C%20macOS-lightgrey"> </p>

Overview

This repository is the restart of a cross‑platform 3D engine written in C#. The runtime is being built on Vulkan for rendering and SDL3 for windowing/input. An editor built with Avalonia UI is planned but not implemented yet.

The runtime already includes a minimal ECS and a behavior system powered by a Roslyn source generator. You can author gameplay logic in two ways:

  • Behavior system (attribute-based): mark a struct with [Behavior], add methods with stage attributes like [OnUpdate], and the generator wires them into the schedule.
  • Native ECS style: manually add systems to stages and work with ECSWorld queries and ECSCommands.

Design Goals

  • Ergonomic ECS: Lightweight world + staged schedule inspired by Bevy; attribute-driven behaviors to reduce boilerplate.
  • Explicit Rendering: Vulkan-first with a modern resource and synchronization model; avoid hidden abstractions.
  • Extensibility: Plugins compose engine services; source generation handles repetitive registration tasks.
  • Cross‑Platform: Linux, Windows, macOS (MoltenVK) targeted with minimal platform-specific code in user land.
  • Separation of Concerns: Runtime (game loop, ECS, renderer) separate from future editor (Avalonia UI) tooling layer.
  • Fast Iteration: Hot‑reload ambitions for shaders/assets; clear, inspectable runtime overlay via ImGui.

Current Status

  • SDL3 bootstrap that creates a window and drives a staged update loop.
  • Vulkan and related native dependencies are wired via NuGet (Vortice.* and SDL3‑CS bundles).
  • ECS core (entities/components, simple queries) and a behavior source generator that auto-registers systems.
  • ImGui runtime overlay is available for diagnostics.
  • APIs are evolving; breaking changes are expected.

See Engine/Program.cs for usage examples.

Features

Implemented (early preview):

  • Staged update loop (Startup → First → PreUpdate → Update → PostUpdate → Render → Last)
  • Cache-friendly ECS (sparse-set storage, per-frame bitset change tracking, zero-allocation ref iterators)
  • Attribute-based behavior system with Roslyn source generator
  • Basic plugin model (DefaultPlugins) for window/time/input/ECS/ImGui
  • ImGui runtime overlay integration
  • Vulkan device + window initialization scaffolding (rendering pipeline WIP)

In Progress / Planned (see roadmap for detail):

  • Renderer: swapchain, command buffers, descriptor sets, shader reflection
  • Asset pipeline: import, packaging, caching
  • Scene graph & serialization (USD integration)
  • Editor (Avalonia) with dockable tools & inspectors
  • Job system & parallelism

Tech Stack

  • SDL3 + SDL3‑CS: cross‑platform windowing, input, and basic rendering bootstrap.
  • Vulkan via Vortice.Vulkan and VMA: modern, explicit GPU API and memory allocator.
  • SPIR‑V toolchain (Vortice.SPIRV, SpirvCross): shader compilation and reflection.
  • ImGui bundle: debug and tooling UI (runtime overlay; editor planned separately).
  • AssimpNet: asset import for common 3D formats (planned integration).
  • USD.NET / UniversalSceneDescription: scene and asset interchange (planned integration).
  • Newtonsoft.Json: configuration and serialization utilities.

Supported Platforms

  • Linux: X11 and Wayland supported via SDL3. Ensure recent Vulkan drivers (Mesa/NVIDIA/AMD).
  • Windows: Vulkan-capable GPU + drivers. No WinUI/WinAppSDK dependency; editor will use Avalonia.
  • macOS: via Vulkan portability (MoltenVK) as available through dependencies. Status: experimental.

Build and Run

Prerequisites:

  • .NET SDK matching TargetFramework (see Engine/Engine.csproj, currently net10.0). Preview SDK may be required.
  • A working Vulkan driver/runtime for your GPU.

Clone + build:

# Clone
git clone https://github.com/CanTalat-Yakan/3DEngine.git
cd 3DEngine

# Restore & build all projects
dotnet build

# Run the engine sample (runtime entry point)
dotnet run --project Engine

Open in an IDE (Rider/VS/VSCode) using 3DEngine.sln if you prefer.

Notes:

  • Native binaries for SDL3 and related libraries are bundled via NuGet. On Linux you still need standard system libs ( X11/Wayland, audio, etc.) and up‑to‑date GPU drivers.
  • If Vulkan initialization fails, verify your driver installation and ensure validation layers are either installed or disabled.

Quick Start

An automatic minimal behavior + system example:

[Behavior]
public struct Spinner
{
    public float Angle;

    [OnStartup]
    public static void Spawn(BehaviorContext ctx)
    {
        var e = ctx.Ecs.Spawn();
        ctx.Ecs.Add(e, new Spinner { Angle = 0f });
    }

    [OnUpdate]
    public void Tick(BehaviorContext ctx)
    {
        Angle += (float)ctx.Time.DeltaSeconds * 90f; // 90 deg/s
        Console.WriteLine($"Entity {ctx.EntityID} angle now {Angle:0.00}");
    }
}
  1. Add the struct in any runtime project.
  2. Build: the source generator emits systems automatically.
  3. Run: the console prints per-frame updates from the behavior.

To add a manual system instead:

public sealed class Program
{
    [STAThread]
    private static void Main()
    {
        new App(Config.GetDefault())
            .AddPlugin(new DefaultPlugins())
            .AddPlugin(new SamplePlugin())
            .Run();
    }
}

public sealed class SamplePlugin : IPlugin
{
    public void Build(App app)
    {
        app.AddSystem(Stage.Startup, (world) =>
        {
            var ecs = world.Resource<EcsWorld>();
            var e = ecs.Spawn();
            ecs.Add(e, new Spinner { Angle = 0f });
        });
        
        app.AddSystem(Stage.Update, (world) =>
        {
            var ecs = world.Resource<EcsWorld>();
            foreach (var (e, spinner) in ecs.Query<Spinner>())
            {
                var newSpinner = spinner;
                newSpinner.Angle += (float)world.Resource<Time>().DeltaSeconds * 45f;
                ecs.Update(e, newSpinner);
            }
        });
    }
}

How It Works

Stages and the Schedule

The engine drives a Bevy-like staged loop (Source/App/Stage.cs):

  • Startup (once), then per frame: FirstPreUpdateUpdatePostUpdateRenderLast.
  • Systems are SystemFn(World world) delegates added to stages via App.AddSystem(stage, system) and executed by Schedule.

Plugins

Plugins configure the app and register systems. DefaultPlugins wires everything you typically need:

  • Window, time, input, events
  • ECS world/commands and a post-update command application pass
  • Auto-registration of generated behavior systems (see below)
  • Kernel/ImGui setup and a clear-color system

Resources

World is a simple resource container (Bevy-style). Insert and fetch singletons by type:

  • app.InsertResource(new MyService()) or world.InsertResource(value)
  • world.Resource<T>() to retrieve; throws if missing
  • BehaviorContext.Res<T>() is a shortcut for world.Resource<T>()

Common resources used by systems/behaviors:

  • EcsWorld – entity/component storage and queries
  • EcsCommands – queued mutations applied after Update (at PostUpdate)
  • AppWindow, Time, GUIRenderer, etc.

ECS: Entities, Components, and Commands

  • Entities are int IDs only (no public handle type). Create with var id = ecs.Spawn(); and remove with ecs.Despawn(id);.
  • Component APIs (ID-only): Add<T>(id, comp), Update<T>(id, comp), TryGet<T>(id, out comp), Has<T>(id), Remove<T>(id).
  • Queries: ecs.Query<T>(), ecs.Query<T1,T2>(), ecs.Query<T1,T2,T3>() iterate matching entities. Joins walk the smallest set for speed.
  • Mutations can be immediate or queued via EcsCommands (ID-only): Spawn, Add<T>, Despawn. Commands are applied in PostUpdate.
  • Disposal: On Despawn, components that implement IDisposable are disposed.

Beyond Query, the ECS exposes zero-allocation iteration and transforms for hot loops:

  • In-place ref iteration: foreach (var rc in ecs.IterateRef<T>()) { rc.Component ... } (marks changed automatically).
  • Transform helpers:
    • TransformEach<T>((id, c) => { /* mutate */ return c; })
    • ParallelTransformEach<T>((id, c) => { /* mutate */ return c; })
  • Span access: var span = ecs.GetSpan<T>(); returns entities/components spans for tight loops (manual marking recommended).

See also: ECS Iteration Modes, Change Tracking, and Entity Generations.

ECS Iteration Modes: Query vs IterateRef vs GetSpan

  • Query<T>/Query<T1,T2[,T3]>:
    • Returns value tuples; component structs are copied when iterating.
    • Easy and LINQ-friendly; good for read-only scans or when you call Update after changing a local copy.
    • Does not mark components as changed.
  • IterateRef<T>/IterateRef<T1,T2>:
    • Zero-allocation ref enumerators; returns refs to live component storage for in-place mutation.
    • Marks components as changed while iterating (suitable for systems that mutate frequently).
    • Ref structs can’t be stored/escaped; use immediately inside the loop.
  • GetSpan<T>:
    • Returns (ReadOnlySpan<int> Entities, Span<T> Components) for manual indexed loops.
    • Doesn’t mark changed by itself; combine with TransformEach<T> (no-op transform) to mark, or call Update.

Examples:

// Read-only query
foreach (var (e, comp) in ecs.Query<Position>())
{
    // inspect comp
}

// In-place mutation with IterateRef (marks changed)
foreach (var rc in ecs.IterateRef<Velocity>())
{
    rc.Component.dx += 1;
}

// Transform helper (marks changed)
ecs.TransformEach<Position>((e, p) => { p.x += 1; return p; });

// Parallel transform (marks changed)
ecs.ParallelTransformEach<Position>((e, p) => { p.x += 1; return p; });

// Spans (manual marking)
var span = ecs.GetSpan<Mass>();
for (int i = 0; i < span.Entities.Length; i++)
{
    span.Components[i].value *= 2;
}
// mark all as changed without altering values
ecs.TransformEach<Mass>((e, m) => m);

Change Tracking

  • Each component store maintains a per-entity “changed this frame” bitset.
  • The bit is set when you Update, when a TransformEach writes, or as you iterate via IterateRef.
  • Changed<T>(id) reads the bit; bits are cleared at BeginFrame() (stage First).

Entity Generations

  • The world tracks a generation counter per entity ID internally to guard against stale IDs.
  • On Despawn(id), the generation for that ID is incremented and the ID is added to a free list for reuse.
  • Public API remains ID-only. For diagnostics or tooling, you can inspect the current generation via ecs.GetGeneration(id).

ECS Internals and File Layout

  • Storage: Sparse-set layout (sparse index + dense arrays for entities and components) — cache-friendly, O(1) lookups.
  • Changed flags: Compact bitset aligned to dense storage; cleared per frame.
  • Type lookup: One store per component type with direct casting (no interfaces in hot loops).
  • File split (partial class):
    • EcsWorld.cs – entity lifecycle (spawn/despawn), frame management, counts, and entity lists
    • EcsWorld.Components.cs – component storage, CRUD, spans, and single-type query
    • EcsWorld.Queries.cs – multi-type queries and predicates
    • EcsWorld.RefIterators.cs – ref iterators, transforms, and parallel transforms

Behavior System (Attribute-based ECS)

Author gameplay in a script-like way:

  1. Mark a struct with [Behavior].
  2. Add methods and mark when they should run using attributes:
    • [OnStartup], [OnFirst], [OnPreUpdate], [OnUpdate], [OnPostUpdate], [OnRender], [OnLast].
  3. Optionally add filters on instance methods:
    • [With(typeof(Position), typeof(Velocity))]
    • [Without(typeof(Disabled))]
    • [Changed(typeof(Transform))]
  • Note: The current generator supports With joins of up to two component types. If more are specified, it falls back to querying only the behavior component and applies Without/Changed checks inside the loop.

Static vs Instance methods:

  • Static methods run once per stage invocation and receive BehaviorContext.
  • Instance methods run per entity that has this behavior component. They can use fields/properties on this. The generator:
    • Iterates ecs.Query<YourBehavior>()
    • Sets ctx.EntityID
    • Calls your method
    • Writes back the component with ecs.Update

Creating entities for instance behaviors:

  • Instance methods only run if at least one entity has that behavior. A common pattern is a static [OnStartup] to spawn and add the behavior component.

Access to engine services:

  • Use ctx.Res<T>() for other resources (e.g., Time, Input, etc.).
  • ctx.Ecs and ctx.Cmd provide ECS access.

Reference types inside behavior structs:

  • Safe and supported. Storing a class reference in your behavior struct allows complex per-entity state without copying large data. Initialize lazily or in [OnStartup] as needed.

Examples:

using ImGuiNET;

[Behavior]
public struct HUDOverlay
{
    [OnUpdate]
    public static void Draw(BehaviorContext ctx)
    {
        ImGui.Begin("HUD");
        ImGui.Text($"FPS: {(1.0 / ctx.Time.DeltaSeconds):0}");
        ImGui.End();
    }
}

[Behavior]
public struct Spawner
{
    public float a;
    private float b { get; set; }

    [OnStartup]
    public static void Init(BehaviorContext ctx)
    {
        var e = ctx.Ecs.Spawn();
        ctx.Ecs.Add(e, new Spawner { a = 1.0f });
    }

    [OnUpdate]
    public void Tick(BehaviorContext ctx)
    {
        b += (float)ctx.Time.DeltaSeconds;
        Console.WriteLine($"Spawner running. a={a}, b={b}");
    }
}

public class SomeDisposable : IDisposable
{
    private float _num = 2;

    public string Log() => _num.ToString();
    
    public void Dispose()
    {
        // Cleanup resources
    }
}

[Behavior]
public struct HeavyBehavior : IDisposable
{
    private SomeDisposable _handle;

    [OnStartup]
    public static void Init(BehaviorContext ctx)
    {
        var e = ctx.Ecs.Spawn();
        ctx.Ecs.Add(e, new HeavyBehavior { _handle = new SomeDisposable() });
    }

    [OnUpdate]
    public void Tick(BehaviorContext ctx)
    {
        Console.WriteLine(_handle.Log());
    }

    public void Dispose()
    {
        _handle?.Dispose();
        _handle = null;
    }
}

Native ECS Style (Manual Systems)

Prefer writing systems directly? Use App.AddSystem and operate on EcsWorld:

app.AddSystem(Stage.Update, (World w) =>
{
    var ecs = w.Resource<EcsWorld>();
    foreach (var (e, comp) in ecs.Query<MyComponent>())
    {
        // mutate comp and write back
        ecs.Update(e, comp);
    }
});

You can mix and match: the behavior generator emits systems under the hood; you can still register hand-written systems alongside them.

Source Generator

Engine.SourceGen scans for [Behavior] structs and methods with stage attributes, then emits:

  • Per-behavior static classes with stage entry points that call your methods (static or per-entity loops for instance methods).
  • A BehaviorsPlugin that registers those systems into the app.

DefaultPlugins includes BehaviorsPlugin, so behaviors are picked up automatically at build time—no manual registration required.

FAQ

  • Static vs Instance: Static methods run once per stage and are great for global logic/UI; instance methods run per entity and can use fields/properties on the component. structs.
  • Struct lifetimes and disposal: Structs are value types; they aren't “destroyed” with a finalizer. If your struct holds class references with unmanaged resources, implement IDisposable on the struct and dispose those references in Dispose(). The ECS will invoke Dispose() for components on Despawn.

Roadmap (high-level)

  • Core
    • Robust platform layer (windowing, input, timing) on SDL3
    • Vulkan renderer: swapchain, command submission, synchronization, VMA allocations
    • Shader pipeline: SPIR‑V compilation, reflection, hot‑reload
    • Asset pipeline: import (Assimp), packaging, and caching
    • Scene graph and serialization (USD integration)
    • ECS and job system
  • Tooling
    • Editor built with Avalonia UI (dockable panes, inspectors, scene view)
    • ImGui runtime overlay for debugging
    • Live reload for assets and scripts
  • Systems
    • Material system and PBR
    • Compute workloads (culling, particles, post‑processing)
    • Audio, physics, and networking (research and vendor selection TBD)
  • CI/DevX
    • Cross‑platform builds (Linux/Windows/macOS)
    • Automated formatting, linting, and basic tests

Items are aspirational and subject to change as the project evolves.

Contributing

Early days! If you want to help:

  • Try building/running on your platform and open issues for any rough edges.
  • Propose small, well‑scoped PRs (build scripts, docs, samples, or isolated subsystems).
  • Keep changes platform‑agnostic when possible.

By participating, you agree to abide by our Code of Conduct.

A formal guideline will be added once the editor and initial subsystems land.

License

Code: MIT license. See LICENSE for the full text.

Contributions: By submitting a contribution, you agree to license your contribution under the same license as this repository.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.1 272 11/16/2025
4.0.0 266 11/16/2025
3.1.0 360 10/30/2024
3.0.99 244 10/30/2024
3.0.98 248 10/30/2024
3.0.97 287 10/29/2024
3.0.96 286 10/29/2024
3.0.95 238 10/29/2024
3.0.94 239 10/29/2024
3.0.93 269 10/29/2024
3.0.92 248 10/29/2024
3.0.91 242 10/29/2024
3.0.90 249 10/29/2024
3.0.89 285 10/29/2024
3.0.88 252 10/29/2024
3.0.87 284 10/29/2024
3.0.86 250 10/29/2024
3.0.85 266 10/29/2024
3.0.84 233 10/29/2024
3.0.83 262 10/29/2024
3.0.82 242 10/27/2024
3.0.81 236 10/26/2024
3.0.80 272 10/26/2024
3.0.79 254 10/25/2024
3.0.78 260 10/24/2024
3.0.77 222 10/24/2024
3.0.76 251 10/24/2024
3.0.75 280 10/24/2024
3.0.74 283 10/23/2024
3.0.73 283 10/23/2024
3.0.72 278 10/23/2024
3.0.71 234 10/23/2024
3.0.70 231 10/21/2024
3.0.69 252 10/21/2024
3.0.68 262 10/21/2024
3.0.67 263 10/21/2024
3.0.66 220 10/21/2024
3.0.65 271 10/21/2024
3.0.64 234 10/21/2024
3.0.63 250 10/21/2024
3.0.62 267 10/21/2024
3.0.61 258 10/21/2024
3.0.60 239 10/21/2024
3.0.59 241 10/21/2024
3.0.58 216 10/21/2024
3.0.57 235 10/21/2024
3.0.56 247 10/21/2024
3.0.55 255 10/21/2024
3.0.54 278 10/19/2024
3.0.53 249 10/19/2024
3.0.52 313 10/19/2024
3.0.51 284 10/19/2024
3.0.50 324 10/18/2024
3.0.49 238 10/18/2024
3.0.48 255 10/17/2024
3.0.47 241 10/17/2024
3.0.46 211 10/17/2024
3.0.45 259 10/17/2024
3.0.44 251 10/17/2024
3.0.43 255 10/17/2024
3.0.42 251 10/17/2024
3.0.41 275 10/17/2024
3.0.40 270 10/17/2024
3.0.39 249 10/9/2024
3.0.38 253 10/9/2024
3.0.37 211 10/7/2024
3.0.36 221 10/7/2024
3.0.35 240 10/7/2024
3.0.34 230 10/3/2024
3.0.33 243 10/1/2024
3.0.32 269 10/1/2024
3.0.31 258 9/30/2024
3.0.30 236 9/30/2024
3.0.29 227 9/30/2024
3.0.28 234 9/28/2024
3.0.27 246 9/27/2024
3.0.26 284 9/26/2024
3.0.25 276 9/25/2024
3.0.24 226 9/25/2024
3.0.23 234 9/22/2024
3.0.21 243 9/19/2024
3.0.19 278 9/19/2024
3.0.18 241 9/18/2024
3.0.17 244 9/18/2024
3.0.16 277 9/18/2024
3.0.15 328 9/17/2024
3.0.14 241 9/17/2024
3.0.13 244 9/17/2024
3.0.12 292 9/15/2024
3.0.11 293 9/15/2024
3.0.10 249 9/15/2024
3.0.9 263 9/15/2024
3.0.8 256 9/15/2024
3.0.7 246 9/15/2024
3.0.6 245 9/15/2024
3.0.5 254 9/15/2024
3.0.4 267 9/15/2024
3.0.3 257 9/15/2024
3.0.2 264 9/11/2024
3.0.1 289 9/11/2024
3.0.0 267 9/8/2024
2.2.2 257 8/11/2024
2.0.2 434 3/7/2024
2.0.1 382 2/25/2024
2.0.0 432 2/25/2024
1.0.7 859 10/20/2023
1.0.6 320 10/20/2023
1.0.5 412 10/19/2023
1.0.4 369 10/18/2023
1.0.3 421 10/18/2023
1.0.2 395 10/18/2023
1.0.1 460 10/18/2023
1.0.0 411 10/13/2023