3DEngine.Core
1.0.1
dotnet add package 3DEngine.Core --version 1.0.1
NuGet\Install-Package 3DEngine.Core -Version 1.0.1
<PackageReference Include="3DEngine.Core" Version="1.0.1" />
<PackageVersion Include="3DEngine.Core" Version="1.0.1" />
<PackageReference Include="3DEngine.Core" />
paket add 3DEngine.Core --version 1.0.1
#r "nuget: 3DEngine.Core, 1.0.1"
#:package 3DEngine.Core@1.0.1
#addin nuget:?package=3DEngine.Core&version=1.0.1
#tool nuget:?package=3DEngine.Core&version=1.0.1
<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
- Design Goals
- Current Status
- Features
- Tech Stack
- Supported Platforms
- Build and Run
- Quick Start
- How It Works
- Roadmap (high-level)
- Contributing
- License
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
ECSWorldqueries andECSCommands.
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(seeEngine/Engine.csproj, currentlynet10.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}");
}
}
- Add the struct in any runtime project.
- Build: the source generator emits systems automatically.
- 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:First→PreUpdate→Update→PostUpdate→Render→Last.- Systems are
SystemFn(World world)delegates added to stages viaApp.AddSystem(stage, system)and executed bySchedule.
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())orworld.InsertResource(value)world.Resource<T>()to retrieve; throws if missingBehaviorContext.Res<T>()is a shortcut forworld.Resource<T>()
Common resources used by systems/behaviors:
EcsWorld– entity/component storage and queriesEcsCommands– queued mutations applied afterUpdate(atPostUpdate)AppWindow,Time,GUIRenderer, etc.
ECS: Entities, Components, and Commands
- Entities are
intIDs only (no public handle type). Create withvar id = ecs.Spawn();and remove withecs.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 inPostUpdate. - Disposal: On
Despawn, components that implementIDisposableare 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
Updateafter 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 callUpdate.
- Returns
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 aTransformEachwrites, or as you iterate viaIterateRef. Changed<T>(id)reads the bit; bits are cleared atBeginFrame()(stageFirst).
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 listsEcsWorld.Components.cs– component storage, CRUD, spans, and single-type queryEcsWorld.Queries.cs– multi-type queries and predicatesEcsWorld.RefIterators.cs– ref iterators, transforms, and parallel transforms
Behavior System (Attribute-based ECS)
Author gameplay in a script-like way:
- Mark a struct with
[Behavior]. - Add methods and mark when they should run using attributes:
[OnStartup],[OnFirst],[OnPreUpdate],[OnUpdate],[OnPostUpdate],[OnRender],[OnLast].
- Optionally add filters on instance methods:
[With(typeof(Position), typeof(Velocity))][Without(typeof(Disabled))][Changed(typeof(Transform))]
- Note: The current generator supports
Withjoins of up to two component types. If more are specified, it falls back to querying only the behavior component and appliesWithout/Changedchecks 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
- Iterates
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.Ecsandctx.Cmdprovide 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
BehaviorsPluginthat 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
IDisposableon the struct and dispose those references inDispose(). The ECS will invokeDispose()for components onDespawn.
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 | Versions 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. |
-
net10.0
- 3DEngine.App (>= 1.0.1)
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.