3DEngine.Core 1.0.1

dotnet add package 3DEngine.Core --version 1.0.1
                    
NuGet\Install-Package 3DEngine.Core -Version 1.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.Core" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="3DEngine.Core" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="3DEngine.Core" />
                    
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.Core --version 1.0.1
                    
#r "nuget: 3DEngine.Core, 1.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.Core@1.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.Core&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=3DEngine.Core&version=1.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 (3)

Showing the top 3 NuGet packages that depend on 3DEngine.Core:

Package Downloads
3DEngine

3D Game Engine - Vulkan - SDL3 - .NET 10 - C# 14

3DEngine.Window

3D Game Engine - Vulkan - SDL3 - .NET 10 - C# 14

3DEngine.ECS

3D Game Engine - Vulkan - SDL3 - .NET 10 - C# 14

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 334 11/16/2025
1.0.0 272 11/16/2025