Skip to content

Custom Code Generator Tips And Tricks

FNGgames edited this page Feb 23, 2019 · 1 revision

This page assumes that you've been through the code generator tutorial here and you understand the basic workflow for creating a new code generator and adding it to the Jenny as a plugin.

Here I am just going to list a few tips and tricks as a reference guide for how a few possibly desirable things can be acheived. This page may be added to over time as we think of more things we might want to do with the code generator.

Use an existing data provider

In some cases you might want to simply use one of the existing data providers that comes with Entitas. For this example we will use the ComponentDataProvider which is the DP responsible for finding all the IComponents in your solution. In the case of a built in provider you don't need any new attributes or data providers, you would only need the new code generator file.

To do this you would create the codegen file in MyNamespace.CodeGeneration.Plugins and filter it for the data type related to the data provider. In this case we want ComponentData which is the type generated by the ComponentDataProvider. You can find these types by looking at the data providers in the entitas source code.

To filter the incoming CodeGeneratorData[] in your generator you can simply use the .OfType<T>() query expression in the Generate() method, e.g.

public CodeGenFile[] Generate(CodeGeneratorData[] data)
{
    return data
        .OfType<ComponentData>() // this line filters the array by this type
        .Select(Generate) // this is the method that produces a CodeGenFile from incoming ComponentData
        .ToArray();
}

Getting argument data from attributes with non-parameterless contructors

You might want to pass additional data to your attributes to finesse the generated code. If your attribute has a constructor with arguments, Roslyn can access the semantics of these arguments (their name, order, typename etc) during code analysis. For this example we will create a Dependencies attribute that takes type arguments that indicate dependencies between a component and other components (e.g. EnemyComponent might require HealthComponent and SpeedComponent). You could use this data to auto-generate systems that ensure required dependencies are met.

namespace MyNamespace.CodeGeneration.Attributes
{
    [AttributeUsage(AttributeTargets.Class)]
    public class DependenciesAttribute : Attribute
    {
        private Type[] _dependencies;
        public List<Type> Dependencies { get { return new List<Type>(_dependencies); } }

        public DependenciesAttribute (params Type[] dependencies)
        {
            _dependencies = dependencies;
        }
    }
}

You could use this attribute to decorate components like so:

[Game, Dependencies(typeof(HealthComponent), typeof(DamageComponent))]
public sealed class EnemyComponent : IComponent { }

To pull this data into the code generator you would want to create the wrapper class with room to store the dependency data and then create a data provider that will read the data and store it. This might look like:

namespace MyNamespace.CodeGeneration.Plugins
{
    public class DependencyData : CodeGeneratorData
    {       
        public const string NameKey = "Dependency.Name";
        public const string MemberKey = "Dependency.Members";
        public const string DependenciesKey = "Dependency.Dependencies";

        public string Name
        {
            get => (string)this[NameKey];
            set => this[NameKey] = value;
        }

        public MemberData[] MemberData
        {
            get => (MemberData[])this[MemberKey];
            set => this[MemberKey] = value;
        }

        public string[] Dependencies
        {
            get => (string[])this[DependenciesKey];
            set => this[DependenciesKey] = value;
        }
    }
}

In your data provider you'd need to add a method that reads the attribute's constructor arguments and turns them into strings to store in the above class.

public CodeGeneratorData[] GetData()
{
    return DesperateDevs.Roslyn.CodeGeneration.Plugins.PluginUtil
        .GetCachedProjectParser(objectCache, _projectPathConfig.projectPath)
        .GetTypes()
        .Where(type => type.GetAttribute<DependenciesAttribute>() != null)
        .Select(type => new DependencyData
        {
            Name = type.Name,
            MemberData = GetData(type),
            Dependencies = GetDependencies(type)
        }).ToArray();
}

private string[] GetDependencies(INamedTypeSymbol type)
{
    return type
        .GetAttribute<DependenciesAttribute>() // get the attribute
        .ConstructorArguments[0] // get it's first and only contructor argument
        .Values // the argument is an array so we want Value(s) not Value here
        // the argument is a typeof(T) which is INamedTypeSymbol in Roslyn
        .Select(v => (v.Value as INamedTypeSymbol).Name) // we want its name
        .ToArray();         
}

Now we have an array of dependencies that we can use in a code generator to generate systems that ensure these dependencies are satisfied.

Clone this wiki locally