Skip to content

Custom Source Generator Attributes #85

@lgarczyn

Description

@lgarczyn

For context, we most use a BoundQuery wrapper over QueryDescription, that adds a few features:

  • A World is bound to each query
  • The world is locked on unlocked for each query, to avoid structural change during the query
  • All query types are checked on construction to see if they belong to that world (some types are available on multiple worlds, but it has to be defined)
  • By default, None<Destroyed> is added to the query
  • Utility methods are mirrored from QueryDescription, such as Count, Destroy Add and Remove

This is extremely useful. It even works as a higher order definition. For example our UI widgets simply define the query that will spawn them automatically.

However, it also has issues. Since our query api uses lambdas for flexibility, avoiding allocations is difficult. We also lose on some performance compared to the source generator.

Here's an simple example:

    private static readonly BoundQuery ContractQuery = PersistentQuery
                                                      .Base
                                                      .WithAll<ECContract>();

    public static void FailUnfinishedContract()
    {
        ContractQuery.Run((ref ECContract contract) =>
        {
            if (contract.State == ECContract.EState.InProgress)
            {
                contract.State = ECContract.EState.FinishedFailed;
                contract.FinishedTime = Time.timeAsDouble;
            }
        });
    }

I've attempted to mirror these features using the source generator, it's however been difficult. Adding default flags is impossible, and we have to specify and lock/unlock the world every time.

  • attributes are read by reading constructor parameters, so it's impossible to create an attribute with a "default" or hardcoded query params

  • only the first attribute that matches the name pattern is used, so "NotDestroyedAttribute" could not be composited with "NotAttribute"

I also wanted to be able to bind a world to a query, or to just add a wrapper around the query to lock the world, but neither seemed possible.

We end up with boilerplate around each generated queries:

    [Query]
    [All(typeof(ECResourceContainer), typeof(ECLink<ECShip>))]
    [None(typeof(ECFlag_Destroyed))]
    private void HandleContainer([Data]List<ResourceJobKey> queryBuffer, Entity containerEntity)
    {
      //...
    }

    private void HandleContainerQuery(List<ResourceJobKey> queryBuffer)
    {
        using WorldLock _ = new (ECSWorlds.Game);
        HandleContainerQuery(ECSWorlds.Game, queryBuffer);
    }

Ideally, this would look more like

    [NotDestroyedQuery]
    [GameWorld(lock: true)]
    [All(typeof(ECResourceContainer), typeof(ECLink<ECShip>))]
    private void HandleContainer([Data]List<ResourceJobKey> queryBuffer, Entity containerEntity)
    {
      //...
    }

This could be done by:

  • adding a virtual query argument attribute that can post-process the query
  • adding a virtual world attribute that can inject any world
  • adding some types of source-generated events with the world param to lock it and unlock it

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions