forked from Misaki/GhostEngine
ECS refactor: new ComponentSet, serialization, generators
Major ECS API overhaul: added ComponentSet, refactored ComponentRegistry, and updated all entity/component creation methods. Introduced robust custom serialization infrastructure and per-component source generators for registration and (de)serialization. Updated editor, engine, and test code to use new APIs. Improved code quality, naming, and performance throughout. Removed obsolete code and updated dependencies.
This commit is contained in:
138
Ghost.Generator/ComponentRegistrationGenerator.cs
Normal file
138
Ghost.Generator/ComponentRegistrationGenerator.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ghost.Generator
|
||||
{
|
||||
[Generator]
|
||||
public class ComponentRegistrationGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// 1. Pipeline: Find all structs implementing IComponent
|
||||
var componentCandidates = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: (s, _) => s is StructDeclarationSyntax,
|
||||
transform: GetComponentSymbol)
|
||||
.Where(symbol => symbol != null)
|
||||
.Collect();
|
||||
|
||||
// 2. Pipeline: Find the ONE class with [EngineEntry]
|
||||
var engineEntryClass = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
predicate: (s, _) => s is ClassDeclarationSyntax,
|
||||
transform: GetEngineEntrySymbol)
|
||||
.Where(symbol => symbol != null)
|
||||
.Collect(); // Returns ImmutableArray<INamedTypeSymbol>
|
||||
|
||||
// 3. COMBINE: Pair the list of components with the list of entry classes
|
||||
// The result 'source' in the callback will be a tuple: (ImmutableArray<Info>, ImmutableArray<Entry>)
|
||||
var combinedProvider = componentCandidates.Combine(engineEntryClass);
|
||||
|
||||
// 4. Output: Generate the source using both pieces of data at once
|
||||
context.RegisterSourceOutput(combinedProvider, GenerateRegistrationCode);
|
||||
}
|
||||
|
||||
// Extraction Logic for Components
|
||||
private static INamedTypeSymbol GetComponentSymbol(GeneratorSyntaxContext ctx, CancellationToken _)
|
||||
{
|
||||
var structSyntax = (StructDeclarationSyntax)ctx.Node;
|
||||
if (!(ctx.SemanticModel.GetDeclaredSymbol(structSyntax) is INamedTypeSymbol symbol))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var iComponentSymbol = ctx.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Entities.IComponent");
|
||||
if (iComponentSymbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var iface in symbol.AllInterfaces)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, iComponentSymbol))
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extraction Logic for Engine Entry
|
||||
private static INamedTypeSymbol GetEngineEntrySymbol(GeneratorSyntaxContext ctx, CancellationToken _)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
|
||||
if (!(ctx.SemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol symbol))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check attributes
|
||||
foreach (var attribute in symbol.GetAttributes())
|
||||
{
|
||||
if (attribute.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Ghost.Engine.EngineEntryAttribute")
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// The Generation Logic (Stateless)
|
||||
private static void GenerateRegistrationCode(
|
||||
SourceProductionContext context,
|
||||
(ImmutableArray<INamedTypeSymbol> Components, ImmutableArray<INamedTypeSymbol> Entries) source)
|
||||
{
|
||||
var components = source.Components;
|
||||
var entries = source.Entries;
|
||||
|
||||
// 1. Validation: Ensure we found exactly one [EngineEntry] class
|
||||
if (entries.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick the first one (if multiple exist, you might want to report a diagnostic error)
|
||||
var targetClass = entries[0];
|
||||
|
||||
if (components.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Extract Namespace and Class Name directly from the symbol found in the pipeline
|
||||
var targetNamespace = targetClass.ContainingNamespace.ToDisplayString();
|
||||
var targetClassName = targetClass.Name;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($@"
|
||||
namespace {targetNamespace}
|
||||
{{
|
||||
public partial class {targetClassName}
|
||||
{{
|
||||
private static void RegisterIComponentTypes()
|
||||
{{");
|
||||
|
||||
foreach (var symbol in components.Distinct(SymbolEqualityComparer.Default))
|
||||
{
|
||||
if (symbol is null) continue;
|
||||
|
||||
sb.Append($@"
|
||||
global::Ghost.Entities.ComponentRegistry.GetOrRegisterComponent<{symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
|
||||
}
|
||||
|
||||
sb.Append(@"
|
||||
}
|
||||
}
|
||||
}");
|
||||
|
||||
context.AddSource($"{targetClassName}.ComponentReg.gen.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -10,67 +9,200 @@ namespace Ghost.Generator
|
||||
[Generator]
|
||||
public class ComponentSerializationGenerator : IIncrementalGenerator
|
||||
{
|
||||
private void GenerateJsonContext(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> symbols)
|
||||
private string GetJsonWriteCall(ITypeSymbol type, string fieldName)
|
||||
{
|
||||
if (symbols.IsDefaultOrEmpty)
|
||||
// 1. PRIMITIVES (Fastest)
|
||||
switch (type.SpecialType)
|
||||
{
|
||||
return;
|
||||
case SpecialType.System_Byte:
|
||||
case SpecialType.System_SByte:
|
||||
case SpecialType.System_Int16:
|
||||
case SpecialType.System_Int32:
|
||||
case SpecialType.System_Int64:
|
||||
case SpecialType.System_Single:
|
||||
case SpecialType.System_Double:
|
||||
case SpecialType.System_Decimal:
|
||||
case SpecialType.System_UInt64:
|
||||
case SpecialType.System_UInt16:
|
||||
case SpecialType.System_UInt32:
|
||||
case SpecialType.System_IntPtr:
|
||||
case SpecialType.System_UIntPtr:
|
||||
return $@"writer.WriteNumber(""{fieldName}"", value.{fieldName});";
|
||||
case SpecialType.System_Boolean:
|
||||
return $@"writer.WriteBoolean(""{fieldName}"", value.{fieldName});";
|
||||
case SpecialType.System_Char:
|
||||
return $@"writer.WriteString(""{fieldName}"", [value.{fieldName}]);";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(@"
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Ghost.Engine.Components.Serialization
|
||||
{
|
||||
[JsonSourceGenerationOptions(WriteIndented = true, IncludeFields = true)]");
|
||||
|
||||
foreach (var symbol in symbols.Distinct(SymbolEqualityComparer.Default))
|
||||
// TODO: Add well-known types like float3, Vector3, etc.
|
||||
#if false
|
||||
// 2. KNOWN MATH TYPES (Optimized Arrays)
|
||||
// Checking by name is simple and effective for standard math libs
|
||||
if (type.Name == "float3" || type.Name == "Vector3")
|
||||
{
|
||||
if (symbol is null)
|
||||
return $@"writer.WritePropertyName(""{fieldName}"");
|
||||
writer.WriteStartArray();
|
||||
writer.WriteNumberValue(value.{fieldName}.x);
|
||||
writer.WriteNumberValue(value.{fieldName}.y);
|
||||
writer.WriteNumberValue(value.{fieldName}.z);
|
||||
writer.WriteEndArray();";
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. FALLBACK: System.Text.Json (Reflection)
|
||||
// If we don't know what it is, let the standard serializer handle it.
|
||||
// This handles float4x4, List<T>, and other nested structs automatically.
|
||||
return $@"writer.WritePropertyName(""{fieldName}""); global::System.Text.Json.JsonSerializer.Serialize(writer, value.{fieldName}, options);";
|
||||
}
|
||||
|
||||
private string GetJsonReadCall(ITypeSymbol type)
|
||||
{
|
||||
switch (type.SpecialType)
|
||||
{
|
||||
case SpecialType.System_Byte: return "reader.GetByte()";
|
||||
case SpecialType.System_SByte: return "reader.GetSByte()";
|
||||
case SpecialType.System_Int16: return "reader.GetInt16()";
|
||||
case SpecialType.System_Int32: return "reader.GetInt32()";
|
||||
case SpecialType.System_Int64: return "reader.GetInt64()";
|
||||
case SpecialType.System_Single: return "reader.GetSingle()";
|
||||
case SpecialType.System_Double: return "reader.GetDouble()";
|
||||
case SpecialType.System_Decimal: return "reader.GetDecimal()";
|
||||
case SpecialType.System_UInt16: return "reader.GetUInt16()";
|
||||
case SpecialType.System_UInt32: return "reader.GetUInt32()";
|
||||
case SpecialType.System_UInt64: return "reader.GetUInt64()";
|
||||
case SpecialType.System_IntPtr: return "reader.GetInt64()"; // Note: IntPtr size varies by platform
|
||||
case SpecialType.System_UIntPtr: return "reader.GetUInt64()"; // Note: UIntPtr size varies by platform
|
||||
case SpecialType.System_Boolean: return "reader.GetBoolean()";
|
||||
case SpecialType.System_Char: return "reader.GetString()[0]";
|
||||
}
|
||||
|
||||
#if false
|
||||
// For custom math types, you'd need to generate code to read the array back
|
||||
if (type.Name == "float3") return "new float3(reader.GetSingle(), reader.GetSingle(), reader.GetSingle())"; // Simplified
|
||||
#endif
|
||||
|
||||
return $"global::System.Text.Json.JsonSerializer.Deserialize<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(ref reader, options)";
|
||||
}
|
||||
|
||||
private void GenerateJsonSerializer(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> symbols)
|
||||
{
|
||||
var sbWrites = new StringBuilder();
|
||||
var sbReads = new StringBuilder();
|
||||
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var namespaceName = symbol.ContainingNamespace.ToDisplayString();
|
||||
var structName = symbol.Name;
|
||||
var fullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// 1. Build Field Logic (Same as before)
|
||||
sbWrites.Clear();
|
||||
sbReads.Clear();
|
||||
|
||||
var fields = symbol.GetMembers()
|
||||
.OfType<IFieldSymbol>()
|
||||
.Where(f => !f.IsStatic && f.DeclaredAccessibility == Accessibility.Public);
|
||||
|
||||
foreach (var field in fields)
|
||||
{
|
||||
continue;
|
||||
// Note: GetJsonWriteCall returns a string ending with ";"
|
||||
var writeCall = GetJsonWriteCall(field.Type, field.Name);
|
||||
if (writeCall != null)
|
||||
{
|
||||
sbWrites.Append(" "); // Indentation
|
||||
sbWrites.AppendLine(writeCall);
|
||||
}
|
||||
|
||||
var readCall = GetJsonReadCall(field.Type);
|
||||
if (readCall != null)
|
||||
{
|
||||
// Note the double quotes ""{field.Name}"" for the case string
|
||||
sbReads.Append(" "); // Indentation
|
||||
sbReads.AppendLine($@"case ""{field.Name}"": result.{field.Name} = {readCall}; break;");
|
||||
}
|
||||
}
|
||||
|
||||
var fqtn = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
sb.Append($@"
|
||||
[JsonSerializable(typeof({fqtn}))]");
|
||||
// 2. The Main Template using $@
|
||||
// Watch the double braces {{ }} and double quotes "" ""
|
||||
var sourceCode = $@"// <auto-generated/>
|
||||
|
||||
namespace {namespaceName}
|
||||
{{
|
||||
public unsafe static class {structName}_Serializer
|
||||
{{
|
||||
[global::System.Runtime.CompilerServices.ModuleInitializer]
|
||||
internal static void Init()
|
||||
{{
|
||||
var id = Ghost.Entities.ComponentTypeID<{fullTypeName}>.Value;
|
||||
Ghost.Engine.IO.ComponentSerializerRegistry.Register(id, SerializeBinaryUnsafe, SerializeJsonUnsafe);
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// BINARY (Fast Path)
|
||||
// ---------------------------------------------------------
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe void SerializeBinaryUnsafe(global::System.IO.BinaryWriter writer, void* value)
|
||||
{{
|
||||
writer.Write(new global::System.ReadOnlySpan<byte>(value, sizeof({fullTypeName})));
|
||||
}}
|
||||
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static void SerializeBinary(this global::System.IO.BinaryWriter writer, ref {fullTypeName} value)
|
||||
{{
|
||||
unsafe {{ writer.Write(new global::System.ReadOnlySpan<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
|
||||
}}
|
||||
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static void DeserializeBinary(this global::System.IO.BinaryReader reader, ref {fullTypeName} value)
|
||||
{{
|
||||
unsafe {{ reader.Read(new global::System.Span<byte>(global::System.Runtime.CompilerServices.Unsafe.AsPointer(ref value), sizeof({fullTypeName}))); }}
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// JSON WRITE
|
||||
// ---------------------------------------------------------
|
||||
public static unsafe void SerializeJsonUnsafe(System.Text.Json.Utf8JsonWriter writer, void* ptr, System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
SerializeJson(writer, ref *( {fullTypeName}*)ptr, options);
|
||||
}}
|
||||
|
||||
public static void SerializeJson(this global::System.Text.Json.Utf8JsonWriter writer, ref {fullTypeName} value, global::System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
writer.WriteStartObject();
|
||||
{sbWrites}
|
||||
writer.WriteEndObject();
|
||||
}}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// JSON READ
|
||||
// ---------------------------------------------------------
|
||||
public static {fullTypeName} DeserializeJson(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Text.Json.JsonSerializerOptions? options)
|
||||
{{
|
||||
var result = default({fullTypeName});
|
||||
|
||||
if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject) throw new global::System.Text.Json.JsonException();
|
||||
|
||||
while (reader.Read())
|
||||
{{
|
||||
if (reader.TokenType == global::System.Text.Json.JsonTokenType.EndObject) return result;
|
||||
if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName) throw new global::System.Text.Json.JsonException();
|
||||
|
||||
var propName = reader.GetString();
|
||||
reader.Read();
|
||||
|
||||
switch (propName)
|
||||
{{
|
||||
{sbReads}
|
||||
default: reader.Skip(); break;
|
||||
}}
|
||||
}}
|
||||
return result;
|
||||
}}
|
||||
}}
|
||||
}}";
|
||||
|
||||
context.AddSource($"{structName}.Serializer.gen.cs", sourceCode);
|
||||
}
|
||||
|
||||
sb.Append(@"
|
||||
public partial class ComponentJsonContext : JsonSerializerContext
|
||||
{
|
||||
private static readonly Dictionary<Type, JsonTypeInfo> _typeLookup = new()
|
||||
{");
|
||||
|
||||
foreach (var symbol in symbols.Distinct(SymbolEqualityComparer.Default))
|
||||
{
|
||||
if (symbol is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fqtn = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
sb.Append($@"
|
||||
{{ typeof({fqtn}), ComponentJsonContext.{symbol.Name} }},");
|
||||
}
|
||||
|
||||
sb.Append(@"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the generated JsonTypeInfo for a given component type.
|
||||
/// </summary>
|
||||
public static bool TryGetTypeInfo(Type componentType, out JsonTypeInfo jsonTypeInfo) =>
|
||||
_typeLookup.TryGetValue(componentType, out jsonTypeInfo);
|
||||
}
|
||||
}");
|
||||
|
||||
context.AddSource("ComponentJsonContext.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
@@ -88,16 +220,16 @@ namespace Ghost.Engine.Components.Serialization
|
||||
}
|
||||
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var iComponentDataSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.Components.IComponentData");
|
||||
var iComponentSymbol = compilation.GetTypeByMetadataName("Ghost.Entities.IComponent");
|
||||
|
||||
if (iComponentDataSymbol == null)
|
||||
if (iComponentSymbol == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var iface in symbol.AllInterfaces)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, iComponentDataSymbol))
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, iComponentSymbol))
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
@@ -108,7 +240,7 @@ namespace Ghost.Engine.Components.Serialization
|
||||
.Where(symbol => symbol != null)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(componentCandidates, GenerateJsonContext);
|
||||
context.RegisterSourceOutput(componentCandidates, GenerateJsonSerializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Shader\**" />
|
||||
<EmbeddedResource Remove="Shader\**" />
|
||||
<None Remove="Shader\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -13,8 +19,4 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Shader\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user