Skip to content

Code Generator

Jesse Talavera-Greenberg edited this page May 4, 2019 · 43 revisions

Entitas Code Generator

The Code Generator generates classes and methods for you, so you can focus on getting the job done. It radically reduces the amount of code you have to write and improves readability by a huge magnitude. It makes your code less error-prone while ensuring best performance. I strongly recommend using it!

The Code Generator is flexible and can be customized to fit your needs. See the installation guide to learn how to use the code generator in a pure C# or Unity project.

The Code Generator Pipeline

The code generator system works as a pipeline that runs the following steps:

  1. Configuration: Read all configuration options from the .properties files in your project's root directory, and configure all plugins accordingly.
  2. Pre-Processing: Pre-process your code or environment, via plugins that implement IPreProcessor.
  3. Parsing and Caching: Parse your project's code into a format that the rest of your plugins can use.
  4. Data Extraction: Extract whatever data you want from your project and store it in a big array for use by code generators. The extraction is done by plugins that implement IDataProvider, and is stored in an array of instances or subclasses of CodeGeneratorData.
  5. Code Generation: The code is generated by any plugin that implements ICodeGenerator, and stored in instances of CodeGenFile. A CodeGeneratorData[] containing all data from all IDataProviders is made available for use as you see fit.
  6. Post-Processing: The generated code is post-processed by all plugins that implement IPostProcessor. The previously-generated CodeGenFiles are made available to post-processors and can be manipulated freely.
  7. Using Generated Code: Your code is now ready for use!

Let's go through these steps one at a time.

Configuration

All plugins are configured with standard .properties files. Most of the keys you'll see start with Jenny or Entitas, but you can add your own for custom plugins if you need it.

To use the parsed configuration, your plugins will need to implement IConfigurable, like so:

using System.Collections.Generic;
using DesperateDevs.Serialization;

public class MyConfigurablePlugin : ICodeGenerationPlugin, IConfigurable
{
    Dictionary<string, string> defaultProperties { get; }

    void Configure(Preferences preferences);
    // Other methods and properties omitted for brevity
}

TODO: Finish this section

Possible Use Cases

  • For kits containing pre-made IComponents or ISystems, configure which Contexts are used.
  • When C# attributes are unsuitable for your desired customization, such as for search paths or for fundamentally altering your plugin's behavior.
  • For providing sensitive API information to your plugins, if necessary.

TODO: Document IConfigurable TODO: Document how it's decided which .properties files to load

Tips

Key Names

Prepending key names with your project or company name to mitigate collisions with those of other projects, i.e. use YourCompany.CustomCodeGens.Contexts instead of Contexts.

Special Key Values

If you intend configuration values to be used as C# identifiers (e.g. when listing contexts) but also want to provide values with special meaning, give these values names that are not valid identifiers. Suppose, for instance, that you have a configuration that looks like this:

MyCompany.MyProject.SupportedContexts = Game, Input, Ui, Config
Entitas.CodeGeneration.Plugins.Contexts = Game, Input, Ui, Config

If you want to denote support for all contexts in your project, you could add a special value named All that generates code for all contexts, like so:

MyCompany.MyProject.SupportedContexts = All

But Entitas generates lots of code that contains identifiers derived from context names, such as GameContext or InputEntity or UiMatcher. If you have a context that happens to be named All, this can complicate your life; you'd have to either handle errors or disambiguate it.

MyCompany.MyProject.SupportedContexts = All
Entitas.CodeGeneration.Plugins.Contexts = Game, Input, Ui, Config, All

Should this custom plugin support all contexts? Or just the context named All? If you have to write code to answer that question, you will have to deal with any bugs or edge cases that occur as a result. Consider using a special name that's not a valid C# identifier, like anything prepended with $:

MyCompany.MyProject.SupportedContexts = $All
Entitas.CodeGeneration.Plugins.Contexts = Game, Input, Ui, Config, All

Pre-Processing

Pre-processors implement the IPreProcessor, and are executed before your project's source code is analyzed. They're useful for validating assumptions or enforcing prerequisites for your other plugins. If these assumptions fail or you can't enforce prerequisites, you can stop dependent plugins from executing here.

Possible Use Cases

  • Search for or download external programs or libraries that your code generator relies on.
  • Verify the existence of files that your code generator reads, or that a file you intend to generate doesn't already exist.
  • Validate the format of complicated configuration options.

Code Parsing

After your plugins have been configured and your pre-processors run, it's time to load your code. There's no dedicated pipeline stage for this; all plugins are expected to call a function that either parses your code or loads a cached version. Here's how that works.

Caching

Parsing source code is fairly slow. But the Jenny pipeline provides a cache that is shared among all plugins. To use it, ensure your plugins implement ICachable and use the default property implementation of objectCache, like so:

using System.Collections.Generic;
using DesperateDevs.CodeGeneration;

public class MyCustomPlugin : ICodeGenerationPlugin, ICachable
{
    public Dictionary<string, object> objectCache { get; set; }
    // Other methods and properties omitted for brevity
}

The cache is mostly used for reading the parsed project, but can be used to store the results of any expensive computation. To access the cached project, use the PluginUtil class in the DesperateDevs.Roslyn.CodeGeneration.Plugins namespace like so:

TODO Finish this section

Data Extraction

TODO

Code Generation

TODO

Post-Processing

TODO

Final Product

Your code is now ready for use! But, as with other code generation systems, there are caveats:

Manual Changes

Manual changes to generated code will be lost the next time the code generator is run. If you need to augment the output of a code generator, consider making a new one.

Version Control

Generated code exists as plain text files and is thus safe for storage in any version control system. However, the previous warnings about manual changes still hold, thus complicating merge conflicts. We see two main workflows for handling this:

Track Generated Code

This is the simplest option, and may be sufficient for solo projects. As a consequence, small changes to your code (such as introducing a handful of components) can result in disproportionately large commits, especially if your project uses Unity and its .meta file system.

Don't Track Generated Code

For this workflow, you would not track generated source code; instead, you would expect everyone on your team to be able to generate a local copy at will, as they would for compiled binaries. This is the best option in the long run, but if you're using the commercial version of Entitas then every member on your team will need a license. Additionally, if your project is open source then this workflow may hinder user's ability to contribute (since you may have to help them set up Entitas and Jenny).

Directory Size

Jenny usually generates a large number of files. If your project uses Unity, double this to account for .meta files. If your development environment scans your code base in real time (e.g. for autocomplete or diagnostics), the sheer number of generated files may slow down or break certain features.

Unity MonoBehaviours

MonoBehaviours and other Unity assets deserve special mention. For every asset (source file, texture, etc.) in your project, Unity generates a corresponding .meta file containing metadata. This .meta file assigns a randomly-generated GUID to its corresponding asset for the purpose of declaring relationships between assets. If a file is deleted outside of Unity but then re-added, it will have a different GUID, and all references to it in other assets (e.g. prefabs or animations) will be lost. For this reason, if you use custom code generators to generate MonoBehaviours, you must take extra steps to ensure that their .meta files are preserved between generations. You will need to write a custom plugin for this.

Included Plugins

Jenny comes with a lot of useful plugins, including those needed for Entitas. All of these are enabled by default unless otherwise specified. Descriptions of each follow.

Pre-Processors

TODO

Data Providers

TODO

Code Generators

TODO

Post-Processors

TODO

Doctors

TODO

Customizing and extending the Code Generator

You can easily implement your own generators by implementing one of the available ICodeGeneratorInterfaces:

  • ICodeGeneratorDataProvider: Returns CodeGeneratorData[] to provide information for CodeGenerators
  • ICodeGenerator: Takes CodeGeneratorData and returns CodeGenFile[] that contain all generated file contents
  • ICodeGenFilePostProcessor: Takes CodeGenFile[] to add modifications to any generated files

But for most projects the provided generators will cover all needs.

See this tutorial and this page for extra help setting up custom code generators for your project.

Available Generators

Entitas already comes with all Generators that are vital for clean and simple coding using Entitas framework. You can configure them and toggle them on/off using the Entitas>Preferences Editor Window within Unity or manually editing the Entitas.properties file in your root directory.

ComponentEntityCreator

This Generator provides you with a simple interface for accessing and manipulating components on Entities. The outcome depends on whether your Component is a:

  • flag Component without any fields (e.g. MovableComponent)
  • standard Component with public fields (e.g. PositionComponent)

Flag Component (e.g. MovableComponent)

[Game]
public class MovableComponent : IComponent {}

[Game, FlagPrefix("flag")]
public class DestroyComponent : IComponent {}

You get

GameEntity e;
var movable = e.isMovable;
e.isMovable = true;
e.isMovable = false;

e.flagDestroy = true;

Standard Component (e.g. PositionComponent)

[Game]
public class PositionComponent : IComponent {
    public int x;
    public int y;
    public int z;
}

You get

GameEntity e;
if(e.hasPosition)
    var position = e.position;
else
    e.AddPosition(x, y, z);
e.ReplacePosition(x, y, z);
e.RemovePosition();

ComponentContextCreator

This Generator helps you to manage Components, that are meant to exist once per Context. Think of a Singleton-Component that only exists within a Context instead of statically. You can mark Single-Instance Components by using the [Unique]-Attribute. The output depends on, where your Component is a:

  • Unique flag Component without public fields (e.g. AnimatingComponent)
  • Unique standard Component with public fields (e.g. UserComponent)

Single flag component (e.g. AnimatingComponent)

[Game, Unique]
public class AnimatingComponent : IComponent {}

You get

// extensions from `ComponentEntityCreator` are also available

GmeContext context;
GameEntity e = context.animatingEntity;
var isAnimating = context.isAnimating;
context.isAnimating = true;
context.isAnimating = false;

Single standard component (e.g. UserComponent)

[Game, Unique]
public class UserComponent : IComponent {
    public string name;
    public int age;
}

You get

// extensions from `ComponentEntityCreator` are also available

GameContext context;
GameEntity e = context.userEntity;
if(context.hasUser)
    var name = context.user.name;
else
    context.SetUser("John", 42);
context.ReplaceUser("Max", 24);
context.RemoveUser();

ComponentGenerator

[todo]

ComponentLookupGenerator

[todo]

ContextAttributeGenerator

[todo]

ContextGenerator

[todo]

ContextsGenerator

[todo]

EntityGenerator

[todo]

MatcherGenerator

[TODO]

Adding your own Generator

  1. Create a new class that implements e.g. ICodeGenerator and save it in an Editor folder
  2. Your Assembly-CSharp-Editor.dll can now be considered a plugin, because it contains your new generator
  3. Add Assembly-CSharp-Editor to Jenny.Plugins field in Preferences.properties file
  4. Add Assembly-CSharp-Editor.dll folder path to Jenny.SearchPaths. This is usually Library/ScriptAssemblies
  5. Entitas preferences window will contain name of your custom code generator in one of the generator dropdowns. Enable it and generate.

Fixing compilation errors

The Code Generator is based on runtime reflection. The project has to compile before you can generate. This is not an issue when you creating new components, however when it comes to changing or deleting components, your code might stop compiling. Here is a list of recipes how you can avoid bigger hassle while changing and deleting components.

Renaming component fields

Use rename refactoring of your IDE and generate. Things should not break because fields only affect method parameter names in the generated methods.

Renaming component

Use rename refactoring of your IDE and also rename the existing methods, setters and getters in the generated class and generate.

Adding new fields to a component

Add the new fields and generate. This will result in compile errors because some methods now expect more parameters, e.g. e.AddXyz() and e.ReplaceXyz(). You'll have to update all the places where you call these methods.

Removing fields from a component

This will directly lead to compilation errors because at least the generated class is using them. In this case you can just comment out the implementation of the affected methods, e.g e.AddXyz() and e.ReplaceXyz(). After that, generate again.

Deleting a component

Delete the component and the generated class completely, then remove the component under componentTypes in ComponentIds (might have a prefix if you changed the context name). After that, remove/change your usages of the component and generate again. You may need to close unity and open again. Also make sure your code compiles when you remove the component, like remove all references to it as well.

Renaming context names

Use rename refactoring of your IDE to rename the ContextAttribute class name first, then generate.

Clone this wiki locally