feat: Step 3 Flecs.NET ECS integration with animated triangle

- Add Transform component and ECS world in Engine.Core.
- TriangleRenderer.RenderWorld queries entities and applies transforms.
- Animate a single triangle entity in Program.cs.
- Pin ppy.SDL3-CS to 2026.520.0 and add linux-x64 RID for bundled native SDL3.
- Add Flecs.NET.Debug/Release 4.0.4-build.546 per configuration.
- Update SDL3 API usage for the 2026.520.0 binding.
- Update architecture file to Silk.NET.Vulkan and Flecs 4.0.4.
This commit is contained in:
emil28092005
2026-06-16 19:06:04 +03:00
parent b32469d42d
commit 608c3c24a8
10 changed files with 112 additions and 10 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ Cortex Engine is a 3D game engine built from scratch to provide a Unity-like dev
- **Read** the complete ECS world state through native JSON serialization.
- **Modify** the running engine via declarative JSON commands and, in Development Mode, via hot-reloaded C# scripts.
The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Vortice.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend.
The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Silk.NET.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend.
---
@@ -137,7 +137,7 @@ All Roslyn and `AssemblyLoadContext` code is wrapped in `#if DEV_MODE`.
**Flecs.NET**
- NuGet: `Flecs.NET.Release` 4.0.3
- NuGet: `Flecs.NET.Debug` (Debug) / `Flecs.NET.Release` (Release) 4.0.4-build.546
- High-level C# wrapper over the C-based Flecs ECS
- Supports .NET Standard 2.1, .NET 5/6/7/8
- NativeAOT-compatible via static linking: `<FlecsStaticLink>true</FlecsStaticLink>`
@@ -11,6 +11,7 @@
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>DEV_MODE</DefineConstants>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
+19 -3
View File
@@ -1,6 +1,9 @@
using System;
using System.Numerics;
using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics;
using Flecs.NET.Core;
namespace CortexEngine.App;
@@ -8,17 +11,21 @@ class Program
{
static void Main(string[] args)
{
Console.WriteLine("Cortex Engine Step 2 — Starting up...");
Console.WriteLine("Cortex Engine Step 3 — Starting up...");
try
{
using var window = new Sdl3Window("Cortex Engine — Step 2", 1280, 720);
using var world = World.Create();
using var window = new Sdl3Window("Cortex Engine — Step 3", 1280, 720);
var timing = new Timing();
var input = new InputMapping();
using var vulkan = new VulkanContext(window, enableValidation: false);
using var swapchain = new Swapchain(vulkan);
using var renderer = new TriangleRenderer(vulkan, swapchain);
var triangle = world.Entity("Triangle")
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f)));
var frames = 0;
var lastFpsTime = 0.0;
var lastWidth = window.Width;
@@ -39,7 +46,16 @@ class Program
swapchain.Recreate(lastWidth, lastHeight);
}
renderer.RenderFrame();
// Animate the entity transform.
ref var transform = ref triangle.Ensure<Transform>();
transform.Position = new Vector3(
MathF.Sin((float)timing.TotalTime) * 0.5f,
0.0f,
0.0f);
transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)timing.TotalTime);
transform.Scale = Vector3.One * (1.0f + MathF.Sin((float)timing.TotalTime * 2.0f) * 0.2f);
renderer.RenderWorld(world);
frames++;
if (timing.TotalTime - lastFpsTime >= 1.0)
+30
View File
@@ -0,0 +1,30 @@
using System.Numerics;
namespace Engine.Core.Components;
/// <summary>
/// Basic 3D transform component for the ECS.
/// Uses SIMD-friendly System.Numerics types.
/// </summary>
public record struct Transform
{
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public Transform(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null)
{
Position = position ?? Vector3.Zero;
Rotation = rotation ?? Quaternion.Identity;
Scale = scale ?? Vector3.One;
}
public static Transform Identity => new(Vector3.Zero, Quaternion.Identity, Vector3.One);
public Matrix4x4 GetMatrix()
{
return Matrix4x4.CreateScale(Scale)
* Matrix4x4.CreateFromQuaternion(Rotation)
* Matrix4x4.CreateTranslation(Position);
}
}
+2
View File
@@ -19,6 +19,8 @@
<ItemGroup>
<PackageReference Include="ppy.SDL3-CS" Version="2026.520.0" />
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release'" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -27,7 +27,7 @@ public sealed class InputMapping
public void ProcessEvent(SDL_Event evt)
{
switch ((SDL_EventType)evt.Type)
switch ((SDL_EventType)evt.type)
{
case SDL_EventType.SDL_EVENT_KEY_DOWN:
if (!_keysDown.Contains((SDL_Keycode)evt.key.key))
+1 -1
View File
@@ -50,7 +50,7 @@ public sealed unsafe class Sdl3Window : IDisposable
SDL_Event evt;
while (SDL3.SDL_PollEvent(&evt))
{
switch ((SDL_EventType)evt.Type)
switch ((SDL_EventType)evt.type)
{
case SDL_EventType.SDL_EVENT_QUIT:
ShouldClose = true;
@@ -20,6 +20,8 @@
<ItemGroup>
<PackageReference Include="Silk.NET.Vulkan" Version="2.21.0" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release'" />
</ItemGroup>
<ItemGroup>
+46 -3
View File
@@ -1,12 +1,15 @@
using System;
using System.Numerics;
using Flecs.NET.Core;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Engine.Core.Components;
namespace Engine.Graphics;
/// <summary>
/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline.
/// Uses Silk.NET.Vulkan.
/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world.
/// </summary>
public sealed unsafe class TriangleRenderer : IDisposable
{
@@ -115,7 +118,7 @@ public sealed unsafe class TriangleRenderer : IDisposable
}
}
public void RenderFrame()
public void RenderWorld(World world)
{
var frame = _currentFrame % 2;
@@ -160,7 +163,14 @@ public sealed unsafe class TriangleRenderer : IDisposable
var vertexBuffer = _vertexBuffer.Buffer;
var offset = 0ul;
_context.Vk.CmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
_context.Vk.CmdDraw(cmd, 3, 1, 0, 0);
var drawCmd = cmd;
world.Each((Entity e, ref Transform transform) =>
{
var bytes = BuildTriangleVertices(transform);
_vertexBuffer.Update(bytes);
_context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0);
});
_context.Vk.CmdEndRenderPass(cmd);
_context.Vk.EndCommandBuffer(cmd);
@@ -197,6 +207,39 @@ public sealed unsafe class TriangleRenderer : IDisposable
_currentFrame++;
}
private byte[] BuildTriangleVertices(Transform transform)
{
var matrix = transform.GetMatrix();
var positions = new Vector3[]
{
new Vector3(0.0f, -0.5f, 0.0f),
new Vector3(0.5f, 0.5f, 0.0f),
new Vector3(-0.5f, 0.5f, 0.0f)
};
var colors = new[]
{
new Vector3(1.0f, 0.0f, 0.0f),
new Vector3(0.0f, 1.0f, 0.0f),
new Vector3(0.0f, 0.0f, 1.0f)
};
var bytes = new byte[3 * 5 * sizeof(float)];
fixed (byte* p = bytes)
{
var dst = (float*)p;
for (var i = 0; i < 3; i++)
{
var transformed = Vector3.Transform(positions[i], matrix);
dst[i * 5 + 0] = transformed.X;
dst[i * 5 + 1] = transformed.Y;
dst[i * 5 + 2] = colors[i].X;
dst[i * 5 + 3] = colors[i].Y;
dst[i * 5 + 4] = colors[i].Z;
}
}
return bytes;
}
public void Dispose()
{
_context.Vk.DeviceWaitIdle(_context.Device);
+8
View File
@@ -102,6 +102,14 @@ public sealed unsafe class VertexBuffer : IDisposable
_context.Vk.UnmapMemory(_context.Device, Memory);
}
public void Update(ReadOnlySpan<byte> data)
{
if ((ulong)data.Length != Size)
throw new ArgumentException($"Vertex buffer update size mismatch: {data.Length} != {Size}");
CopyData(data);
}
public void Dispose()
{
_context.Vk.DeviceWaitIdle(_context.Device);