Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entitas with Roslyn Code Generation via dotnet IIncrementalGenerator #1005

Closed
31 tasks done
studentutu opened this issue Aug 26, 2022 · 79 comments
Closed
31 tasks done

Comments

@studentutu
Copy link

studentutu commented Aug 26, 2022

[ EDIT by @sschmid ]

I edited the issue description because this issue is now the main issue for the new code generator

dotnet's source generators, specially IIncrementalGenerator may be a valid alternative to the current approach with Jenny

Learn about dotnet Incremental Generators:

While migrating to dotnet source generators, I will use the opportunity to update and improve the generated code:

  • add namespaces support for components and contexts
  • support multiple projects and multiple Unity asmdef

I already made good progress but it's still in research state. Any help from the community is greatly appreciated! Please feel free to engage and help in the conversation if you can! 🙏


Tasks

Generators

  • Component ContextApi Generator (Unique, Normal, Flag)
  • Component EntityApi Generator (Normal, Flag)
  • OBSOLETE Component EntityApiInterface Generator (Multiple Contexts)
  • OBSOLETE Component Generator
  • Component Lookup Generator
  • Component MatcherApi Generator
  • ContextAttribute Generator
  • Context Generator
  • ContextMatcher Generator
  • OBSOLETE Contexts Generator
  • Entity Generator
  • EntityIndex Generator
  • Event EntityApi Generator
  • Event ListenerComponent Generator
  • Event ListenerInterface Generator
  • Event System Generator
  • Event Systems Generator
  • OBSOLETE ContextObserver Generator
  • OBSOLETE FeatureClass Generator

Attributes

  • CleanupAttribute
  • ContextAttribute
  • OBSOLETE CustomEntityIndexAttribute
  • EntityIndexAttribute
  • OBSOLETE EntityIndexGetMethodAttribute
  • EventAttribute
  • OBSOLETE ComponentNameAttribute
  • OBSOLETE DontGenerateAttribute
  • OBSOLETE FlagPrefixAttribute
  • OBSOLETE PostConstructorAttribute
  • OBSOLETE PrimaryEntityIndexAttribute
  • UniqueAttribute

original message from issue author:

Hi,

I have a suggestion and would want to contribute.
As in official code generation guide by Unity - https://docs.unity3d.com/Manual/roslyn-analyzers.html - we can use separate project and create dll which unity will use right in it's code compilation steps.

We already have a separate project for it - Jehny, all we need is to make sure that code gen is done by Microsoft Roslyn Source Generators, and put a label “RoslynAnalyzer” for the DLL inside the release branch (create a Unity package)

This way we don't need to use Jehny Server for constant monitoring of changes, and it will help clean up the workflow.

Here's some code that we need to use for Roslyn Source Generators

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text

[Generator]
public class ExampleSourceGenerator : ISourceGenerator
{
}

Hope it will be helpful.

@sschmid
Copy link
Owner

sschmid commented Aug 27, 2022

@studentutu thanks! Roslyn Source Generators is definitely sth I would like to research at some point! That might be useful for Entitas. Jenny however follows a different idea where the input can be anything, not just source code or assemblies. It more general purpose for any kind of code generation

@sschmid
Copy link
Owner

sschmid commented Sep 14, 2022

One downside is

Note: Roslyn analyzers are only compatible with the IDEs that Unity publically supports, which are Visual Studio and JetBrains Rider.

But I'll have a look

@sschmid
Copy link
Owner

sschmid commented Sep 14, 2022

Looks like we would need to switch to Unity when we make changes in order to recompile, which would take too long.

@sschmid
Copy link
Owner

sschmid commented Sep 14, 2022

Ok, building in the IDE works too 👍

@rubenwe
Copy link

rubenwe commented Nov 1, 2022

I've written a few Source Generators. My note would be to look into Incremental Source Generators if you decide to go this route. The perf of a normal ISourceGenerator isn't that great.

Unity 2022.2 supports the required newer Microsoft packages for that. They just haven't fixed their documentation yet.

@studentutu
Copy link
Author

yep, definitely better with IIncrementalGenerator

@rubenwe
Copy link

rubenwe commented Jan 6, 2023

One thing of note though: I'm having severe problems with AdditionalFiles in Unity 2021.3 - that's files you want to pass along to source generators outside of source code. So for example if you had some csv files you would want to use as a source to generate from.

The Unity docs on this are kind of sparse - and the one page that does mention this describes an implementation that:
a) does not work - at all - as described
b) is of questionable design, given that one needs to rename files to somename.[Gernerator].additionalfile

So if that is something that one wants to support I'd wait for Unity to actually switch to permanent, modern csproj files and using MsBuild, as outlined in their roadmap talk. Although it is a bit bold to assume that this will just work then. They usually manage to do stuff that is not in line with what .NET devs are used to ;).

@sschmid
Copy link
Owner

sschmid commented Jun 8, 2023

Hi, I started testing IIncrementalGenerator and started a new branch. I added Entitas.Generators and Entitas.Generators.Tests projects to get started.

https://github.com/sschmid/Entitas/tree/roslyn-source-generators/src/Entitas.Generators

https://github.com/sschmid/Entitas/tree/roslyn-source-generators/tests/Entitas.Generators.Tests

So far I'am happy with IIncrementalGenerator performance and will look more into it.

@sschmid
Copy link
Owner

sschmid commented Jun 8, 2023

If all goes well I can imagine that it can replace Jenny and setting up Entitas will be much easier.

I started with snapshot testing to verify the output of the source generators, it's pretty cool. A test looks like this

[Fact]
public Task Test() => TestHelper.Verify(
    GetFixture("NamespacedComponentWithOneField"),
    new ComponentGenerator());

More about snapshot testing:
https://andrewlock.net/creating-a-source-generator-part-2-testing-an-incremental-generator-with-snapshot-testing/

@sschmid
Copy link
Owner

sschmid commented Jun 13, 2023

I made some progress using IIncrementalGenerator. It's great. But now I was testing it with the latest Unity 2022.3 LTS and it looks like IIncrementalGenerator is not yet supported. Can this be true? I hope I'm wrong!

@sschmid
Copy link
Owner

sschmid commented Jun 13, 2023

Ok, got it to work!

Unity docs say you should use this specific version: Microsoft.CodeAnalysis 3.8
https://docs.unity3d.com/Manual/roslyn-analyzers.html

This version does not contain IIncrementalGenerator.

The current version is 4.6.0, but that one does not work in Unity.
I manually checked each version to find the latest one that works, which is

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />

And you can use netstandard2.1
<TargetFramework>netstandard2.1</TargetFramework>

@sschmid
Copy link
Owner

sschmid commented Jun 19, 2023

Currently stuck, because it seems like you cannot resolve generated attributes, e.g.
similar to the current approach I wanted to generate a convenience context attribute for each context, so you can add this attribute to components as usual

[MyApp.Main.Context, Other.Context]
partial MovableComponent : IComponent { }

But since those attributes are generated, the don't seem to be part of the compilation for looking up symbols 😭

This was easily possible with Jenny...

Any ideas how to solve this?

@sschmid
Copy link
Owner

sschmid commented Jun 19, 2023

To be more specific:
Testing with non-generated attributes, I can easily get the attribues like this

var attribute = symbol.GetAttributes().First();

With the generated ones the same code returns ErrorTypeSymbol instead of the attributes

@studentutu
Copy link
Author

studentutu commented Jun 19, 2023

@sschmid you can use following

  1. Create a list of ICustomGenerators with Execute(GeneratorExecutionContext cx, Compilation compilation, other params if needed)

  2. For each generator create a separate class:

/// <summary>
///   Generates systems for step ..... for all components.
/// </summary>
[Generator(LanguageNames.CSharp)]
public class SystemsDescriptorGenerator : IIncrementalGenerator, ICustomGenerators 
{

void ICustomGenerators.Execute(GeneratorExecutionContext cx, Compilation compilation, other params)
  {
    // Do work.
        cx.AddSource($"{cx}.{Attribute}CustomStep{component.Name}.generated", GeneratedCode(cx, component));
  }
}
  1. Don't forget to add to the YourComponents.Generators.csproj
    <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
  </ItemGroup>

And for the actual game /Runtime .csproj:

    <ItemGroup>
    <ProjectReference Include="..\YourComponents.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>

@Ants-Aare
Copy link

afaik step 3. and 4. are already part of the current setup.

Can you provide documentation for ICustomGenerators interface?
Is it part of roslyn or a concept you introduced yourself?
what's calling ICustomGenerators.Execute?

I don't see how this makes generated code of other incremental generators available to the input compilation of this generator. Can you explain how your approach solves/bypasses this?

@studentutu
Copy link
Author

studentutu commented Jun 19, 2023

@ants Aare
ICustomGenerators is just a custom interface, for use with a single generator (ISourceGenerator)

Basically, you begin with a normal Generator (ISourceGenerator), that includes a list of other generators (list of ICustomGenerators ).

Then in the main Generator, you simply execute each custom one by providing GeneratorExecutionContext and Compilation into them.

So by using a predefined ordering, you will get a correct compilation of source generators.

@studentutu
Copy link
Author

By the way, if you will use IIncrementalGenerator - there is no need for custom ordering, as all IIncrementalGenerator's are executed before final compilation.

@Ants-Aare
Copy link

Maybe I'm understanding it wrong, but I don't think this solves the problem(?) What you're describing is just a way to call multiple methods one after each other through an interface inside a regular sourcegenerator. First of all this doesn't include recompilation steps inbetween the calls to ICustomGenerators, which would be neccessary to get the symbols as INamedTypeSymbols using GetAttributes(). Secondly this would downgrade the current solution to a normal ISourceGenerator instead of an IIncrementalGenerator.
Btw: The order in which we call cx.AddSource is irrelevant as all files will be added to the compilation in the same pass once the generator finishes executing. No matter if we're using SourceProductionContext or GeneratorExecutionContext

@studentutu
Copy link
Author

right, missed the part of the "generated source code with new attributes".

@rubenwe
Copy link

rubenwe commented Jun 20, 2023

But since those attributes are generated, they don't seem to be part of the compilation for looking up symbols 😭

Yes, this is part of the design of source generators. This avoids having to have multiple runs and allows the caching mechanisms that make them so efficient.

For my ECS approach I'm not using generated attributes because of this.

The user defines partial classes for the contexts and components implement IComponent<TContext1, ...>.

@sschmid
Copy link
Owner

sschmid commented Jun 20, 2023

Hi, a quick update and some thoughts:

I have a working proof of concept using IIncrementalGenerator.
Main goal of this new generator is easy of use (no Jenny setup needed anymore) and support for namespaces in components and contexts + support for Unity asmdefs.
As previously explained in other GitHub issues, it's necessary to change the generated code to not used partial on the Entity classes (and others), but instead use static C# extension methods. This way it's possible to get a similar and familiar API as we have now, but at the same time support asmdefs. We also cannot pre-generate all component indexes anymore, as new components may be introduced from other dlls. So there are still a few challenges ahead, but here's a quick sample of how it could like based on my current generators:

Contexts

Now with namespace support!
Works with or without a namespace.

You can define contexts in your code like this:

// MainContext.cs
namespace MyApp
{
    partial class MainContext : Entitas.IContext { }
}

// OtherContext.cs
partial class OtherContext : Entitas.IContext { }

Components

Now with namespace support!
Works with or without a namespace.

You can define components in your code like this:

// MovableComponent.cs
namespace MyFeature
{
    [MyApp.Main.Context, Other.Context]
    partial class MovableComponent : Entitas.IComponent { }
}

// PositionComponent.cs
namespace MyFeature
{
    [MyApp.Main.Context, Other.Context]
    partial class PositionComponent : Entitas.IComponent
    {
        public int X;
        public int Y;
    }
}

The generated component extensions work for all specified contexts and can be chained:

MainContext mainContext = new MyApp.MainContext();
MyApp.Main.Entity mainEntity = mainContext.CreateEntity()
    .AddMovable()
    .ReplaceMovable()
    .RemoveMovable()
    .AddPosition(1, 2)
    .ReplacePosition(3, 4)
    .RemovePosition();

OtherContext otherContext = new OtherContext();
Other.Entity otherEntity = otherContext.CreateEntity()
    .AddMovable()
    .ReplaceMovable()
    .RemoveMovable()
    .AddPosition(1, 2)
    .ReplacePosition(3, 4)
    .RemovePosition();

Matchers

I currently generate component indexes for each context, e.g.
MyFeaturePositionComponentIndex. They also include the namespace.
I might use them later to assign component indexes ones the app starts.

Matcher.AllOf(stackalloc[]
{
    MyFeaturePositionComponentIndex.Value,
    MyFeatureMovableComponentIndex.Value
});

@sschmid
Copy link
Owner

sschmid commented Jun 21, 2023

More updates:
Yay, added component deconstructors

var (x, y) = entity.GetPosition();
x.Should().Be(1);
y.Should().Be(2);

Also, since the only purpose of ContextAttributes is for code generators, I added a [Conditional] attribute, so all those attributes are stripped from components once compiled.

@sschmid
Copy link
Owner

sschmid commented Jun 21, 2023

Fyi, for those who are interested about the changes, see branch: roslyn-source-generators
https://github.com/sschmid/Entitas/tree/roslyn-source-generators

@sschmid
Copy link
Owner

sschmid commented Jun 26, 2023

More updates:
I'm currently testing alternatives to the currently generated ComponentLookup. The current approach doesn't allow Unity's asmdefs or multiple projects.

The new approach should work with multiple assemblies per solution. At some point however, you would need to assign an index to each component. With the following idea you can build up your game with multiple separate assemblies and the main project that consumes them can implement a partial method per context and add the ContextInitializationAttribute to it:

public static partial class ContextInitialization
{
    [MyApp.Main.ContextInitialization]
    public static partial void Initialize();
}

This will be picked up by the generator and it will generate everything that used to be in ComponentLookup, e.g.:

namespace Entitas.Generators.IntegrationTests
{
public static partial class ContextInitialization
{
    public static partial void Initialize()
    {
        MyFeatureMovableComponentIndex.Index = new ComponentIndex(0);
        MyFeaturePositionComponentIndex.Index = new ComponentIndex(1);

        MyApp.MainContext.ComponentNames = new string[]
        {
            "MyFeature.Movable",
            "MyFeature.Position"
        };

        MyApp.MainContext.ComponentTypes = new System.Type[]
        {
            typeof(MyFeature.MovableComponent),
            typeof(MyFeature.PositionComponent)
        };
    }
}
}

@sschmid
Copy link
Owner

sschmid commented Jun 27, 2023

More updates:
I improved the overall caching performance by using multiple pipelines. This means, only affected files should be regenerated leaving most of the files untouched.

I can recommend this cookbook for incremental generators:
https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md

Next problem: if something in the generator fails, nothing will be generated. This can easily be reproduced by declaring the same component twice which will break everything. I tried to wrap all spc.AddSource() calls in a try catch block, because they fail when filenames are not unique, but this didn't help either.

Does anyone know how to make handle exceptions in a source generator?

@studentutu
Copy link
Author

studentutu commented Jun 27, 2023

@sschmid you can use tests, and check snapshots? https://andrewlock.net/creating-a-source-generator-part-2-testing-an-incremental-generator-with-snapshot-testing/

Otherwise - you can even use simpler diagnostic as a log inside the compilation
See https://github.com/dotnet/roslyn-sdk/blob/main/src/Microsoft.CodeAnalysis.Testing/README.md

await new CSharpAnalyzerTest<SomeAnalyzerType, XUnitVerifier>
{
    TestState =
    {
        Sources = { @"original source code" },
        ExpectedDiagnostics = null,
        AdditionalFiles =
        {
            ("File1.ext", "content1"),
            ("File2.ext", "content2"),
        },
    },
}.RunAsync();

// Inside try-catch, catch report diagnostic
 context.ReportDiagnostic(Diagnostic.Create(ExceptionRule,
            constructorParameter.Locations[0],
            error));

@rubenwe
Copy link

rubenwe commented Jun 28, 2023

As @studentutu hinted it's probably a good idea to emit diagnostics for these problems if you run into generation failures.

On top of that, you can also be proactive and write additional implementations of DiagnosticAnalyzer for common problem scenarios. Those can also offer code fixes if the class of error has an easy way to fix it. That's the lightbulb fixes that are popping up in Visual Studio / Rider.

In my ECS prototype I used those to validate Queries (so impossible queries produce errors).

@sschmid
Copy link
Owner

sschmid commented Jul 4, 2023

Btw, I'd like to take a moment an thank everyone for your suggestions and help! It's a lot of fun to work on Entitas like that again! 🫶 Together we can make it 💪

@sschmid
Copy link
Owner

sschmid commented Jul 5, 2023

I never used source generator in big projects. Do they scale well with the codebase size?

I just did a simple test which confirms predicates and transforms are ran with every keystroke, no matter if the changes are done in a related or unrelated file, even if it's just whitespace such as spaces or newlines.

initContext.SyntaxProvider.CreateSyntaxProvider(
    Predicate,
    Transform
);

The only caching optimization seems to happen when the transform method produces the same result; then it skips the generation part.

That sounds kind of scary to me, as a simple class itself has multiple child nodes, each triggering the predicate. So 1 class with a few fields and methods might trigger the predicate 10x. Scale that up to a massive codebase and 🤯

Does anyone have experience with larger codebases and source generators?
Will Jenny be a better choice in those cases?

@rubenwe
Copy link

rubenwe commented Jul 5, 2023

Given that dotnet itself is increasingly using source generators, I wouldn't worry too much. Having early exits and a meta model that can easily be created, cached and compared in early stages of the source generator pipeline is absolutely relevant. And so is respecting cancellation requests.

The discussion here is quite relevant I think. They explicitly mention it should be able to scale to repos the size of the CLR itself...

I'm not sure if the generators are currently called in a multi-threaded way, but given their Single-Pass nature and conceptual order independence I wouldn't be surprised.

@studentutu
Copy link
Author

@sschmid using source generators on the large project doesn't impact is that much, whole compilation lasts 1-2 minutes, as we are using a separate dll and a separate project outside of Unity. In unity we then simply use that dll in conjunction with Unity view layer logic.

@sschmid
Copy link
Owner

sschmid commented Jul 5, 2023

@rubenwe Yeah, I might worry too much :D
I remember that feeling made me stick to Jenny another year, where I can opt in and generate once sth relevant changed. I feel better now that I tested it in the IntegrationTests project: the part I was worried about takes 5ms... it's alright I guess :D
And yes, it's not blocking the IDE experience and runs in the background.

@studentutu yeah, separat projects will help to improve the generator, but I was mostly worried about how busy the machine is during coding, not just compiling. Every keystroke will invoke the generator pipeline, and that always felt like a waste of resources to me. But I guess I need to learn to live with it :D

@sschmid
Copy link
Owner

sschmid commented Jul 5, 2023

Followup performance test result:
2000 components in 29ms. I'll take that :D

@sschmid
Copy link
Owner

sschmid commented Jul 6, 2023

Bummer: I tried the current state in older Unity LTS versions, only 2022.3 worked 😭 I also downgraded to netstandard2.0 to support VS code. The irony that new stuff works in Rider but not VS 🤪
I can imagine it's gonna be a tough sell for many to upgrade their project to Unity 2022.3. I'll see if I can find a solution to support older versions too

@sschmid
Copy link
Owner

sschmid commented Jul 6, 2023

I can share a little bit about the version drama based on my current testing:

1st version clash:

  • I typically use netstandard2.1 as a default, it also works with Unity (kind of)
  • testing the genrstors in Rider worked well, but VS code requires netstandard2.0 💥
  • so I downgraded back to netstandard2.0

2st version clash:

  • IIncrementalGenerator have been add in Microsoft.CodeAnalysis.CSharp 4.0 (I need those, must have)
  • Unity docs tell we must use Microsoft.CodeAnalysis 3.8 💥
  • that seems to be true until Unity 2022.3 LTS (I only test with LTS versions)
    • luckily starting from this version I was able to use Microsoft.CodeAnalysis.CSharp 4.0
  • however, latest is Microsoft.CodeAnalysis.CSharp 4.6, which doesn't work in Unity
    • I tested all versions in between and found Microsoft.CodeAnalysis.CSharp 4.1 is the latest you can use

So my current results:

  • use netstandard2.0, not latest
  • use Microsoft.CodeAnalysis.CSharp 4.1, not latest
  • only works in Unity 2022.3 LTS and later

🤷‍♂️ but fair enough

@sschmid
Copy link
Owner

sschmid commented Jul 6, 2023

Question:

While updating the output of the generated code I was thinking about updating it to use Nullable.

Example: imagine we have a unique UserComponent, so we typically get a context method that gets the single entity and then returns the UserComponent

var user = context.GetUser();
public static UserComponent GetUser(this MyApp.MainContext context)
{
    var entity = context.GetUserEntity();
    return entity != null ? entity.GetUser() : null;
}

Using nullable, it would return UserComponent? and could look like this:

  public static UserComponent? GetUser(this MyApp.MainContext context)
  {
      return context.GetUserEntity()?.GetUser();
  }

Nullables would also allow me to remove throwing exceptions in the generated (e.g. when you try to get a component that has not been set) and move the responsibility to the consumer.
I could also remove Has methods like HasUser() and only rely on GetUser() which returns a nullable UserComponent?

Will nullable break your projects?

@rubenwe
Copy link

rubenwe commented Jul 6, 2023

Followup performance test result: 2000 components in 29ms. I'll take that :D

When you get closer to completion or after it's in I can also take a look if I can help and find a few extra places to tweak - but honestly 29ms is probably good enough ;D

Nullables would also allow me to remove throwing exceptions in the generated (e.g. when you try to get a component that has not been set) and move the responsibility to the consumer.

Please do that! This would allow us to use proper pattern expressions as filters in reactive systems and would simplify code in a lot of places!

So something like this:

protected override bool Filter(GameEntity entity) =>
    entity.hasEquipmentChestType &&
    entity.equipmentChestType.Value is ChestType.Small or ChestType.Big && 
    entity.isInstantOpen;

Could at least become something like this:

protected override bool Filter(GameEntity entity) => entity is
{
    equipmentChestType: { Value: ChestType.Small or ChestType.Big },
    isInstantOpen: true
};

And even better, for newer version of C#:

protected override bool Filter(GameEntity entity) => entity is
{
    equipmentChestType.Value: ChestType.Small or ChestType.Big,
    isInstantOpen: true
};

@sschmid sschmid mentioned this issue Jul 6, 2023
@Ants-Aare
Copy link

@rubenwe omg that last code snippet feels fresh, I like it a lot. This entitas version is breaking anyways, so it makes sense to include multiple changes and the cost of implementing it right now is fairly low.

@sschmid
Copy link
Owner

sschmid commented Jul 11, 2023

More updates and infos on the route to source generators:

In a simple setup with source generators and Unity, the source generator will be active in each asmdef. This is convenient because you can split up your game in multiple sub projects with asmdefs and all works as expected.

Example:

Split up the app into Contexts, FeatureOne and FeatureTwo asmdefs. FeatureOne and FeatureTwo are completely decoupled from each other, but both depend on Contexts, because they need MainContext in the systems but also to add the context attribute to their components.

MyApp
└─ `Contexts` asmdef with `MainContext`
   ├─ `FeatureOne` asmdef (with some components and systems using `MainContext`)
   └─ `FeatureTwo` asmdef (with some components and systems using `MainContext`)

Contexts could contain context definitions like

namespace MyApp
{
    partial class MainContext : Entitas.IContext { }
}

The source generator will generate the rest of the classes, like the other part of the partial MainContext, but also the context specific classes like ComponentIndex, Entity, Matcher. All of those will be in the resulting Contexts.dll.

FeatureOne and FeatureTwo could contain component definitions like

using Entitas;
using Entitas.Generators.Attributes;
using MyApp;

namespace MyFeature
{
    [Context(typeof(MainContext))]
    public sealed class PositionComponent : IComponent
    {
        public int X;
        public int Y;
    }
}

The source generator will generate extensions like AddPosition(), ReplacePosition() and RemovePosition and all the other things we're familiar with, like Matchers etc.


However, there are also some files that have been generated by Jenny so far, that only exists once in the app, like the Feature class that is a sub class of Systems. This class has been generated, because it contains compiler flags like ENTITAS_DISABLE_VISUAL_DEBUGGING, UNITY_EDITOR, DEVELOPMENT_BUILD in various combinations. The Unity project needs this file as source code, so it can compile this class based on the current compiler flags and therefore could not be part of Entitas.dll.

As described above, the source generator is in every asmdef, so it cannot generate the Feature class anymore, because each sub project would generate this class. But it must be unique.

I actually moved the Feature class to Entitas now because I plan to distribute Entitas using openupm which allows me to distibute packages with the Entitas source code instead of dlls. If you prefer to consume Entitas as a dll, you would need to add the Feature class manually to you game.


On another note, the Contexts class that you probably know from Contexts.sharedInstance is probably going away. It was always meant as a convenience class for easy access, but every codebase is different, and some people might prefer other solutions like dependency injection. Since you can define your contexts in code now instead of the Jenny.properties file, you can decide how you want you access the context. If you like a static instance you can simply implement a context like this

namespace MyApp
{
    partial class MainContext : Entitas.IContext
    {
        public static MainContext Instance
        {
            get => _instance ??= new MainContext();
            set => _instance = value;
        }

        static MainContext _instance;
    }
}

and later use it like this

var entity = MainContext.Instance.CreateEntity();

You're in full control of your contexts now. Of course, you can also just recreate the Contexts class yourself, if you prefer that.

The Contexts class also used compiler flags to add ContextObserver when running in Unity, so you can see contexts in the hierarchy. Since the Contexts class in gine now, I added a new extension method for contexts that you can call yourself:

context.CreateContextObserver();
#if ENTITAS_DISABLE_VISUAL_DEBUGGING || !UNITY_EDITOR
[System.Diagnostics.Conditional("false")]
#endif
public static void CreateContextObserver(this IContext context)
{
    Object.DontDestroyOnLoad(new ContextObserver(context).gameObject);
}

This call will be stipped out when compiling using the ConditionalAttribute. Only when UNITY_EDITOR or !ENTITAS_DISABLE_VISUAL_DEBUGGING the code remains and a ContextObserver will be created.


And finally, to follow up on previous thoughts on nullables and other changes, I will try to keep the new generated code as similar to the current version as possible to not break too much (see #1069 (comment)).

@sschmid
Copy link
Owner

sschmid commented Jul 14, 2023

I just finished event component and system generation, and finally fixed a subtle bug, where listener entities might leak.

Example of the old code:
https://github.com/sschmid/Match-One/blob/7d490c6d236c478f62b83a579724d730d2c9b88f/Assets/Generated/Game/Components/GamePositionListenerComponent.cs#L76-L84

Example of the new code:
https://github.com/sschmid/Entitas/blob/roslyn-source-generators/tests/Entitas.Generators.Tests/snapshots/ComponentGeneratorTests.EventNamespacedComponent%23MyFeature.MyAppMainAnyEventNamespacedAddedListenerComponent.g.verified.cs#L36-L51

Basically, when unsubscribing from event the listener can be auto cleanup up when empty. But it only removed the component, leaving the entity without any components. Now the listener entity will be destroyed when there are no components left on the entity

@sschmid
Copy link
Owner

sschmid commented Jul 16, 2023

Hello!

Does anyone know how to set and use global options for generators (or analyzers)?

I described my previous solution here: #1069
but I have some open question:

What I'm currently doing:

  • add some key-value pairs to .editorconfig see code
    • please irgnore the fact that they are commented in this commit
  • create an OptionsProvider: see code
  • use it to check if a generator should run or be skipped: see code
    • this uses a helper class EntitasAnalyzerConfigOptions which is calling this: see code
optionsProvider.GetOptions(syntaxTree).TryGetValue(key, out var value) 

Question

Why do I need to pass in a syntaxTree?
I initially tried to use the GlobalOptions, but this didn't work and the gobal options did not contain the keys from .editorconfig

optionsProvider.GlobalOptions.TryGetValue(key, out var value)

It sounds like GlobalOptions would be the correct place to check? How can I add custom key-value pairs to GlobalOptions? Seems like .editorconfig is not the correct place?

Also, I don't always have a syntaxTree that I can pass in. I pass one in where ever I can, and it works, but e.g. when woking with the compilation like with initContext.CompilationProvider, I cannot pass a syntaxTree.

@sschmid
Copy link
Owner

sschmid commented Jul 17, 2023

I'm referring to this that I found in the docs: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#access-analyzer-config-properties

A generator is free to use a global option to customize its output. For example, consider a generator that can optionally emit logging. The author may choose to check the value of a global analyzer config value in order to control whether or not to emit the logging code. A user can then choose to enable the setting per project via an .editorconfig file:

mygenerator_emit_logging = true
[Generator]
public class MyGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // control logging via analyzerconfig
        bool emitLogging = false;
        if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("mygenerator_emit_logging", out var emitLoggingSwitch))
        {
            emitLogging = emitLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);
        }

        // add the source with or without logging...
    }

    public void Initialize(GeneratorInitializationContext context)
    {
    }
}

Unfortunately, context.AnalyzerConfigOptions.GlobalOptions.TryGetValue doesn't work for me, only when passing a syntaxTree

@sschmid
Copy link
Owner

sschmid commented Jul 17, 2023

Alternatively, this seems to work:

Add msbuild props to Directory.Build.props like

<ItemGroup>
  <CompilerVisibleProperty Include="entitas_generator_disable" />
</ItemGroup>

@sschmid
Copy link
Owner

sschmid commented Jul 17, 2023

... not in Unity of course 🙈

@sschmid
Copy link
Owner

sschmid commented Jul 17, 2023

One benefit you get with keeping the syntaxTree is that you could potentially configure the generator per component. Not sure if that's ever useful, but you can do it:

.editorconfig

[PositionComponent.cs]
entitas_generator.component.component_index = false
entitas_generator.component.context_extension = false
entitas_generator.component.context_initialization_method = false
entitas_generator.component.entity_extension = false
entitas_generator.component.entity_index_extension = false
entitas_generator.component.events = false
entitas_generator.component.event_systems_extension = false
entitas_generator.component.matcher = false

entitas_generator.context.component_index = false
entitas_generator.context.context = false
entitas_generator.context.entity = false
entitas_generator.context.matcher = false

@sschmid sschmid pinned this issue Jul 18, 2023
@sschmid
Copy link
Owner

sschmid commented Jul 19, 2023

Yay, all generators are complete. I updated my local MatchOne repo to give it a test run. Works great! I will share more soon!

@sschmid
Copy link
Owner

sschmid commented Jul 22, 2023

Completed all tasks from above. Will close.
Development continues on branch entitas-2.0-beta, which includes the new code generator + more

@sschmid sschmid closed this as completed Jul 22, 2023
@sschmid
Copy link
Owner

sschmid commented Aug 3, 2023

Update:

entitas-2.0-beta is now merged into main and I updated Match-One, so you can test and play around with Entitas 2.0-beta and the new dotnet source generators:

https://github.com/sschmid/Match-One/tree/entitas-2.0-beta

Development is not complete, things may change, but it's the first working version that I wanted to share to get feedback. Once Entitas 2.0 gets closer to release I will share more docs and upgrade guides. For now Match-One is the only "documentation" in form of a working project. Try it, break it and have fun :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

5 participants