sschmid / Entitas

Entitas is a super fast Entity Component System (ECS) Framework specifically made for C# and Unity
MIT License
7.17k stars 1.11k forks source link

Entitas packages support #977

Closed sschmid closed 1 year ago

sschmid commented 3 years ago

Hi,

I'd like to build new game features as packages. In Unity's package manager terms, a package is basically a separat project that also compiles into a separat dll. Instead of having all sources in Assembly-CSharp.csproj, we can split up projects into multiple smaller packages within Unity using Unity's Assembly definitions: https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html

This workflow can have multiple benefits: Compile times may improve because code changes may only require to recompile a package instead of the complete project. Decoupled packages can lead to more reusable code. Packages can be shared easily. Sharing code via packages leads to small git repositories. Overall, project size and iteration times may improve (and therefore fun :))

In order to benefit from packages, Entitas needs an update.

Here are a few things I have in mind that need to be updated in order to have a great package experience:

  1. Remove the use of partial classes
  2. Add proper namespace support
  3. Update roslyn code generator to support solutions and multiple projects

And while we're at it:

  1. Use official C# naming conventions

C# Coding Conventions https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions

Naming Guidelines https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines

There will probably more things I haven't thought of yet. If you can think of something or have any kind of question / suggestions / feedback, please comment. I'd like to get as much input from you as possible, because this might result in a breaking change and I'd like to have a smooth transition.

1. Partial classes

The generatde code uses partial classes. Partial classes is a C# concepts that allows us to split a class into multiple files on disk. However, this only works if all those files are in the same project and compiled into one dll. You cannot extend classes from another assembly using partial classes. You can however use C# extension methods.

Suggestion 1:

Replace all use of pratial classes with C# extension methods.

public class MyStringComponent : IComponent
{
    public string Value;
}

This will result in something like this

public static class MyStringEntityExtension
{
    public static MyStringComponent GetMyString(this Entity entity)
    {
        return (MyStringComponent)entity.GetComponent(MyStringComponentIndex.Value);
    }

    public static bool HasMyString(this Entity entity)
    {
        return entity.HasComponent(MyStringComponentIndex.Value);
    }

    public static Entity AddMyString(this Entity entity, string newValue)
    {
        var index = MyStringComponentIndex.Value;
        var component = entity.CreateComponent<MyStringComponent>(index);
        component.Value = newValue;
        entity.AddComponent(index, component);
        return entity;
    }

    public static Entity ReplaceMyString(this Entity entity, string newValue)
    {
        var index = MyStringComponentIndex.Value;
        var component = entity.CreateComponent<MyStringComponent>(index);
        component.Value = newValue;
        entity.ReplaceComponent(index, component);
        return entity;
    }

    public static void RemoveMyString(this Entity entity)
    {
        entity.RemoveComponent(MyStringComponentIndex.Value);
    }
}

Old vs new

// Old
var text = entity.myString.Value;

// new
var text = entity.GetMyString().Value;

In order to keep code completion support, we still need an Entity class per context.

namespace Game // <- Namespace is context name
{
    public sealed class Entity : Entitas.Entity
    {
    }
}

Matcher will also need to change

var group = _contexts.Game.GetGroup(MyStringMatcher.Instance);
namespace Game
{
    public sealed class MyStringMatcher : Entitas.Matcher<Entity>
    {
        public static MyStringMatcher Instance => _instance ?? (_instance = new MyStringMatcher());

        static MyStringMatcher _instance;

        MyStringMatcher()
        {
            _allOfIndices = distinctIndices(new[] {MyStringComponentIndex.Value});
            componentNames = ComponentHelper.ComponentNames["Game"];
        }
    }
}

ComponentsLookup is al class per context that maps component to a component index

public static class GameComponentsLookup
{
    public const int Asset = 0;
    public const int Camera = 1;
    public const int Destroyed = 2;

    // ... etc
}

This needs an update, too. Packages should probably already contains the generated code, like entities, matchers and component index consts. Generated methods like Add() and Replace() as well as the matchers use a variable that represents the index of a component. When precompield in a package, we cannot know the index at build-time, because we don't not how many other packages will be used that may also contain Entitas code and therefore add new components. A solution could be to delay this until runtime and scan all components during app start and then set the index of a new generated class like

namespace Game
{
    public static class MyStringComponentIndex
    {
        public static int Value;
    }
}

2. Namespaces

Packages encourages the use of namespaces as each package might want to have it's own namespace. Namespace support is currently bad. Applying Solution 1 allows for fixing this.

Suggestion 2

This is where I'm currently at. I don't have a full answer yet, but I will continue working on this. Keep in mind, a component can be in multiple contexts.

namespace Sandwich
{
    [Game /*, other contexts */]
    public class StandardComponent : IComponent
    {
        public string Value;
    }
}
namespace Game.Sandwich // or Sandwich.Game
{
    public static class StandardEntityExtension
    {
        public static Sandwich.StandardComponent GetStandard(this Entity entity)
        {
            return (Sandwich.StandardComponent)entity.GetComponent(StandardComponentIndex.Value);
        }

// ...

I need to try more, I'm not sure on the namespace of the generated code yet.

3. Update roslyn code generator to support solutions and multiple projects

While developing multiple packages in one Unity project we need to be able to generate code for each project to a separat folder. The generator should also be aware of all other projects in the solution in order to resolve types correctly.

Feedack

If you have ideas / suggestion / or any kind of feedback let me know.

sschmid commented 3 years ago

This update will touch multiple parts of Entitas and more concrete task will emerge over time I think. I will drop more ideas and thoughts here. Another I just had is to extract the code generation part out into template.txt files to make it way easier to customize the generator output to your requirements. Basically the idea is to have template files and certain placeholder variable, so you can customize various things without touching the code like in some template text file

public sealed class ${Component.Name} ...
JesseTG commented 3 years ago

I think ReplaceComponent should be named SetComponent instead; the semantics are clearer. In fact, while we're at it, if we have a SetComponent method then why do we need an AddComponent method?

JesseTG commented 3 years ago

Also:

This update will touch multiple parts of Entitas and more concrete task will emerge over time I think. I will drop more ideas and thoughts here. Another I just had is to extract the code generation part out into template.txt files to make it way easier to customize the generator output to your requirements. Basically the idea is to have template files and certain placeholder variable, so you can customize various things without touching the code like in some template text file

public sealed class ${Component.Name} ...

This is basically exactly what T4 does. So I think you should add T4 support instead of making some home-grown templating system. I've written a few custom codegens using T4 and it...well, it's wonderful.

T4 is actually very easy; it's literally plain text with some inline C#. You can generate whatever you want with T4; in practice it's mostly C# source code but that's not a requirement.

sschmid commented 3 years ago

@JesseTG Haha, just as a last minute edit, I removed my notes on SetComponent(). I was also thinking to combine them. Add() and Replace() almost do the same thing. Let's see if there are usecases where that would break sth

sschmid commented 3 years ago

I'll have a look what T4 can do and if it fits well. I'm happy to replace roslyn with sth that's easier to use

JesseTG commented 3 years ago

I'll have a look what T4 can do and if it fits well. I'm happy to replace roslyn with sth that's easier to use

T4 would be used alongside Roslyn. Roslyn is used to parse, compile, and generate C#; T4 is only used to generate text.

WeslomPo commented 3 years ago

This will be not an Entitas anymore :(.

I don't think that packages will be so useful - compare to how it need to rework all engine to work with (imho). Also we will lose entity.isSomething shortcut. I think simple namespace support (like add any namespace to generated files) is sufficient. I really need a way to create a different code generators for my use (like other events systems, destroy systems and so on).

sschmid commented 3 years ago

@WeslomPo Adding package support will not require you to use packages or change your workflow. It will however still result in some breaking changes in the generated code. A migration assistant should take care of updating all or at least most of the existing code. I also have a few custom code generators and I want to keep the possibility to modify or extends the code generators

RedDude commented 2 years ago

I agree in change any api need to have this feature. And during my life as entitas developer I did stumble with this more times that I wish for. Entitas Redux, Genesis already support solutions and multiple projects, and many times I did consider migrate to it because of that.

sschmid commented 1 year ago

I have updates on this that you can follow here #1005 Will close this issue in favour of #1005