-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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 system works as a pipeline that runs the following steps:
-
Configuration: Read all configuration options from the
.properties
files in your project's root directory, and configure all plugins accordingly. -
Pre-Processing: Pre-process your code or environment, via plugins that implement
IPreProcessor
. - Parsing and Caching: Parse your project's code into a format that the rest of your plugins can use.
-
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 ofCodeGeneratorData
. -
Code Generation: The code is generated by any plugin that implements
ICodeGenerator
, and stored in instances ofCodeGenFile
. ACodeGeneratorData[]
containing all data from allIDataProvider
s is made available for use as you see fit. -
Post-Processing: The generated code is post-processed by all plugins that implement
IPostProcessor
. The previously-generatedCodeGenFile
s are made available to post-processors and can be manipulated freely. - Using Generated Code: Your code is now ready for use!
Let's go through these steps one at a time.
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
- For kits containing pre-made
IComponent
s orISystem
s, configure whichContext
s 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
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
.
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-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.
- 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.
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.
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
TODO
TODO
TODO
Your code is now ready for use! But, as with other code generation systems, there are caveats:
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.
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:
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.
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).
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.
MonoBehaviour
s 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 MonoBehaviour
s, 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.
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.
TODO
TODO
TODO
TODO
TODO
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 returnsCodeGenFile[]
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.
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.
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)
[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;
[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();
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)
[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;
[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();
[todo]
[todo]
[todo]
[todo]
[todo]
[todo]
[TODO]
- Create a new class that implements e.g.
ICodeGenerator
and save it in an Editor folder - Your Assembly-CSharp-Editor.dll can now be considered a plugin, because it contains your new generator
- Add
Assembly-CSharp-Editor
toJenny.Plugins
field in Preferences.properties file - Add Assembly-CSharp-Editor.dll folder path to
Jenny.SearchPaths
. This is usuallyLibrary/ScriptAssemblies
- Entitas preferences window will contain name of your custom code generator in one of the generator dropdowns. Enable it and generate.
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.
Use rename refactoring of your IDE and generate. Things should not break because fields only affect method parameter names in the generated methods.
Use rename refactoring of your IDE and also rename the existing methods, setters and getters in the generated class and generate.
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.
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.
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.
Use rename refactoring of your IDE to rename the ContextAttribute class name first, then generate.
Guides: Introduction - Installation - Upgrading - FAQ - Cookbook - Contributing
Need Help? Ask a question on Discord or create an issue.
- The Basics
- Concepts
- Architecture / Patterns