Compare commits
No commits in common. "5594515a53fcc0142fc7b39f7d9c286ab3c0e357" and "339e850e10e609cfa195025680a04876c98b9a0e" have entirely different histories.
5594515a53
...
339e850e10
|
|
@ -15,7 +15,6 @@ x86/
|
||||||
!**/[Pp]ackages/build/
|
!**/[Pp]ackages/build/
|
||||||
*.nuget.props
|
*.nuget.props
|
||||||
*.nuget.targets
|
*.nuget.targets
|
||||||
nupkgs/
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vs/
|
.vs/
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<!--
|
|
||||||
Directory.Build.props for Godot C# projects consuming OECS.
|
|
||||||
Godot regenerates .csproj files on certain editor actions, which can
|
|
||||||
wipe custom <ItemGroup> entries. This file is never touched by Godot.
|
|
||||||
|
|
||||||
Placement: put this file in the root of your Godot project (next to
|
|
||||||
the .sln file), or in any parent directory.
|
|
||||||
|
|
||||||
You also need a nuget.config next to your .sln with the local feed:
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<packageSources>
|
|
||||||
<add key="local-oecs" value="path/to/oecs-sharp/nupkgs" />
|
|
||||||
</packageSources>
|
|
||||||
</configuration>
|
|
||||||
-->
|
|
||||||
<Project>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="OECS" Version="0.1.*" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
61
OECS.sln
61
OECS.sln
|
|
@ -1,84 +1,25 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.SourceGen", "src\OECS.SourceGen\OECS.SourceGen.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS", "src\OECS\OECS.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS", "src\OECS\OECS.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.Tests", "tests\OECS.Tests\OECS.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OECS.Tests", "tests\OECS.Tests\OECS.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blackjack", "examples\Blackjack\Blackjack.csproj", "{85631EDE-F984-4DA6-8EE9-0715AF1204E6}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Debug|x64 = Debug|x64
|
|
||||||
Debug|x86 = Debug|x86
|
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
Release|x64 = Release|x64
|
|
||||||
Release|x86 = Release|x86
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{C3D4E5F6-A7B8-9012-CDEF-123456789012} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
|
||||||
{85631EDE-F984-4DA6-8EE9-0715AF1204E6} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ namespace OECS;
|
||||||
public readonly struct Entity : IEquatable<Entity>
|
public readonly struct Entity : IEquatable<Entity>
|
||||||
{
|
{
|
||||||
public static Entity Null { get; } // ID=0, Version=0
|
public static Entity Null { get; } // ID=0, Version=0
|
||||||
public uint Id { get; } // lower 24-bit identifier
|
public uint Id { get; } // 24-bit identifier
|
||||||
public uint Version { get; } // upper 8-bit generation
|
public uint Version { get; } // 8-bit generation
|
||||||
public bool IsNull { get; } // true for Entity.Null
|
public bool IsNull { get; } // true for Entity.Null
|
||||||
|
|
||||||
public bool Equals(Entity other);
|
public bool Equals(Entity other);
|
||||||
|
|
@ -37,9 +37,6 @@ namespace OECS;
|
||||||
|
|
||||||
public class World : IDisposable
|
public class World : IDisposable
|
||||||
{
|
{
|
||||||
// --- Singleton entity ---
|
|
||||||
public static Entity SingletonEntity { get; } // ID=1, Version=1
|
|
||||||
|
|
||||||
// --- Lifecycle ---
|
// --- Lifecycle ---
|
||||||
public World();
|
public World();
|
||||||
|
|
||||||
|
|
@ -52,14 +49,11 @@ public class World : IDisposable
|
||||||
public void AddComponent<T>(Entity entity, T component) where T : struct;
|
public void AddComponent<T>(Entity entity, T component) where T : struct;
|
||||||
public void RemoveComponent<T>(Entity entity) where T : struct;
|
public void RemoveComponent<T>(Entity entity) where T : struct;
|
||||||
public ref T GetComponent<T>(Entity entity) where T : struct;
|
public ref T GetComponent<T>(Entity entity) where T : struct;
|
||||||
public T ReadComponent<T>(Entity entity) where T : struct;
|
|
||||||
public bool TryGetComponent<T>(Entity entity, out T value) where T : struct;
|
|
||||||
public bool HasComponent<T>(Entity entity) where T : struct;
|
public bool HasComponent<T>(Entity entity) where T : struct;
|
||||||
|
|
||||||
// --- Singleton ---
|
// --- Singleton ---
|
||||||
public void SetSingleton<T>(T component) where T : struct;
|
public void SetSingleton<T>(T component) where T : struct;
|
||||||
public ref T GetSingleton<T>() where T : struct;
|
public ref T GetSingleton<T>() where T : struct;
|
||||||
public T ReadSingleton<T>() where T : struct;
|
|
||||||
public bool HasSingleton<T>() where T : struct;
|
public bool HasSingleton<T>() where T : struct;
|
||||||
public void RemoveSingleton<T>() where T : struct;
|
public void RemoveSingleton<T>() where T : struct;
|
||||||
|
|
||||||
|
|
@ -69,19 +63,12 @@ public class World : IDisposable
|
||||||
// --- Query Execution ---
|
// --- Query Execution ---
|
||||||
public void ForEach<T1>(
|
public void ForEach<T1>(
|
||||||
QueryDescriptor query,
|
QueryDescriptor query,
|
||||||
ForEachAction<T1> action) where T1 : struct;
|
Action<Entity, ref T1> action) where T1 : struct;
|
||||||
|
|
||||||
// Overloads for 2–6 component types:
|
// Overloads for 2–6 component types:
|
||||||
public void ForEach<T1, T2>(QueryDescriptor, ForEachAction<T1, T2>)
|
public void ForEach<T1, T2>(QueryDescriptor, Action<Entity, ref T1, ref T2>)
|
||||||
where T1 : struct where T2 : struct;
|
where T1 : struct where T2 : struct;
|
||||||
public void ForEach<T1, T2, T3>(QueryDescriptor, ForEachAction<T1, T2, T3>)
|
// ... up to T6
|
||||||
where T1 : struct where T2 : struct where T3 : struct;
|
|
||||||
public void ForEach<T1, T2, T3, T4>(QueryDescriptor, ForEachAction<T1, T2, T3, T4>)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
|
||||||
public void ForEach<T1, T2, T3, T4, T5>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5>)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
|
||||||
public void ForEach<T1, T2, T3, T4, T5, T6>(QueryDescriptor, ForEachAction<T1, T2, T3, T4, T5, T6>)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
|
||||||
|
|
||||||
// --- Commands ---
|
// --- Commands ---
|
||||||
public CommandQueue Commands { get; }
|
public CommandQueue Commands { get; }
|
||||||
|
|
@ -91,10 +78,10 @@ public class World : IDisposable
|
||||||
public void MarkModified<T>(Entity entity) where T : struct;
|
public void MarkModified<T>(Entity entity) where T : struct;
|
||||||
public void PostChanges();
|
public void PostChanges();
|
||||||
|
|
||||||
public Observable<EntityChange> ObserveEntityChanges();
|
public IObservable<EntityChange> ObserveEntityChanges();
|
||||||
public Observable<EntityChange> ObserveComponentChanges<T>()
|
public IObservable<EntityChange> ObserveComponentChanges<T>()
|
||||||
where T : struct;
|
where T : struct;
|
||||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query);
|
public IObservable<EntityChange> ObserveQuery(QueryDescriptor query);
|
||||||
|
|
||||||
// --- Relationships ---
|
// --- Relationships ---
|
||||||
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
public IReadOnlyCollection<Entity> GetSources<T>(Entity target)
|
||||||
|
|
@ -107,70 +94,6 @@ public class World : IDisposable
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ForEachAction Delegates
|
|
||||||
|
|
||||||
Custom delegate types for query iteration with `ref` parameters. The built-in
|
|
||||||
`Action<...>` delegates do not support `ref` parameters.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1>(Entity entity, ref T1 c1) where T1 : struct;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1, T2>(Entity entity, ref T1 c1, ref T2 c2)
|
|
||||||
where T1 : struct where T2 : struct;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1, T2, T3>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1, T2, T3, T4>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1, T2, T3, T4, T5>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct;
|
|
||||||
|
|
||||||
public delegate void ForEachAction<T1, T2, T3, T4, T5, T6>(Entity entity, ref T1 c1, ref T2 c2, ref T3 c3, ref T4 c4, ref T5 c5, ref T6 c6)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct where T4 : struct where T5 : struct where T6 : struct;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EntityIterator
|
|
||||||
|
|
||||||
Extension methods on `World` for `foreach`-style iteration over queries.
|
|
||||||
Returns `ref struct` iterators that support zero-allocation iteration with
|
|
||||||
`ref` access to components. Supports 1–3 component types.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
public static class EntityIterator
|
|
||||||
{
|
|
||||||
public static Select1<T1> Select<T1>(
|
|
||||||
this World world, QueryDescriptor query) where T1 : struct;
|
|
||||||
|
|
||||||
public static Select2<T1, T2> Select<T1, T2>(
|
|
||||||
this World world, QueryDescriptor query)
|
|
||||||
where T1 : struct where T2 : struct;
|
|
||||||
|
|
||||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
|
||||||
this World world, QueryDescriptor query)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using var iter = world.Select<Position, Velocity>(query);
|
|
||||||
foreach (ref var item in iter)
|
|
||||||
{
|
|
||||||
item.Current1.X += item.Current2.X * dt;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## QueryBuilder
|
## QueryBuilder
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -257,7 +180,6 @@ namespace OECS;
|
||||||
|
|
||||||
public class SystemGroup
|
public class SystemGroup
|
||||||
{
|
{
|
||||||
public SystemGroup(World world);
|
|
||||||
public void Add(ISystem system);
|
public void Add(ISystem system);
|
||||||
public void Remove(ISystem system);
|
public void Remove(ISystem system);
|
||||||
public void RunTimed(float deltaTime);
|
public void RunTimed(float deltaTime);
|
||||||
|
|
@ -290,8 +212,6 @@ public class CommandQueue
|
||||||
{
|
{
|
||||||
public void Enqueue<T>(T command) where T : struct, ICommand;
|
public void Enqueue<T>(T command) where T : struct, ICommand;
|
||||||
public void ExecuteAll(World world);
|
public void ExecuteAll(World world);
|
||||||
public void Clear();
|
|
||||||
public void ClearErrors();
|
|
||||||
public int Count { get; }
|
public int Count { get; }
|
||||||
public IReadOnlyList<Exception> Errors { get; }
|
public IReadOnlyList<Exception> Errors { get; }
|
||||||
}
|
}
|
||||||
|
|
@ -313,31 +233,12 @@ public interface IRelationship
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Relationship\<TSelf, TTarget\>
|
|
||||||
|
|
||||||
A convenience base struct for relationship components. The type parameters are
|
|
||||||
phantom types that differentiate relationship kinds at the type level, enabling
|
|
||||||
type-safe queries and reverse lookups.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct Relationship<TSelf, TTarget> : IRelationship
|
|
||||||
{
|
|
||||||
[Key(0)] public Entity Source { get; set; }
|
|
||||||
[Key(1)] public Entity Target { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EntityChange
|
## EntityChange
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
namespace OECS;
|
namespace OECS;
|
||||||
|
|
||||||
public readonly struct EntityChange : IEquatable<EntityChange>
|
public readonly struct EntityChange
|
||||||
{
|
{
|
||||||
public Entity Entity { get; }
|
public Entity Entity { get; }
|
||||||
public ChangeKind Kind { get; }
|
public ChangeKind Kind { get; }
|
||||||
|
|
@ -356,56 +257,6 @@ public enum ChangeKind
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## WorldSerializer
|
|
||||||
|
|
||||||
Saves and loads a `World` to/from a MessagePack stream. Components are
|
|
||||||
serialized by their runtime type.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
public static class WorldSerializer
|
|
||||||
{
|
|
||||||
public static void Save(World world, Stream stream);
|
|
||||||
public static void Load(World world, Stream stream);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## WorldSnapshot / EntitySnapshot / ComponentEntry
|
|
||||||
|
|
||||||
Serialization types used by `WorldSerializer`. These are public but intended
|
|
||||||
primarily for serialization infrastructure.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public class WorldSnapshot
|
|
||||||
{
|
|
||||||
[Key(0)] public EntitySnapshot[] Entities { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public class EntitySnapshot
|
|
||||||
{
|
|
||||||
[Key(0)] public uint Id { get; set; }
|
|
||||||
[Key(1)] public uint Version { get; set; }
|
|
||||||
[Key(2)] public ComponentEntry[] Components { get; set; }
|
|
||||||
public Entity Entity { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public class ComponentEntry
|
|
||||||
{
|
|
||||||
[Key(0)] public string TypeName { get; set; }
|
|
||||||
[Key(1)] public byte[] Data { get; set; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -418,19 +269,13 @@ public struct Position : IMessagePackSerializationCallbackReceiver
|
||||||
{
|
{
|
||||||
[Key(0)] public float X;
|
[Key(0)] public float X;
|
||||||
[Key(1)] public float Y;
|
[Key(1)] public float Y;
|
||||||
|
|
||||||
public void OnBeforeSerialize() { }
|
|
||||||
public void OnAfterDeserialize() { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public struct Velocity : IMessagePackSerializationCallbackReceiver
|
public struct Velocity
|
||||||
{
|
{
|
||||||
[Key(0)] public float X;
|
[Key(0)] public float X;
|
||||||
[Key(1)] public float Y;
|
[Key(1)] public float Y;
|
||||||
|
|
||||||
public void OnBeforeSerialize() { }
|
|
||||||
public void OnAfterDeserialize() { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a system
|
// Define a system
|
||||||
|
|
@ -446,8 +291,6 @@ public class MovementSystem : ITickedSystem
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world) => Run(world, Tick.Logical());
|
|
||||||
|
|
||||||
public void Run(World world, Tick tick)
|
public void Run(World world, Tick tick)
|
||||||
{
|
{
|
||||||
float dt = tick.DeltaTime;
|
float dt = tick.DeltaTime;
|
||||||
|
|
@ -463,7 +306,7 @@ public class MovementSystem : ITickedSystem
|
||||||
|
|
||||||
// Wire it up
|
// Wire it up
|
||||||
var world = new World();
|
var world = new World();
|
||||||
var group = new SystemGroup(world);
|
var group = new SystemGroup();
|
||||||
group.Add(new MovementSystem(world));
|
group.Add(new MovementSystem(world));
|
||||||
|
|
||||||
// Observe changes
|
// Observe changes
|
||||||
|
|
@ -491,7 +334,5 @@ These types are implementation details and may change without notice:
|
||||||
| `SparseSet<T>` | Dense/sparse array pair for component storage. |
|
| `SparseSet<T>` | Dense/sparse array pair for component storage. |
|
||||||
| `ComponentStore` | Registry of `SparseSet<T>` instances by type. |
|
| `ComponentStore` | Registry of `SparseSet<T>` instances by type. |
|
||||||
| `ChangeBuffer` | Accumulates `EntityChange` during system run, posts to R3 subjects. |
|
| `ChangeBuffer` | Accumulates `EntityChange` during system run, posts to R3 subjects. |
|
||||||
| `ChangeSet` | Deduplicated change accumulator within a single batch. |
|
|
||||||
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
| `RelationshipIndex` | Reverse lookup from target entity to source entities. |
|
||||||
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
| `EntityAllocator` | Free-list + bump allocator for entity IDs. |
|
||||||
| `QueryExecutor` | Query iteration logic with smallest-set driver optimization. |
|
|
||||||
|
|
@ -45,8 +45,8 @@ signature) and sparse sets (one dense array per component type).
|
||||||
**Context:** Entities need to be cheap to copy, comparable, and safe against
|
**Context:** Entities need to be cheap to copy, comparable, and safe against
|
||||||
use-after-free (accessing a recycled entity ID).
|
use-after-free (accessing a recycled entity ID).
|
||||||
|
|
||||||
**Decision:** `readonly struct Entity` wrapping a `uint`. Lower 24 bits are the
|
**Decision:** `readonly struct Entity` wrapping a `uint`. Upper 24 bits are the
|
||||||
ID, upper 8 bits are the version.
|
ID, lower 8 bits are the version.
|
||||||
|
|
||||||
**Rationale:**
|
**Rationale:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<RootNamespace>Blackjack</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
|
||||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Player hits: draws one card from the deck to the player's hand.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct HitCommand : ICommand
|
|
||||||
{
|
|
||||||
public void Execute(World world)
|
|
||||||
{
|
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
if (state.Phase != GamePhase.PlayerTurn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
DrawCard<PlayerHand>(world);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draws the top card from the deck and adds it to the given hand.
|
|
||||||
/// </summary>
|
|
||||||
internal static void DrawCard<THand>(World world)
|
|
||||||
where THand : struct
|
|
||||||
{
|
|
||||||
var singletonEntity = World.SingletonEntity;
|
|
||||||
var deckEntity = FindEntity<Deck>(world, singletonEntity);
|
|
||||||
var handEntity = FindEntity<THand>(world, singletonEntity);
|
|
||||||
|
|
||||||
if (deckEntity == Entity.Null || handEntity == Entity.Null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Find a card still in the deck.
|
|
||||||
var cardsInDeck = world.GetSources<InDeck>(deckEntity);
|
|
||||||
if (cardsInDeck.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var cardEntity = cardsInDeck.First();
|
|
||||||
|
|
||||||
// Remove from deck, add to hand.
|
|
||||||
world.RemoveComponent<InDeck>(cardEntity);
|
|
||||||
world.AddComponent(cardEntity, new Holds { Source = cardEntity, Target = handEntity });
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Entity FindEntity<T>(World world, Entity singletonEntity)
|
|
||||||
where T : struct
|
|
||||||
{
|
|
||||||
using var iter = world.Select<T>();
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
if (iter.CurrentEntity != singletonEntity)
|
|
||||||
return iter.CurrentEntity;
|
|
||||||
}
|
|
||||||
return Entity.Null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Starts a new round after the previous one ended.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct NewRoundCommand : ICommand
|
|
||||||
{
|
|
||||||
public void Execute(World world)
|
|
||||||
{
|
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
if (state.Phase != GamePhase.RoundOver)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Clear hands from previous round.
|
|
||||||
var singletonEntity = World.SingletonEntity;
|
|
||||||
var handEntities = new List<Entity>();
|
|
||||||
using (var iter = world.Select<PlayerHand>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
if (iter.CurrentEntity != singletonEntity)
|
|
||||||
handEntities.Add(iter.CurrentEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
using (var iter = world.Select<DealerHand>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
if (iter.CurrentEntity != singletonEntity)
|
|
||||||
handEntities.Add(iter.CurrentEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var hand in handEntities)
|
|
||||||
{
|
|
||||||
var cards = world.GetSources<Holds>(hand);
|
|
||||||
var deckEntity = HitCommand.FindEntity<Deck>(world, singletonEntity);
|
|
||||||
foreach (var card in cards)
|
|
||||||
{
|
|
||||||
world.RemoveComponent<Holds>(card);
|
|
||||||
world.AddComponent(card, new InDeck { Source = card, Target = deckEntity });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Phase = GamePhase.Betting;
|
|
||||||
state.Result = RoundResult.None;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Places a bet and advances the game to the dealing phase.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct PlaceBetCommand : ICommand
|
|
||||||
{
|
|
||||||
[Key(0)] public int Amount;
|
|
||||||
|
|
||||||
public void Execute(World world)
|
|
||||||
{
|
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
if (state.Phase != GamePhase.Betting)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (Amount < 1 || Amount > state.Chips)
|
|
||||||
return;
|
|
||||||
|
|
||||||
state.CurrentBet = Amount;
|
|
||||||
state.Chips -= Amount;
|
|
||||||
state.Phase = GamePhase.Dealing;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Player stands: advance to the dealer's turn.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct StandCommand : ICommand
|
|
||||||
{
|
|
||||||
public void Execute(World world)
|
|
||||||
{
|
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
if (state.Phase != GamePhase.PlayerTurn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
state.Phase = GamePhase.DealerTurn;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A playing card with a suit and rank.
|
|
||||||
/// One entity per card.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct Card
|
|
||||||
{
|
|
||||||
[Key(0)] public Suit Suit;
|
|
||||||
[Key(1)] public Rank Rank;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tag component marking the dealer's hand entity.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct DealerHand { }
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tag component marking the deck entity.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct Deck { }
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Relationship from a hand entity to a card entity,
|
|
||||||
/// representing that the hand holds this card.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct Holds : IRelationship
|
|
||||||
{
|
|
||||||
[Key(0)] public Entity Source { get; set; }
|
|
||||||
[Key(1)] public Entity Target { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Relationship from a card entity to the deck entity,
|
|
||||||
/// representing that the card is in the deck.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct InDeck : IRelationship
|
|
||||||
{
|
|
||||||
[Key(0)] public Entity Source { get; set; }
|
|
||||||
[Key(1)] public Entity Target { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tag component marking the player's hand entity.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct PlayerHand { }
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
public enum Rank : byte
|
|
||||||
{
|
|
||||||
Ace = 1,
|
|
||||||
Two = 2,
|
|
||||||
Three = 3,
|
|
||||||
Four = 4,
|
|
||||||
Five = 5,
|
|
||||||
Six = 6,
|
|
||||||
Seven = 7,
|
|
||||||
Eight = 8,
|
|
||||||
Nine = 9,
|
|
||||||
Ten = 10,
|
|
||||||
Jack = 11,
|
|
||||||
Queen = 12,
|
|
||||||
King = 13
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
public enum Suit : byte
|
|
||||||
{
|
|
||||||
Clubs = 0,
|
|
||||||
Diamonds = 1,
|
|
||||||
Hearts = 2,
|
|
||||||
Spades = 3
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Mulberry32 PRNG — a fast, high-quality 32-bit random number generator.
|
|
||||||
/// Seed is stored in the ECS GameState singleton.
|
|
||||||
/// </summary>
|
|
||||||
public static class Mulberry32
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Advances the state and returns a random float in [0, 1).
|
|
||||||
/// </summary>
|
|
||||||
public static float NextFloat(ref uint state)
|
|
||||||
{
|
|
||||||
state += 0x6D2B79F5u;
|
|
||||||
uint z = state;
|
|
||||||
z = (z ^ (z >> 15)) * (z | 1u);
|
|
||||||
z ^= z + (z ^ (z >> 7)) * (z | 61u);
|
|
||||||
return (z ^ (z >> 14)) / (float)uint.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Advances the state and returns a random integer in [min, max].
|
|
||||||
/// </summary>
|
|
||||||
public static int NextInt(ref uint state, int min, int max)
|
|
||||||
{
|
|
||||||
return (int)(NextFloat(ref state) * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
public static class Program
|
|
||||||
{
|
|
||||||
public static void Main()
|
|
||||||
{
|
|
||||||
var world = new World();
|
|
||||||
|
|
||||||
// ── Initialize game state ─────────────────────────────────────
|
|
||||||
|
|
||||||
// Use a seed based on current time, or a fixed seed for reproducibility.
|
|
||||||
uint seed = (uint)Environment.TickCount;
|
|
||||||
// Uncomment the next line for a deterministic game:
|
|
||||||
// seed = 12345u;
|
|
||||||
|
|
||||||
world.SetSingleton(new GameState
|
|
||||||
{
|
|
||||||
Phase = GamePhase.Betting,
|
|
||||||
Result = RoundResult.None,
|
|
||||||
Chips = 100,
|
|
||||||
CurrentBet = 0,
|
|
||||||
RoundNumber = 1,
|
|
||||||
Seed = seed
|
|
||||||
});
|
|
||||||
world.PostChanges();
|
|
||||||
|
|
||||||
// ── Wire up systems ───────────────────────────────────────────
|
|
||||||
|
|
||||||
var group = new SystemGroup(world);
|
|
||||||
group.Add(new DeckSetupSystem());
|
|
||||||
group.Add(new DealSystem());
|
|
||||||
group.Add(new PlayerBustCheckSystem());
|
|
||||||
group.Add(new DealerSystem());
|
|
||||||
group.Add(new RenderSystem());
|
|
||||||
|
|
||||||
// ── Game loop ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
group.RunLogical();
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
|
|
||||||
// Check for game over (out of chips).
|
|
||||||
if (state.Chips <= 0 && state.Phase == GamePhase.Betting)
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" You're out of chips! Game over.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state.Phase)
|
|
||||||
{
|
|
||||||
case GamePhase.Betting:
|
|
||||||
var input = Console.ReadLine();
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
continue;
|
|
||||||
if (input.Trim().ToLower() == "quit")
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (int.TryParse(input.Trim(), out var bet))
|
|
||||||
{
|
|
||||||
world.Commands.Enqueue(new PlaceBetCommand { Amount = bet });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine(" Invalid bet. Enter a number.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GamePhase.PlayerTurn:
|
|
||||||
var choice = Console.ReadLine();
|
|
||||||
if (string.IsNullOrWhiteSpace(choice))
|
|
||||||
continue;
|
|
||||||
if (choice.Trim().ToLower() == "quit")
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (choice.Trim().ToLower())
|
|
||||||
{
|
|
||||||
case "h":
|
|
||||||
case "hit":
|
|
||||||
world.Commands.Enqueue(new HitCommand());
|
|
||||||
break;
|
|
||||||
case "s":
|
|
||||||
case "stand":
|
|
||||||
world.Commands.Enqueue(new StandCommand());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Console.WriteLine(" Invalid choice. Enter H or S.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GamePhase.RoundOver:
|
|
||||||
Console.ReadLine();
|
|
||||||
ref var roundState = ref world.GetSingleton<GameState>();
|
|
||||||
roundState.RoundNumber++;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
world.PostChanges();
|
|
||||||
world.Commands.Enqueue(new NewRoundCommand());
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Dealing, DealerTurn — systems handle these automatically.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
group.RunLogical();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
public enum GamePhase : byte
|
|
||||||
{
|
|
||||||
Betting = 0,
|
|
||||||
Dealing = 1,
|
|
||||||
PlayerTurn = 2,
|
|
||||||
DealerTurn = 3,
|
|
||||||
RoundOver = 4
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Global game state stored on the singleton entity.
|
|
||||||
/// </summary>
|
|
||||||
[MessagePackObject]
|
|
||||||
public struct GameState
|
|
||||||
{
|
|
||||||
[Key(0)] public GamePhase Phase;
|
|
||||||
[Key(1)] public RoundResult Result;
|
|
||||||
[Key(2)] public int Chips;
|
|
||||||
[Key(3)] public int CurrentBet;
|
|
||||||
[Key(4)] public int RoundNumber;
|
|
||||||
[Key(5)] public uint Seed;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
public enum RoundResult : byte
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
PlayerBust = 1,
|
|
||||||
DealerBust = 2,
|
|
||||||
PlayerWin = 3,
|
|
||||||
DealerWin = 4,
|
|
||||||
Push = 5,
|
|
||||||
Blackjack = 6
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deals initial two cards to player and dealer when entering the dealing phase.
|
|
||||||
/// Advances to player turn after dealing.
|
|
||||||
/// </summary>
|
|
||||||
public class DealSystem : ISystem
|
|
||||||
{
|
|
||||||
public void Run(World world)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
if (state.Phase != GamePhase.Dealing)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
// Deal two cards to player, then two to dealer.
|
|
||||||
HitCommand.DrawCard<PlayerHand>(world);
|
|
||||||
HitCommand.DrawCard<PlayerHand>(world);
|
|
||||||
HitCommand.DrawCard<DealerHand>(world);
|
|
||||||
HitCommand.DrawCard<DealerHand>(world);
|
|
||||||
|
|
||||||
mutableState.Phase = GamePhase.PlayerTurn;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class PlayerHandTag { public static readonly PlayerHand Instance = new(); }
|
|
||||||
internal static class DealerHandTag { public static readonly DealerHand Instance = new(); }
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dealer draws cards until reaching 17 or higher,
|
|
||||||
/// then resolves the round result.
|
|
||||||
/// </summary>
|
|
||||||
public class DealerSystem : ISystem
|
|
||||||
{
|
|
||||||
public void Run(World world)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
if (state.Phase != GamePhase.DealerTurn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
|
||||||
|
|
||||||
// Dealer must hit on 16 and below, stand on 17+.
|
|
||||||
int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
|
||||||
while (dealerTotal < 17)
|
|
||||||
{
|
|
||||||
HitCommand.DrawCard<DealerHand>(world);
|
|
||||||
dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
int playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
|
||||||
|
|
||||||
mutableState.Phase = GamePhase.RoundOver;
|
|
||||||
|
|
||||||
if (dealerTotal > 21)
|
|
||||||
{
|
|
||||||
mutableState.Result = RoundResult.DealerBust;
|
|
||||||
mutableState.Chips += mutableState.CurrentBet * 2;
|
|
||||||
}
|
|
||||||
else if (playerTotal > dealerTotal)
|
|
||||||
{
|
|
||||||
mutableState.Result = RoundResult.PlayerWin;
|
|
||||||
mutableState.Chips += mutableState.CurrentBet * 2;
|
|
||||||
}
|
|
||||||
else if (dealerTotal > playerTotal)
|
|
||||||
{
|
|
||||||
mutableState.Result = RoundResult.DealerWin;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Push: return the bet.
|
|
||||||
mutableState.Result = RoundResult.Push;
|
|
||||||
mutableState.Chips += mutableState.CurrentBet;
|
|
||||||
}
|
|
||||||
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the deck (52 cards + deck/hand entities) and shuffles
|
|
||||||
/// using mulberry32 with the seed from the GameState singleton.
|
|
||||||
/// Runs once at the start of each round (during Dealing phase).
|
|
||||||
/// </summary>
|
|
||||||
public class DeckSetupSystem : ISystem
|
|
||||||
{
|
|
||||||
public void Run(World world)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
if (state.Phase != GamePhase.Dealing)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Only create deck entities if they don't exist yet.
|
|
||||||
var singletonEntity = World.SingletonEntity;
|
|
||||||
bool hasDeck = false;
|
|
||||||
using (var iter = world.Select<Deck>())
|
|
||||||
{
|
|
||||||
hasDeck = iter.MoveNext() && iter.CurrentEntity != singletonEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasDeck)
|
|
||||||
{
|
|
||||||
// Create deck entity.
|
|
||||||
var deckEntity = world.CreateEntity();
|
|
||||||
world.AddComponent(deckEntity, new Deck());
|
|
||||||
|
|
||||||
// Create all 52 cards.
|
|
||||||
foreach (Suit suit in Enum.GetValues<Suit>())
|
|
||||||
{
|
|
||||||
foreach (Rank rank in Enum.GetValues<Rank>())
|
|
||||||
{
|
|
||||||
var cardEntity = world.CreateEntity();
|
|
||||||
world.AddComponent(cardEntity, new Card { Suit = suit, Rank = rank });
|
|
||||||
world.AddComponent(cardEntity, new InDeck { Source = cardEntity, Target = deckEntity });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create hand entities.
|
|
||||||
var playerHand = world.CreateEntity();
|
|
||||||
world.AddComponent(playerHand, new PlayerHand());
|
|
||||||
|
|
||||||
var dealerHand = world.CreateEntity();
|
|
||||||
world.AddComponent(dealerHand, new DealerHand());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle the deck using mulberry32 with the current seed.
|
|
||||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
|
||||||
var deckEntity2 = HitCommand.FindEntity<Deck>(world, singletonEntity);
|
|
||||||
var cards = world.GetSources<InDeck>(deckEntity2).ToArray();
|
|
||||||
Shuffle(world, deckEntity2, cards, ref mutableState.Seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Shuffle(World world, Entity deckEntity, Entity[] cardEntities, ref uint seed)
|
|
||||||
{
|
|
||||||
// Fisher-Yates shuffle using mulberry32.
|
|
||||||
for (int i = cardEntities.Length - 1; i > 0; i--)
|
|
||||||
{
|
|
||||||
int j = Mulberry32.NextInt(ref seed, 0, i);
|
|
||||||
|
|
||||||
// Swap the InDeck relationship targets (cards are always in the deck,
|
|
||||||
// so there's nothing to swap except the cards themselves — but we
|
|
||||||
// shuffle the card order conceptually by removing and re-adding
|
|
||||||
// InDeck components in shuffled order). Actually, since InDeck
|
|
||||||
// is just a tag, we just re-shuffle the order in the source collection.
|
|
||||||
// The simplest approach: no need to swap component data; we just
|
|
||||||
// need to ensure the cards are iterated in shuffled order.
|
|
||||||
// We'll swap the card entities in the array.
|
|
||||||
(cardEntities[i], cardEntities[j]) = (cardEntities[j], cardEntities[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now remove all InDeck and re-add in shuffled order so GetSources
|
|
||||||
// returns them in shuffled order.
|
|
||||||
for (int i = cardEntities.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
world.RemoveComponent<InDeck>(cardEntities[i]);
|
|
||||||
}
|
|
||||||
for (int i = 0; i < cardEntities.Length; i++)
|
|
||||||
{
|
|
||||||
world.AddComponent(cardEntities[i], new InDeck { Source = cardEntities[i], Target = deckEntity });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper to calculate the blackjack value of a hand.
|
|
||||||
/// Aces count as 11 unless that would bust, then they count as 1.
|
|
||||||
/// </summary>
|
|
||||||
public static class HandUtil
|
|
||||||
{
|
|
||||||
public static int CalculateHand(World world, DealerHand hand)
|
|
||||||
{
|
|
||||||
return CalculateHandImpl(world, hand);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int CalculateHand(World world, PlayerHand hand)
|
|
||||||
{
|
|
||||||
return CalculateHandImpl(world, hand);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CalculateHandImpl<T>(World world, T handTag)
|
|
||||||
where T : struct
|
|
||||||
{
|
|
||||||
// Find the hand entity.
|
|
||||||
Entity handEntity = Entity.Null;
|
|
||||||
using (var iter = world.Select<T>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
if (iter.CurrentEntity != World.SingletonEntity)
|
|
||||||
{
|
|
||||||
handEntity = iter.CurrentEntity;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handEntity == Entity.Null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var cards = world.GetSources<Holds>(handEntity);
|
|
||||||
int total = 0;
|
|
||||||
int aceCount = 0;
|
|
||||||
|
|
||||||
foreach (var cardEntity in cards)
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(cardEntity);
|
|
||||||
int value = card.Rank switch
|
|
||||||
{
|
|
||||||
Rank.Ace => 11,
|
|
||||||
Rank.Jack => 10,
|
|
||||||
Rank.Queen => 10,
|
|
||||||
Rank.King => 10,
|
|
||||||
_ => (int)card.Rank
|
|
||||||
};
|
|
||||||
|
|
||||||
if (card.Rank == Rank.Ace)
|
|
||||||
aceCount++;
|
|
||||||
|
|
||||||
total += value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Downgrade aces from 11 to 1 as needed.
|
|
||||||
while (total > 21 && aceCount > 0)
|
|
||||||
{
|
|
||||||
total -= 10;
|
|
||||||
aceCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluates the player's hand total after each hit.
|
|
||||||
/// Busts the player if they exceed 21.
|
|
||||||
/// </summary>
|
|
||||||
public class PlayerBustCheckSystem : ISystem
|
|
||||||
{
|
|
||||||
public void Run(World world)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
if (state.Phase != GamePhase.PlayerTurn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var total = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
|
||||||
if (total <= 21)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ref var mutableState = ref world.GetSingleton<GameState>();
|
|
||||||
mutableState.Phase = GamePhase.RoundOver;
|
|
||||||
mutableState.Result = RoundResult.PlayerBust;
|
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
using OECS;
|
|
||||||
|
|
||||||
namespace Blackjack;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Renders the current game state to the console.
|
|
||||||
/// </summary>
|
|
||||||
public class RenderSystem : ISystem
|
|
||||||
{
|
|
||||||
public void Run(World world)
|
|
||||||
{
|
|
||||||
var state = world.ReadSingleton<GameState>();
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" Blackjack");
|
|
||||||
Console.WriteLine(" ═════════");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine($" Chips: {state.Chips} | Round: {state.RoundNumber}");
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
// Show dealer's hand.
|
|
||||||
var dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
|
|
||||||
Console.Write(" Dealer: ");
|
|
||||||
if (state.Phase == GamePhase.PlayerTurn)
|
|
||||||
{
|
|
||||||
// Hide first card (hole card) during player's turn.
|
|
||||||
Console.Write("?? ");
|
|
||||||
PrintHand(world, DealerHandTag.Instance, skipFirst: true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PrintHand(world, DealerHandTag.Instance);
|
|
||||||
Console.Write($" ({dealerTotal})");
|
|
||||||
}
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
// Show player's hand.
|
|
||||||
var playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
|
|
||||||
Console.Write(" Player: ");
|
|
||||||
PrintHand(world, PlayerHandTag.Instance);
|
|
||||||
Console.Write($" ({playerTotal})");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
// Status line.
|
|
||||||
switch (state.Phase)
|
|
||||||
{
|
|
||||||
case GamePhase.Betting:
|
|
||||||
Console.Write($" Enter bet (1–{state.Chips}): ");
|
|
||||||
break;
|
|
||||||
case GamePhase.PlayerTurn:
|
|
||||||
Console.Write(" [H]it or [S]tand: ");
|
|
||||||
break;
|
|
||||||
case GamePhase.RoundOver:
|
|
||||||
Console.WriteLine($" {ResultText(state.Result)}");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.Write(" Press Enter for next round...");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PrintHand(World world, DealerHand hand, bool skipFirst = false)
|
|
||||||
{
|
|
||||||
PrintHandImpl(world, hand, skipFirst);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PrintHand(World world, PlayerHand hand)
|
|
||||||
{
|
|
||||||
PrintHandImpl(world, hand, skipFirst: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PrintHandImpl<T>(World world, T handTag, bool skipFirst)
|
|
||||||
where T : struct
|
|
||||||
{
|
|
||||||
Entity handEntity = Entity.Null;
|
|
||||||
using (var iter = world.Select<T>())
|
|
||||||
{
|
|
||||||
while (iter.MoveNext())
|
|
||||||
{
|
|
||||||
if (iter.CurrentEntity != World.SingletonEntity)
|
|
||||||
{
|
|
||||||
handEntity = iter.CurrentEntity;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handEntity == Entity.Null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var cards = world.GetSources<Holds>(handEntity);
|
|
||||||
var ordered = cards.OrderByDescending(c =>
|
|
||||||
{
|
|
||||||
var card = world.ReadComponent<Card>(c);
|
|
||||||
return (int)card.Rank;
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
bool first = true;
|
|
||||||
foreach (var cardEntity in ordered)
|
|
||||||
{
|
|
||||||
if (skipFirst && first)
|
|
||||||
{
|
|
||||||
first = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
var card = world.ReadComponent<Card>(cardEntity);
|
|
||||||
Console.Write(CardToString(card));
|
|
||||||
Console.Write(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CardToString(Card card)
|
|
||||||
{
|
|
||||||
var suit = card.Suit switch
|
|
||||||
{
|
|
||||||
Suit.Hearts => "♥",
|
|
||||||
Suit.Diamonds => "♦",
|
|
||||||
Suit.Clubs => "♣",
|
|
||||||
Suit.Spades => "♠",
|
|
||||||
_ => "?"
|
|
||||||
};
|
|
||||||
|
|
||||||
var rank = card.Rank switch
|
|
||||||
{
|
|
||||||
Rank.Ace => "A",
|
|
||||||
Rank.Jack => "J",
|
|
||||||
Rank.Queen => "Q",
|
|
||||||
Rank.King => "K",
|
|
||||||
_ => ((int)card.Rank).ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return $"{rank}{suit}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResultText(RoundResult result) => result switch
|
|
||||||
{
|
|
||||||
RoundResult.PlayerBust => "You bust! Dealer wins.",
|
|
||||||
RoundResult.DealerBust => "Dealer busts! You win!",
|
|
||||||
RoundResult.PlayerWin => "You win!",
|
|
||||||
RoundResult.DealerWin => "Dealer wins.",
|
|
||||||
RoundResult.Push => "Push — it's a tie!",
|
|
||||||
RoundResult.Blackjack => "Blackjack!",
|
|
||||||
_ => ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -50,8 +50,8 @@ public static class Program
|
||||||
// ── Wire up systems ───────────────────────────────────────────
|
// ── Wire up systems ───────────────────────────────────────────
|
||||||
|
|
||||||
var group = new SystemGroup(world);
|
var group = new SystemGroup(world);
|
||||||
group.Add(new WinCheckSystem());
|
group.Add(new WinCheckSystem(world));
|
||||||
group.Add(new RenderSystem());
|
group.Add(new RenderSystem(world));
|
||||||
|
|
||||||
// ── Game loop ─────────────────────────────────────────────────
|
// ── Game loop ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ namespace TicTacToe;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RenderSystem : ISystem
|
public class RenderSystem : ISystem
|
||||||
{
|
{
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
|
public RenderSystem(World world)
|
||||||
|
{
|
||||||
|
Query = world.Query().With<Cell>().Build();
|
||||||
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
{
|
{
|
||||||
var state = world.ReadSingleton<GameState>();
|
var state = world.ReadSingleton<GameState>();
|
||||||
|
|
@ -18,7 +25,8 @@ public class RenderSystem : ISystem
|
||||||
grid[r, c] = '.';
|
grid[r, c] = '.';
|
||||||
|
|
||||||
// Fill in placed marks using the iterator API.
|
// Fill in placed marks using the iterator API.
|
||||||
using var markIter = world.Select<Cell, Mark>();
|
var markQuery = world.Query().With<Cell>().With<Mark>().Build();
|
||||||
|
using var markIter = world.Select<Cell, Mark>(markQuery);
|
||||||
while (markIter.MoveNext())
|
while (markIter.MoveNext())
|
||||||
{
|
{
|
||||||
grid[markIter.Current1.Row, markIter.Current1.Col] =
|
grid[markIter.Current1.Row, markIter.Current1.Col] =
|
||||||
|
|
@ -58,4 +66,4 @@ public class RenderSystem : ISystem
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.Write(" Enter row col (e.g. \"1 2\"): ");
|
Console.Write(" Enter row col (e.g. \"1 2\"): ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,24 @@ namespace TicTacToe;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WinCheckSystem : ISystem
|
public class WinCheckSystem : ISystem
|
||||||
{
|
{
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
|
public WinCheckSystem(World world)
|
||||||
|
{
|
||||||
|
Query = world.Query().With<Cell>().With<Mark>().Build();
|
||||||
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
{
|
{
|
||||||
// Check game status before doing any work. Use ReadSingleton
|
|
||||||
// to avoid auto-marking GameState as modified when we bail early.
|
|
||||||
if (world.ReadSingleton<GameState>().Status != GameStatus.Playing)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ref var state = ref world.GetSingleton<GameState>();
|
ref var state = ref world.GetSingleton<GameState>();
|
||||||
|
|
||||||
|
if (state.Status != GameStatus.Playing)
|
||||||
|
return;
|
||||||
|
|
||||||
// Build a 3×3 grid of marks using the iterator API.
|
// Build a 3×3 grid of marks using the iterator API.
|
||||||
var grid = new Player[3, 3];
|
var grid = new Player[3, 3];
|
||||||
|
|
||||||
using var iter = world.Select<Cell, Mark>();
|
using var iter = world.Select<Cell, Mark>(Query);
|
||||||
while (iter.MoveNext())
|
while (iter.MoveNext())
|
||||||
{
|
{
|
||||||
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
grid[iter.Current1.Row, iter.Current1.Col] = iter.Current2.Player;
|
||||||
|
|
@ -82,4 +87,4 @@ public class WinCheckSystem : ISystem
|
||||||
state.Status = winner == Player.X ? GameStatus.XWon : GameStatus.OWon;
|
state.Status = winner == Player.X ? GameStatus.XWon : GameStatus.OWon;
|
||||||
world.MarkModified<GameState>(World.SingletonEntity);
|
world.MarkModified<GameState>(World.SingletonEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,10 @@
|
||||||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
|
||||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Data\board.csv">
|
<None Update="Data\board.csv">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<packageSources>
|
|
||||||
<!-- Local feed for OECS development builds.
|
|
||||||
Run `dotnet pack src/OECS/OECS.csproj -c Release -o ./nupkgs`
|
|
||||||
to produce the package. -->
|
|
||||||
<add key="local-oecs" value="./nupkgs" />
|
|
||||||
</packageSources>
|
|
||||||
</configuration>
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace OECS.SourceGen;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Incremental source generator that discovers all component types used with
|
|
||||||
/// <see cref="OECS.World"/> and generates a strongly-typed registry so that
|
|
||||||
/// <see cref="OECS.WorldSerializer"/> can save/load without reflection.
|
|
||||||
/// </summary>
|
|
||||||
[Generator]
|
|
||||||
public class ComponentDiscoveryGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private static readonly HashSet<string> _componentApiMethods = new()
|
|
||||||
{
|
|
||||||
// Component management
|
|
||||||
"AddComponent",
|
|
||||||
"RemoveComponent",
|
|
||||||
"HasComponent",
|
|
||||||
"GetComponent",
|
|
||||||
"TryGetComponent",
|
|
||||||
"ReadComponent",
|
|
||||||
"MarkModified",
|
|
||||||
|
|
||||||
// Singletons
|
|
||||||
"SetSingleton",
|
|
||||||
"GetSingleton",
|
|
||||||
"ReadSingleton",
|
|
||||||
"HasSingleton",
|
|
||||||
"RemoveSingleton",
|
|
||||||
|
|
||||||
// Observability
|
|
||||||
"ObserveComponentChanges",
|
|
||||||
|
|
||||||
// Query building
|
|
||||||
"With",
|
|
||||||
"Without",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly HashSet<string> _forEachNames = new()
|
|
||||||
{
|
|
||||||
"ForEach",
|
|
||||||
};
|
|
||||||
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
// Collect all generic method invocations that reference component types.
|
|
||||||
var invocations = context.SyntaxProvider
|
|
||||||
.CreateSyntaxProvider(
|
|
||||||
predicate: IsCandidateInvocation,
|
|
||||||
transform: ExtractComponentType)
|
|
||||||
.Where(t => t.FullyQualifiedName != null)
|
|
||||||
.Select((t, _) => (t.FullyQualifiedName!, t.AssemblyQualifiedName!));
|
|
||||||
|
|
||||||
// Also collect ForEach type arguments from the World class.
|
|
||||||
var forEachCalls = context.SyntaxProvider
|
|
||||||
.CreateSyntaxProvider(
|
|
||||||
predicate: IsForEachCandidate,
|
|
||||||
transform: ExtractForEachTypes)
|
|
||||||
.Where(list => list.Length > 0)
|
|
||||||
.SelectMany((list, _) => list);
|
|
||||||
|
|
||||||
var allTypes = invocations.Collect().Combine(forEachCalls.Collect());
|
|
||||||
|
|
||||||
context.RegisterSourceOutput(allTypes, GenerateRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsCandidateInvocation(SyntaxNode node, CancellationToken _)
|
|
||||||
{
|
|
||||||
if (node is not InvocationExpressionSyntax invocation)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
|
||||||
{
|
|
||||||
if (memberAccess.Name is GenericNameSyntax genericName)
|
|
||||||
{
|
|
||||||
return _componentApiMethods.Contains(genericName.Identifier.Text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsForEachCandidate(SyntaxNode node, CancellationToken _)
|
|
||||||
{
|
|
||||||
if (node is not InvocationExpressionSyntax invocation)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
|
||||||
{
|
|
||||||
var name = memberAccess.Name is GenericNameSyntax gn
|
|
||||||
? gn.Identifier.Text
|
|
||||||
: memberAccess.Name.Identifier.Text;
|
|
||||||
return _forEachNames.Contains(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string? FullyQualifiedName, string? AssemblyQualifiedName) ExtractComponentType(
|
|
||||||
GeneratorSyntaxContext ctx, CancellationToken _)
|
|
||||||
{
|
|
||||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (memberAccess.Name is not GenericNameSyntax genericName)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var typeArg = genericName.TypeArgumentList.Arguments.FirstOrDefault();
|
|
||||||
if (typeArg == null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg);
|
|
||||||
if (symbolInfo.Symbol is not INamedTypeSymbol typeSymbol)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
// Only public struct types are valid components.
|
|
||||||
if (!typeSymbol.IsValueType || typeSymbol.IsAbstract)
|
|
||||||
return default;
|
|
||||||
if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
||||||
|
|
||||||
// Assembly-qualified name for stable serialization (without assembly version).
|
|
||||||
var aqn = typeSymbol.ContainingAssembly != null
|
|
||||||
? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}"
|
|
||||||
: fqn;
|
|
||||||
|
|
||||||
return (fqn, aqn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> ExtractForEachTypes(
|
|
||||||
GeneratorSyntaxContext ctx, CancellationToken _)
|
|
||||||
{
|
|
||||||
if (ctx.Node is not InvocationExpressionSyntax invocation)
|
|
||||||
return ImmutableArray<(string, string)>.Empty;
|
|
||||||
|
|
||||||
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
|
|
||||||
return ImmutableArray<(string, string)>.Empty;
|
|
||||||
|
|
||||||
if (memberAccess.Name is not GenericNameSyntax genericName)
|
|
||||||
return ImmutableArray<(string, string)>.Empty;
|
|
||||||
|
|
||||||
var types = ImmutableArray.CreateBuilder<(string, string)>();
|
|
||||||
foreach (var typeArg in genericName.TypeArgumentList.Arguments)
|
|
||||||
{
|
|
||||||
var symbolInfo = ctx.SemanticModel.GetSymbolInfo(typeArg);
|
|
||||||
if (symbolInfo.Symbol is INamedTypeSymbol typeSymbol &&
|
|
||||||
typeSymbol.IsValueType && !typeSymbol.IsAbstract &&
|
|
||||||
typeSymbol.DeclaredAccessibility == Accessibility.Public)
|
|
||||||
{
|
|
||||||
var fqn = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
||||||
var aqn = typeSymbol.ContainingAssembly != null
|
|
||||||
? $"{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}, {typeSymbol.ContainingAssembly.Name}"
|
|
||||||
: fqn;
|
|
||||||
types.Add((fqn, aqn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.ToImmutable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void GenerateRegistry(SourceProductionContext context,
|
|
||||||
(ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Left,
|
|
||||||
ImmutableArray<(string FullyQualifiedName, string AssemblyQualifiedName)> Right) data)
|
|
||||||
{
|
|
||||||
var (invocations, forEachTypes) = data;
|
|
||||||
|
|
||||||
// Collect unique types by fully-qualified name, deduplicating.
|
|
||||||
var typeMap = new Dictionary<string, (string Fqn, string Aqn)>();
|
|
||||||
foreach (var t in invocations)
|
|
||||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName);
|
|
||||||
foreach (var t in forEachTypes)
|
|
||||||
{
|
|
||||||
typeMap[t.FullyQualifiedName] = (t.FullyQualifiedName, t.AssemblyQualifiedName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeMap.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("// <auto-generated/>");
|
|
||||||
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
||||||
sb.AppendLine("using MessagePack;");
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("namespace OECS");
|
|
||||||
sb.AppendLine("{");
|
|
||||||
sb.AppendLine(" file static class ComponentRegistryInitializer");
|
|
||||||
sb.AppendLine(" {");
|
|
||||||
sb.AppendLine(" [ModuleInitializer]");
|
|
||||||
sb.AppendLine(" public static void Initialize()");
|
|
||||||
sb.AppendLine(" {");
|
|
||||||
sb.AppendLine(" ComponentRegistry.Initialize(new ComponentDescriptor[]");
|
|
||||||
sb.AppendLine(" {");
|
|
||||||
|
|
||||||
bool first = true;
|
|
||||||
foreach (var (fqn, aqn) in typeMap.Values.OrderBy(t => t.Fqn))
|
|
||||||
{
|
|
||||||
if (!first)
|
|
||||||
sb.AppendLine(",");
|
|
||||||
first = false;
|
|
||||||
|
|
||||||
sb.AppendLine($" new ComponentDescriptor(");
|
|
||||||
sb.AppendLine($" typeName: \"{aqn}\",");
|
|
||||||
sb.AppendLine($" type: typeof({fqn}),");
|
|
||||||
sb.AppendLine($" serialize: obj => MessagePackSerializer.Serialize(({fqn})obj),");
|
|
||||||
sb.AppendLine($" deserializeAndAdd: (world, entity, data) =>");
|
|
||||||
sb.AppendLine($" world.AddComponent(entity, MessagePackSerializer.Deserialize<{fqn}>(data))");
|
|
||||||
sb.Append(" )");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine(" });");
|
|
||||||
sb.AppendLine(" }");
|
|
||||||
sb.AppendLine(" }");
|
|
||||||
sb.AppendLine("}");
|
|
||||||
|
|
||||||
context.AddSource("ComponentRegistry.g.cs", sb.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>12</LangVersion>
|
|
||||||
<RootNamespace>OECS.SourceGen</RootNamespace>
|
|
||||||
<AssemblyName>OECS.SourceGen</AssemblyName>
|
|
||||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
|
||||||
<IsRoslynComponent>true</IsRoslynComponent>
|
|
||||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
|
||||||
<DevelopmentDependency>true</DevelopmentDependency>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
@ -14,18 +14,8 @@ internal class ChangeBuffer
|
||||||
private readonly ChangeSet _pending = new();
|
private readonly ChangeSet _pending = new();
|
||||||
|
|
||||||
private readonly Subject<EntityChange> _entitySubject = new();
|
private readonly Subject<EntityChange> _entitySubject = new();
|
||||||
private readonly Dictionary<Type, TrackedSubject> _componentSubjects = new();
|
private readonly Dictionary<Type, Subject<EntityChange>> _componentSubjects = new();
|
||||||
private readonly Dictionary<QueryDescriptor, TrackedSubject> _querySubjects = new();
|
private readonly Dictionary<QueryDescriptor, Subject<EntityChange>> _querySubjects = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wraps a <see cref="Subject{T}"/> with a subscriber count so that
|
|
||||||
/// subjects with no remaining subscribers can be cleaned up.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class TrackedSubject
|
|
||||||
{
|
|
||||||
public readonly Subject<EntityChange> Subject = new();
|
|
||||||
public int SubscriberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
|
/// The change set currently accumulating. Cleared after each <see cref="Post"/>.
|
||||||
|
|
@ -50,18 +40,18 @@ internal class ChangeBuffer
|
||||||
// Push to component-specific subjects.
|
// Push to component-specific subjects.
|
||||||
if (change.ComponentType != null)
|
if (change.ComponentType != null)
|
||||||
{
|
{
|
||||||
if (_componentSubjects.TryGetValue(change.ComponentType, out var compTracked))
|
if (_componentSubjects.TryGetValue(change.ComponentType, out var compSubject))
|
||||||
{
|
{
|
||||||
compTracked.Subject.OnNext(change);
|
compSubject.OnNext(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to matching query subjects.
|
// Push to matching query subjects.
|
||||||
foreach (var (query, tracked) in _querySubjects)
|
foreach (var (query, subject) in _querySubjects)
|
||||||
{
|
{
|
||||||
if (ChangeMatchesQuery(change, query))
|
if (ChangeMatchesQuery(change, query))
|
||||||
{
|
{
|
||||||
tracked.Subject.OnNext(change);
|
subject.OnNext(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,22 +72,12 @@ internal class ChangeBuffer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Observable<EntityChange> ObserveComponentChanges(Type componentType)
|
public Observable<EntityChange> ObserveComponentChanges(Type componentType)
|
||||||
{
|
{
|
||||||
if (!_componentSubjects.TryGetValue(componentType, out var tracked))
|
if (!_componentSubjects.TryGetValue(componentType, out var subject))
|
||||||
{
|
{
|
||||||
tracked = new TrackedSubject();
|
subject = new Subject<EntityChange>();
|
||||||
_componentSubjects[componentType] = tracked;
|
_componentSubjects[componentType] = subject;
|
||||||
}
|
}
|
||||||
|
return subject;
|
||||||
tracked.SubscriberCount++;
|
|
||||||
return WrapWithCleanup(tracked.Subject, () =>
|
|
||||||
{
|
|
||||||
tracked.SubscriberCount--;
|
|
||||||
if (tracked.SubscriberCount <= 0)
|
|
||||||
{
|
|
||||||
tracked.Subject.Dispose();
|
|
||||||
_componentSubjects.Remove(componentType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -105,40 +85,12 @@ internal class ChangeBuffer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
|
public Observable<EntityChange> ObserveQuery(QueryDescriptor query)
|
||||||
{
|
{
|
||||||
if (!_querySubjects.TryGetValue(query, out var tracked))
|
if (!_querySubjects.TryGetValue(query, out var subject))
|
||||||
{
|
{
|
||||||
tracked = new TrackedSubject();
|
subject = new Subject<EntityChange>();
|
||||||
_querySubjects[query] = tracked;
|
_querySubjects[query] = subject;
|
||||||
}
|
}
|
||||||
|
return subject;
|
||||||
tracked.SubscriberCount++;
|
|
||||||
return WrapWithCleanup(tracked.Subject, () =>
|
|
||||||
{
|
|
||||||
tracked.SubscriberCount--;
|
|
||||||
if (tracked.SubscriberCount <= 0)
|
|
||||||
{
|
|
||||||
tracked.Subject.Dispose();
|
|
||||||
_querySubjects.Remove(query);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wraps an observable so that <paramref name="onLastDispose"/> is called
|
|
||||||
/// when the last subscriber disposes.
|
|
||||||
/// </summary>
|
|
||||||
private static Observable<EntityChange> WrapWithCleanup(
|
|
||||||
Observable<EntityChange> source, Action onLastDispose)
|
|
||||||
{
|
|
||||||
return Observable.Create<EntityChange>(observer =>
|
|
||||||
{
|
|
||||||
var subscription = source.Subscribe(observer);
|
|
||||||
return Disposable.Create(() =>
|
|
||||||
{
|
|
||||||
subscription.Dispose();
|
|
||||||
onLastDispose();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -147,14 +99,14 @@ internal class ChangeBuffer
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_entitySubject.Dispose();
|
_entitySubject.Dispose();
|
||||||
foreach (var tracked in _componentSubjects.Values)
|
foreach (var subject in _componentSubjects.Values)
|
||||||
{
|
{
|
||||||
tracked.Subject.Dispose();
|
subject.Dispose();
|
||||||
}
|
}
|
||||||
_componentSubjects.Clear();
|
_componentSubjects.Clear();
|
||||||
foreach (var tracked in _querySubjects.Values)
|
foreach (var subject in _querySubjects.Values)
|
||||||
{
|
{
|
||||||
tracked.Subject.Dispose();
|
subject.Dispose();
|
||||||
}
|
}
|
||||||
_querySubjects.Clear();
|
_querySubjects.Clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,13 @@ namespace OECS;
|
||||||
/// Accumulates <see cref="EntityChange"/> entries during a system run.
|
/// Accumulates <see cref="EntityChange"/> entries during a system run.
|
||||||
///
|
///
|
||||||
/// Deduplication: if the same (entity, kind, componentType) change is marked
|
/// Deduplication: if the same (entity, kind, componentType) change is marked
|
||||||
/// multiple times, only one entry is kept. However, if a structurally opposed
|
/// multiple times, only one entry is kept. Entity-level changes (Added/Removed)
|
||||||
/// change arrives (Added vs Removed) for the same (entity, componentType),
|
/// take precedence over component-level changes for the same entity.
|
||||||
/// the old entry is removed so observers see the full state transition.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ChangeSet
|
internal class ChangeSet
|
||||||
{
|
{
|
||||||
private readonly List<EntityChange> _changes = new();
|
private readonly List<EntityChange> _changes = new();
|
||||||
private readonly Dictionary<(Entity Entity, ChangeKind Kind, Type? ComponentType), int> _dedup = new();
|
private readonly HashSet<(Entity Entity, ChangeKind Kind, Type? ComponentType)> _dedup = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All accumulated changes in insertion order.
|
/// All accumulated changes in insertion order.
|
||||||
|
|
@ -76,31 +75,9 @@ internal class ChangeSet
|
||||||
private void TryAdd(EntityChange change)
|
private void TryAdd(EntityChange change)
|
||||||
{
|
{
|
||||||
var key = (change.Entity, change.Kind, change.ComponentType);
|
var key = (change.Entity, change.Kind, change.ComponentType);
|
||||||
if (_dedup.ContainsKey(key))
|
if (_dedup.Add(key))
|
||||||
{
|
{
|
||||||
// Same (entity, kind, componentType) already recorded — deduplicate.
|
_changes.Add(change);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a component is re-added after being removed within the same
|
|
||||||
// batch, remove the old ComponentRemoved entry so observers see the
|
|
||||||
// full Add → Remove → Add sequence.
|
|
||||||
if (change.Kind == ChangeKind.ComponentAdded && change.ComponentType != null)
|
|
||||||
{
|
|
||||||
var removedKey = (change.Entity, ChangeKind.ComponentRemoved, change.ComponentType);
|
|
||||||
if (_dedup.Remove(removedKey, out int oldIndex))
|
|
||||||
{
|
|
||||||
_changes.RemoveAt(oldIndex);
|
|
||||||
// Adjust indices for all entries that shifted down.
|
|
||||||
foreach (var k in _dedup.Keys.ToList())
|
|
||||||
{
|
|
||||||
if (_dedup[k] > oldIndex)
|
|
||||||
_dedup[k]--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_dedup[key] = _changes.Count;
|
|
||||||
_changes.Add(change);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Describes a component type discovered at compile time by the source generator.
|
|
||||||
/// Used by <see cref="WorldSerializer"/> to save/load components without reflection.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ComponentDescriptor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The assembly-qualified name of the component type, for stable serialization
|
|
||||||
/// across assemblies.
|
|
||||||
/// </summary>
|
|
||||||
public string TypeName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="System.Type"/> of the component.
|
|
||||||
/// </summary>
|
|
||||||
public Type Type { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Serializes a boxed component instance to a MessagePack byte array.
|
|
||||||
/// </summary>
|
|
||||||
public Func<object, byte[]> Serialize { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserializes a MessagePack byte array and adds the component to the entity
|
|
||||||
/// via the typed <see cref="World.AddComponent{T}"/> method. No reflection.
|
|
||||||
/// </summary>
|
|
||||||
public Action<World, Entity, byte[]> DeserializeAndAdd { get; }
|
|
||||||
|
|
||||||
public ComponentDescriptor(
|
|
||||||
string typeName,
|
|
||||||
Type type,
|
|
||||||
Func<object, byte[]> serialize,
|
|
||||||
Action<World, Entity, byte[]> deserializeAndAdd)
|
|
||||||
{
|
|
||||||
TypeName = typeName;
|
|
||||||
Type = type;
|
|
||||||
Serialize = serialize;
|
|
||||||
DeserializeAndAdd = deserializeAndAdd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace OECS;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registry of component types discovered at compile time by the
|
|
||||||
/// OECS.SourceGen incremental generator. The consuming project's
|
|
||||||
/// generated code populates this via a module initializer, so no
|
|
||||||
/// manual registration is needed.
|
|
||||||
/// </summary>
|
|
||||||
public static class ComponentRegistry
|
|
||||||
{
|
|
||||||
private static ComponentDescriptor[]? _descriptors;
|
|
||||||
private static Dictionary<string, ComponentDescriptor>? _byTypeName;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All discovered component descriptors. Populated automatically
|
|
||||||
/// by the source generator at module initialization.
|
|
||||||
/// </summary>
|
|
||||||
public static ComponentDescriptor[] Descriptors =>
|
|
||||||
_descriptors ?? Array.Empty<ComponentDescriptor>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lookup by assembly-qualified type name. Populated automatically
|
|
||||||
/// by the source generator at module initialization.
|
|
||||||
/// </summary>
|
|
||||||
public static IReadOnlyDictionary<string, ComponentDescriptor> ByTypeName
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_byTypeName == null)
|
|
||||||
{
|
|
||||||
var dict = new Dictionary<string, ComponentDescriptor>();
|
|
||||||
foreach (var desc in Descriptors)
|
|
||||||
dict[desc.TypeName] = desc;
|
|
||||||
_byTypeName = dict;
|
|
||||||
}
|
|
||||||
return _byTypeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by generated code to register discovered component types.
|
|
||||||
/// Must be called before any serialization occurs.
|
|
||||||
/// </summary>
|
|
||||||
public static void Initialize(ComponentDescriptor[] descriptors)
|
|
||||||
{
|
|
||||||
_descriptors = descriptors;
|
|
||||||
_byTypeName = null; // Rebuild on next access.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,17 +20,6 @@ public static class EntityIterator
|
||||||
return new Select1<T1>(world, query);
|
return new Select1<T1>(world, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an iterator over entities that have a component of type
|
|
||||||
/// <typeparamref name="T1"/>. No <c>Without</c> filter is applied.
|
|
||||||
/// </summary>
|
|
||||||
public static Select1<T1> Select<T1>(
|
|
||||||
this World world)
|
|
||||||
where T1 : struct
|
|
||||||
{
|
|
||||||
return new Select1<T1>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an iterator over entities matching the query, with ref access
|
/// Returns an iterator over entities matching the query, with ref access
|
||||||
/// to two component types.
|
/// to two component types.
|
||||||
|
|
@ -42,18 +31,6 @@ public static class EntityIterator
|
||||||
return new Select2<T1, T2>(world, query);
|
return new Select2<T1, T2>(world, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an iterator over entities that have both components of type
|
|
||||||
/// <typeparamref name="T1"/> and <typeparamref name="T2"/>.
|
|
||||||
/// No <c>Without</c> filter is applied.
|
|
||||||
/// </summary>
|
|
||||||
public static Select2<T1, T2> Select<T1, T2>(
|
|
||||||
this World world)
|
|
||||||
where T1 : struct where T2 : struct
|
|
||||||
{
|
|
||||||
return new Select2<T1, T2>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns an iterator over entities matching the query, with ref access
|
/// Returns an iterator over entities matching the query, with ref access
|
||||||
/// to three component types.
|
/// to three component types.
|
||||||
|
|
@ -64,18 +41,6 @@ public static class EntityIterator
|
||||||
{
|
{
|
||||||
return new Select3<T1, T2, T3>(world, query);
|
return new Select3<T1, T2, T3>(world, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an iterator over entities that have all three components of type
|
|
||||||
/// <typeparamref name="T1"/>, <typeparamref name="T2"/>, and
|
|
||||||
/// <typeparamref name="T3"/>. No <c>Without</c> filter is applied.
|
|
||||||
/// </summary>
|
|
||||||
public static Select3<T1, T2, T3> Select<T1, T2, T3>(
|
|
||||||
this World world)
|
|
||||||
where T1 : struct where T2 : struct where T3 : struct
|
|
||||||
{
|
|
||||||
return new Select3<T1, T2, T3>(world, new QueryDescriptor(new HashSet<Type>(), new HashSet<Type>()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,15 @@ namespace OECS;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A system that runs on every tick.
|
/// A system that runs on every tick.
|
||||||
/// Systems receive the world in their <see cref="Run"/> method.
|
/// Systems declare a query and receive the world in their <see cref="Run"/> method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISystem
|
public interface ISystem
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The query that defines which entities this system operates on.
|
||||||
|
/// </summary>
|
||||||
|
QueryDescriptor Query { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the system logic for this tick.
|
/// Executes the system logic for this tick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,4 @@
|
||||||
<PackageReference Include="R3" Version="1.2.9" />
|
<PackageReference Include="R3" Version="1.2.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Bundle the source generator into the NuGet package as an analyzer.
|
|
||||||
Consumers get compile-time component discovery automatically. -->
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="..\OECS.SourceGen\bin\$(Configuration)\netstandard2.0\OECS.SourceGen.dll"
|
|
||||||
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Ensure the source generator is built before packing.
|
|
||||||
Skipped when NoBuild property is set to true. -->
|
|
||||||
<Target Name="BuildSourceGenerator" BeforeTargets="GenerateNuspec"
|
|
||||||
Condition="'$(NoBuild)' != 'true'">
|
|
||||||
<MSBuild Projects="..\OECS.SourceGen\OECS.SourceGen.csproj"
|
|
||||||
Targets="Build"
|
|
||||||
Properties="Configuration=$(Configuration)" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -129,53 +129,19 @@ internal static class QueryExecutor
|
||||||
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
var set3 = store.GetSet(typeof(T3)) as SparseSet<T3>;
|
||||||
if (set3 == null) return;
|
if (set3 == null) return;
|
||||||
|
|
||||||
// Pick the smallest set as the driver.
|
var dense = set1.Dense;
|
||||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count;
|
var denseEntities = set1.DenseEntities;
|
||||||
|
var count = set1.Count;
|
||||||
|
|
||||||
if (c2 <= c1 && c2 <= c3)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var dense = set2.Dense;
|
var entity = denseEntities[i];
|
||||||
var denseEntities = set2.DenseEntities;
|
if (entity.Id == SingletonId) continue;
|
||||||
var count = set2.Count;
|
if (!set2.Contains(entity)) continue;
|
||||||
for (int i = 0; i < count; i++)
|
if (!set3.Contains(entity)) continue;
|
||||||
{
|
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||||
var entity = denseEntities[i];
|
continue;
|
||||||
if (entity.Id == SingletonId) continue;
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c3 <= c1 && c3 <= c2)
|
|
||||||
{
|
|
||||||
var dense = set3.Dense;
|
|
||||||
var denseEntities = set3.DenseEntities;
|
|
||||||
var count = set3.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dense = set1.Dense;
|
|
||||||
var denseEntities = set1.DenseEntities;
|
|
||||||
var count = set1.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,73 +160,20 @@ internal static class QueryExecutor
|
||||||
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
var set4 = store.GetSet(typeof(T4)) as SparseSet<T4>;
|
||||||
if (set4 == null) return;
|
if (set4 == null) return;
|
||||||
|
|
||||||
// Pick the smallest set as the driver.
|
var dense = set1.Dense;
|
||||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count;
|
var denseEntities = set1.DenseEntities;
|
||||||
int min = Math.Min(Math.Min(c1, c2), Math.Min(c3, c4));
|
var count = set1.Count;
|
||||||
|
|
||||||
if (min == c2)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var dense = set2.Dense;
|
var entity = denseEntities[i];
|
||||||
var denseEntities = set2.DenseEntities;
|
if (entity.Id == SingletonId) continue;
|
||||||
var count = set2.Count;
|
if (!set2.Contains(entity)) continue;
|
||||||
for (int i = 0; i < count; i++)
|
if (!set3.Contains(entity)) continue;
|
||||||
{
|
if (!set4.Contains(entity)) continue;
|
||||||
var entity = denseEntities[i];
|
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||||
if (entity.Id == SingletonId) continue;
|
continue;
|
||||||
if (!set1.Contains(entity)) continue;
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c3)
|
|
||||||
{
|
|
||||||
var dense = set3.Dense;
|
|
||||||
var denseEntities = set3.DenseEntities;
|
|
||||||
var count = set3.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c4)
|
|
||||||
{
|
|
||||||
var dense = set4.Dense;
|
|
||||||
var denseEntities = set4.DenseEntities;
|
|
||||||
var count = set4.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dense = set1.Dense;
|
|
||||||
var denseEntities = set1.DenseEntities;
|
|
||||||
var count = set1.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,94 +194,21 @@ internal static class QueryExecutor
|
||||||
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
var set5 = store.GetSet(typeof(T5)) as SparseSet<T5>;
|
||||||
if (set5 == null) return;
|
if (set5 == null) return;
|
||||||
|
|
||||||
// Pick the smallest set as the driver.
|
var dense = set1.Dense;
|
||||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count;
|
var denseEntities = set1.DenseEntities;
|
||||||
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), c5);
|
var count = set1.Count;
|
||||||
|
|
||||||
if (min == c2)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var dense = set2.Dense;
|
var entity = denseEntities[i];
|
||||||
var denseEntities = set2.DenseEntities;
|
if (entity.Id == SingletonId) continue;
|
||||||
var count = set2.Count;
|
if (!set2.Contains(entity)) continue;
|
||||||
for (int i = 0; i < count; i++)
|
if (!set3.Contains(entity)) continue;
|
||||||
{
|
if (!set4.Contains(entity)) continue;
|
||||||
var entity = denseEntities[i];
|
if (!set5.Contains(entity)) continue;
|
||||||
if (entity.Id == SingletonId) continue;
|
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||||
if (!set1.Contains(entity)) continue;
|
continue;
|
||||||
if (!set3.Contains(entity)) continue;
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c3)
|
|
||||||
{
|
|
||||||
var dense = set3.Dense;
|
|
||||||
var denseEntities = set3.DenseEntities;
|
|
||||||
var count = set3.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c4)
|
|
||||||
{
|
|
||||||
var dense = set4.Dense;
|
|
||||||
var denseEntities = set4.DenseEntities;
|
|
||||||
var count = set4.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c5)
|
|
||||||
{
|
|
||||||
var dense = set5.Dense;
|
|
||||||
var denseEntities = set5.DenseEntities;
|
|
||||||
var count = set5.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dense = set1.Dense;
|
|
||||||
var denseEntities = set1.DenseEntities;
|
|
||||||
var count = set1.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,117 +231,22 @@ internal static class QueryExecutor
|
||||||
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
var set6 = store.GetSet(typeof(T6)) as SparseSet<T6>;
|
||||||
if (set6 == null) return;
|
if (set6 == null) return;
|
||||||
|
|
||||||
// Pick the smallest set as the driver.
|
var dense = set1.Dense;
|
||||||
int c1 = set1.Count, c2 = set2.Count, c3 = set3.Count, c4 = set4.Count, c5 = set5.Count, c6 = set6.Count;
|
var denseEntities = set1.DenseEntities;
|
||||||
int min = Math.Min(Math.Min(Math.Min(c1, c2), Math.Min(c3, c4)), Math.Min(c5, c6));
|
var count = set1.Count;
|
||||||
|
|
||||||
if (min == c2)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var dense = set2.Dense;
|
var entity = denseEntities[i];
|
||||||
var denseEntities = set2.DenseEntities;
|
if (entity.Id == SingletonId) continue;
|
||||||
var count = set2.Count;
|
if (!set2.Contains(entity)) continue;
|
||||||
for (int i = 0; i < count; i++)
|
if (!set3.Contains(entity)) continue;
|
||||||
{
|
if (!set4.Contains(entity)) continue;
|
||||||
var entity = denseEntities[i];
|
if (!set5.Contains(entity)) continue;
|
||||||
if (entity.Id == SingletonId) continue;
|
if (!set6.Contains(entity)) continue;
|
||||||
if (!set1.Contains(entity)) continue;
|
if (!PassesWithoutFilter(store, entity, query.Without))
|
||||||
if (!set3.Contains(entity)) continue;
|
continue;
|
||||||
if (!set4.Contains(entity)) continue;
|
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!set6.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref dense[i], ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c3)
|
|
||||||
{
|
|
||||||
var dense = set3.Dense;
|
|
||||||
var denseEntities = set3.DenseEntities;
|
|
||||||
var count = set3.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!set6.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref dense[i], ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c4)
|
|
||||||
{
|
|
||||||
var dense = set4.Dense;
|
|
||||||
var denseEntities = set4.DenseEntities;
|
|
||||||
var count = set4.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!set6.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref dense[i], ref set5.Get(entity), ref set6.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c5)
|
|
||||||
{
|
|
||||||
var dense = set5.Dense;
|
|
||||||
var denseEntities = set5.DenseEntities;
|
|
||||||
var count = set5.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set6.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref dense[i], ref set6.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (min == c6)
|
|
||||||
{
|
|
||||||
var dense = set6.Dense;
|
|
||||||
var denseEntities = set6.DenseEntities;
|
|
||||||
var count = set6.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set1.Contains(entity)) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref set1.Get(entity), ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref dense[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dense = set1.Dense;
|
|
||||||
var denseEntities = set1.DenseEntities;
|
|
||||||
var count = set1.Count;
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
var entity = denseEntities[i];
|
|
||||||
if (entity.Id == SingletonId) continue;
|
|
||||||
if (!set2.Contains(entity)) continue;
|
|
||||||
if (!set3.Contains(entity)) continue;
|
|
||||||
if (!set4.Contains(entity)) continue;
|
|
||||||
if (!set5.Contains(entity)) continue;
|
|
||||||
if (!set6.Contains(entity)) continue;
|
|
||||||
if (!PassesWithoutFilter(store, entity, query.Without)) continue;
|
|
||||||
action(entity, ref dense[i], ref set2.Get(entity), ref set3.Get(entity), ref set4.Get(entity), ref set5.Get(entity), ref set6.Get(entity));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,7 +25,6 @@ namespace OECS;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public struct Relationship<TSelf, TTarget> : IRelationship
|
public struct Relationship<TSelf, TTarget> : IRelationship
|
||||||
where TSelf : struct where TTarget : struct
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The entity that owns this relationship component.
|
/// The entity that owns this relationship component.
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,6 @@ internal class SparseSet<T> : ISparseSet where T : struct
|
||||||
{
|
{
|
||||||
_sparse[_denseEntities[i].Id] = -1;
|
_sparse[_denseEntities[i].Id] = -1;
|
||||||
}
|
}
|
||||||
Array.Clear(_denseEntities, 0, _count);
|
|
||||||
_count = 0;
|
_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,17 +188,6 @@ public class World : IDisposable
|
||||||
// If replacing an existing relationship, remove the old index entry first.
|
// If replacing an existing relationship, remove the old index entry first.
|
||||||
if (component is IRelationship newRel)
|
if (component is IRelationship newRel)
|
||||||
{
|
{
|
||||||
// Guard against mismatched Source: the relationship must be stored
|
|
||||||
// on the entity it claims as its Source, otherwise the reverse
|
|
||||||
// index becomes corrupted.
|
|
||||||
if (newRel.Source != entity)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Relationship of type {typeof(T).Name} has Source={newRel.Source} " +
|
|
||||||
$"but is being added to entity {entity}. " +
|
|
||||||
$"The Source must match the entity the component is added to.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_components.TryGet<T>(entity, out var old))
|
if (_components.TryGet<T>(entity, out var old))
|
||||||
{
|
{
|
||||||
var oldRel = (IRelationship)(object)old;
|
var oldRel = (IRelationship)(object)old;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,9 @@ namespace OECS;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves and loads a <see cref="World"/> to/from a MessagePack stream.
|
/// Saves and loads a <see cref="World"/> to/from a MessagePack stream.
|
||||||
///
|
///
|
||||||
/// Uses the compile-time generated <see cref="ComponentRegistry"/> to
|
/// Components are serialized by their runtime type. The caller must
|
||||||
/// serialize and deserialize components without reflection. All component
|
/// ensure all component types used in the world are resolvable via
|
||||||
/// types used with the World API are discovered by the OECS.SourceGen
|
/// <see cref="Type.GetType(string)"/> at load time.
|
||||||
/// incremental generator at build time.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WorldSerializer
|
public static class WorldSerializer
|
||||||
{
|
{
|
||||||
|
|
@ -19,9 +18,9 @@ public static class WorldSerializer
|
||||||
{
|
{
|
||||||
var entityComponents = new Dictionary<Entity, List<ComponentEntry>>();
|
var entityComponents = new Dictionary<Entity, List<ComponentEntry>>();
|
||||||
|
|
||||||
foreach (var desc in ComponentRegistry.Descriptors)
|
foreach (var ct in world.Components.ComponentTypes)
|
||||||
{
|
{
|
||||||
var set = world.Components.GetSet(desc.Type);
|
var set = world.Components.GetSet(ct);
|
||||||
if (set == null || set.Count == 0) continue;
|
if (set == null || set.Count == 0) continue;
|
||||||
|
|
||||||
var entities = set.GetDenseEntities();
|
var entities = set.GetDenseEntities();
|
||||||
|
|
@ -38,8 +37,8 @@ public static class WorldSerializer
|
||||||
|
|
||||||
list.Add(new ComponentEntry
|
list.Add(new ComponentEntry
|
||||||
{
|
{
|
||||||
TypeName = desc.TypeName,
|
TypeName = ct.AssemblyQualifiedName!,
|
||||||
Data = desc.Serialize(component)
|
Data = MessagePackSerializer.Serialize(ct, component)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,11 +67,6 @@ public static class WorldSerializer
|
||||||
{
|
{
|
||||||
var snapshot = MessagePackSerializer.Deserialize<WorldSnapshot>(stream);
|
var snapshot = MessagePackSerializer.Deserialize<WorldSnapshot>(stream);
|
||||||
|
|
||||||
// Build a lookup by type name for O(1) descriptor resolution.
|
|
||||||
var lookup = new Dictionary<string, ComponentDescriptor>();
|
|
||||||
foreach (var desc in ComponentRegistry.Descriptors)
|
|
||||||
lookup[desc.TypeName] = desc;
|
|
||||||
|
|
||||||
foreach (var es in snapshot.Entities)
|
foreach (var es in snapshot.Entities)
|
||||||
{
|
{
|
||||||
var entity = world.CreateEntity(es.Entity);
|
var entity = world.CreateEntity(es.Entity);
|
||||||
|
|
@ -87,14 +81,18 @@ public static class WorldSerializer
|
||||||
|
|
||||||
foreach (var ce in es.Components)
|
foreach (var ce in es.Components)
|
||||||
{
|
{
|
||||||
if (!lookup.TryGetValue(ce.TypeName, out var desc))
|
var type = Type.GetType(ce.TypeName)
|
||||||
throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"Unknown component type '{ce.TypeName}'. " +
|
$"Unknown component type '{ce.TypeName}'. " +
|
||||||
$"Ensure the component type is used with the World " +
|
$"Ensure the assembly is loaded.");
|
||||||
$"API so the source generator can discover it.");
|
|
||||||
|
|
||||||
// Deserialize and add in one typed operation — no reflection.
|
var component = MessagePackSerializer.Deserialize(type, ce.Data)
|
||||||
desc.DeserializeAndAdd(world, entity, ce.Data);
|
?? throw new InvalidOperationException(
|
||||||
|
$"Failed to deserialize component of type '{ce.TypeName}'.");
|
||||||
|
|
||||||
|
var method = typeof(World).GetMethod(nameof(World.AddComponent))!
|
||||||
|
.MakeGenericMethod(type);
|
||||||
|
method.Invoke(world, [entity, component]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -249,10 +249,13 @@ public class CommandTests
|
||||||
private readonly World _world;
|
private readonly World _world;
|
||||||
private readonly SpawnCommand _command;
|
private readonly SpawnCommand _command;
|
||||||
|
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public CommandEnqueueSystem(World world, SpawnCommand command)
|
public CommandEnqueueSystem(World world, SpawnCommand command)
|
||||||
{
|
{
|
||||||
_world = world;
|
_world = world;
|
||||||
_command = command;
|
_command = command;
|
||||||
|
Query = world.Query().With<TestPosition>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
|
|
@ -263,17 +266,18 @@ public class CommandTests
|
||||||
|
|
||||||
private class EntityCountObserver : ISystem
|
private class EntityCountObserver : ISystem
|
||||||
{
|
{
|
||||||
private readonly QueryDescriptor _query;
|
|
||||||
public int SeenCount { get; private set; }
|
public int SeenCount { get; private set; }
|
||||||
|
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public EntityCountObserver(World world)
|
public EntityCountObserver(World world)
|
||||||
{
|
{
|
||||||
_query = world.Query().With<TestPosition>().Build();
|
Query = world.Query().With<TestPosition>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
{
|
{
|
||||||
world.ForEach(_query, (Entity e, ref TestPosition pos) =>
|
world.ForEach(Query, (Entity e, ref TestPosition pos) =>
|
||||||
{
|
{
|
||||||
SeenCount++;
|
SeenCount++;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,4 @@
|
||||||
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
<ProjectReference Include="..\..\src\OECS\OECS.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\OECS.SourceGen\OECS.SourceGen.csproj"
|
|
||||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -175,11 +175,11 @@ public class SystemTests
|
||||||
|
|
||||||
private class MovementSystem : ITickedSystem
|
private class MovementSystem : ITickedSystem
|
||||||
{
|
{
|
||||||
private readonly QueryDescriptor _query;
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public MovementSystem(World world)
|
public MovementSystem(World world)
|
||||||
{
|
{
|
||||||
_query = world.Query().With<Position>().With<Velocity>().Build();
|
Query = world.Query().With<Position>().With<Velocity>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world) => Run(world, Tick.Logical());
|
public void Run(World world) => Run(world, Tick.Logical());
|
||||||
|
|
@ -187,7 +187,7 @@ public class SystemTests
|
||||||
public void Run(World world, Tick tick)
|
public void Run(World world, Tick tick)
|
||||||
{
|
{
|
||||||
float dt = tick.DeltaTime;
|
float dt = tick.DeltaTime;
|
||||||
world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
world.ForEach(Query, (Entity entity, ref Position pos, ref Velocity vel) =>
|
||||||
{
|
{
|
||||||
pos.X += vel.X * dt;
|
pos.X += vel.X * dt;
|
||||||
pos.Y += vel.Y * dt;
|
pos.Y += vel.Y * dt;
|
||||||
|
|
@ -232,11 +232,13 @@ public class SystemTests
|
||||||
|
|
||||||
private class TestLogicalSystem : ITickedSystem
|
private class TestLogicalSystem : ITickedSystem
|
||||||
{
|
{
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
public bool WasCalled { get; private set; }
|
public bool WasCalled { get; private set; }
|
||||||
public Tick ReceivedTick { get; private set; }
|
public Tick ReceivedTick { get; private set; }
|
||||||
|
|
||||||
public TestLogicalSystem(World world)
|
public TestLogicalSystem(World world)
|
||||||
{
|
{
|
||||||
|
Query = world.Query().With<Position>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world) => Run(world, Tick.Logical());
|
public void Run(World world) => Run(world, Tick.Logical());
|
||||||
|
|
@ -270,10 +272,13 @@ public class SystemTests
|
||||||
private readonly int _id;
|
private readonly int _id;
|
||||||
private readonly List<int> _order;
|
private readonly List<int> _order;
|
||||||
|
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public OrderTrackingSystem(World world, int id, List<int> order)
|
public OrderTrackingSystem(World world, int id, List<int> order)
|
||||||
{
|
{
|
||||||
_id = id;
|
_id = id;
|
||||||
_order = order;
|
_order = order;
|
||||||
|
Query = world.Query().With<Position>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
|
|
|
||||||
|
|
@ -268,9 +268,12 @@ public class ReactivityTests
|
||||||
{
|
{
|
||||||
private readonly World _world;
|
private readonly World _world;
|
||||||
|
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public EntityCreatorSystem(World world)
|
public EntityCreatorSystem(World world)
|
||||||
{
|
{
|
||||||
_world = world;
|
_world = world;
|
||||||
|
Query = world.Query().With<Position>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world)
|
public void Run(World world)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ public struct ChildOf { }
|
||||||
public struct ParentOf { }
|
public struct ParentOf { }
|
||||||
public struct OwnedBy { }
|
public struct OwnedBy { }
|
||||||
public struct MemberOf { }
|
public struct MemberOf { }
|
||||||
public struct Thing { }
|
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -154,20 +153,20 @@ public class RelationshipTests
|
||||||
var owner = world.CreateEntity();
|
var owner = world.CreateEntity();
|
||||||
var group = world.CreateEntity();
|
var group = world.CreateEntity();
|
||||||
|
|
||||||
world.AddComponent(entity, new Relationship<OwnedBy, Thing>
|
world.AddComponent(entity, new Relationship<OwnedBy, object>
|
||||||
{
|
{
|
||||||
Source = entity, Target = owner
|
Source = entity, Target = owner
|
||||||
});
|
});
|
||||||
world.AddComponent(entity, new Relationship<MemberOf, Thing>
|
world.AddComponent(entity, new Relationship<MemberOf, object>
|
||||||
{
|
{
|
||||||
Source = entity, Target = group
|
Source = entity, Target = group
|
||||||
});
|
});
|
||||||
|
|
||||||
world.GetSources<Relationship<OwnedBy, Thing>>(owner).Should().BeEquivalentTo([entity]);
|
world.GetSources<Relationship<OwnedBy, object>>(owner).Should().BeEquivalentTo([entity]);
|
||||||
world.GetSources<Relationship<MemberOf, Thing>>(group).Should().BeEquivalentTo([entity]);
|
world.GetSources<Relationship<MemberOf, object>>(group).Should().BeEquivalentTo([entity]);
|
||||||
|
|
||||||
// Cross-check: OwnedBy sources should not appear in MemberOf lookups.
|
// Cross-check: OwnedBy sources should not appear in MemberOf lookups.
|
||||||
world.GetSources<Relationship<MemberOf, Thing>>(owner).Should().BeEmpty();
|
world.GetSources<Relationship<MemberOf, object>>(owner).Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,12 @@ public class SingletonTests
|
||||||
{
|
{
|
||||||
private readonly World _world;
|
private readonly World _world;
|
||||||
|
|
||||||
|
public QueryDescriptor Query { get; }
|
||||||
|
|
||||||
public TimeSystem(World world)
|
public TimeSystem(World world)
|
||||||
{
|
{
|
||||||
_world = world;
|
_world = world;
|
||||||
|
Query = world.Query().With<GameConfig>().Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run(World world) => Run(world, Tick.Logical());
|
public void Run(World world) => Run(world, Tick.Logical());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue