among many other features, Svelto.ECS 3.3 introduces a new shiny and finally usable filters API. The previous one had the bad habit to get very awkward very fast with the growth of complexity. The new API learns from the previous mistakes and introduces a ton of sweet features.
To recap what filters are useful for:
Svelto.ECS memory layout is driven by the concept of groups. Any subset of entities is found in a specific group and for this reason, groups usually model the state of the entity. Each entity can be found only in one specific group, but this is often not a problem as GroupCompounds are designed to model multiple states, to be specific any combination of up to 4 different states. However in some situations, Groups can either get too awkward or fragment the memory too much, and in these cases, Filters come to the rescue as an alternative way to create a subset of entities. Filters are particularly powerful because can be used together with GroupCompounds. This means that thanks to filters and group compounds entities can be found at the same time in different subsets.
The most important change in the new API is probably the fact that the user now doesn’t need anymore to be aware of the groups where the entities are in order to use filters, making filters completely independent from the groups under the users point of view. The Svelto patterns assume that filters are iterated first and filters will contain the information about in which group the entities are found.
A good example of the new Svelto API can be seen in the shiny new Doofuses Stride example.
If you already used the previous API (probably you didn’t :P) the first thing you’d notice is that now Filters can be Persistent or Transient. While transient filters are cleaned between each entities submission, the more useful persistent filters are held and managed directly by the framework. If an entity is removed, the filters where the entity is found will be automatically updated.
In the new Stride demo, we had the necessity to instance a prefab many times, but demo groups do not represent the entity mesh, so in order to know which mesh the entity uses, filters are used.
The following engine has the responsibility to automatically and generically update filters according to the stride mesh found in the svelto stride entity.
class AddStrideEntityToFiltersEngine : IReactOnAddEx<StrideComponent>, IQueryingEntitiesEngine { public void Add((uint start, uint end) rangeOfEntities, in EntityCollection<StrideComponent> collection, ExclusiveGroupStruct groupID) { var (buffer, entityIDs, _) = collection; //Fetch the new Svelto.ECS filters var sveltoFilters = entitiesDB.GetFilters(); int lastEntity = -1; ref var cachedFilter = ref _default; //for each entity added in this submission phase for (uint index = rangeOfEntities.start; index < rangeOfEntities.end; index++) { //get the Stride entityID that will be instanced multipled times var entity = (int)buffer[index].instancingEntity; //if it doesn't match last one used, let's fetch the filter that is linked to this ID if (lastEntity != entity) { //I use the stride entityID as filter ID cachedFilter = ref sveltoFilters.GetOrCreatePersistentFilter<StrideComponent>(entity, StrideFilterContext.StrideInstanceContext); lastEntity = entity; } //add the current entity instance to the filter linked to the prefab entity that will be instanced cachedFilter.Add(entityIDs[index], groupID, index); } } public void Ready() { } public EntitiesDB entitiesDB { get; set; } EntityFilterCollection _default; }
Note that since the user can choose any ID as filter ID, I decided to use as filter ID the ID of the Stride Entity to instance, which becomes very convenient in the following engine that has the responsibility to fetch the list of instancing matrices per Stride Entity and update it with the new transformations from the Svelto.ECS entities:
/// <summary> /// Iterate all the entities that have matrices and, assuming they are stride objects, set the matrices to the /// matrix to the Stride Entity /// </summary> [Sequenced(nameof(StrideLayerEngineNames.SetTransformsEngine))] class SetTransformsEngine : IQueryingEntitiesEngine, IUpdateEngine { public SetTransformsEngine(ECSStrideEntityManager ecsStrideEntityManager) { _ECSStrideEntityManager = ecsStrideEntityManager; } public EntitiesDB entitiesDB { get; set; } public void Ready() {} public string name => this.TypeName(); public void Step(in float deltaTime) { if (entitiesDB.GetFilters() .TryGetPersistentFilters< StrideComponent>(StrideFilterContext.StrideInstanceContext, out var filters) == false) return; //iterate all the filters linked to the context StrideInstanceContext foreach (ref var filter in filters) { var useFilterIDAsEntityID = (uint)filter.combinedFilterID.filterID; //the id of the filter is the id of the instancing entity. var matrices = _ECSStrideEntityManager.GetInstancingTransformations(useFilterIDAsEntityID); //each batch of instances needs to have its own array of matrices //in order to allocate less often, we allocate more than needed filter.ComputeFinalCount(out var entitiesCount); if (matrices.Length < entitiesCount) Array.Resize(ref matrices, HashHelpers.Expand(entitiesCount)); //each filter can spread over multiple groups, so we iterate the filters per group int matrixIndex = 0; foreach (var (indices, currentGroup) in filter) { var indicesCount = indices.count; //we get the matrices of this group var (matrixComponents, _, _) = entitiesDB.QueryEntities<MatrixComponent, StrideComponent>(currentGroup); //and we copy the values to the matrices array using the filters. for (var i = 0; i < indicesCount; ++i) { matrices[matrixIndex++] = matrixComponents[indices[i]].matrix; } } //finally we set the array of matrices in Stride. remember the filter id was the entityID _ECSStrideEntityManager.SetInstancingTransformations(useFilterIDAsEntityID, matrices, entitiesCount); } } readonly ECSStrideEntityManager _ECSStrideEntityManager; }
if we delete some code that is necessary to make Stride render the instanced entities, we can see how the new Svelto.ECS filters API pattern works when it’s time to iterate filtered entities:
//fetch the filters linked to the components to iterate. Note that no groups knowledge is necessary anymore\ //Filters are not identified only by the ID, but also by a context, so it will easier to use custom IDs if (entitiesDB.GetFilters() .TryGetPersistentFilters< StrideComponent>(StrideFilterContext.StrideInstanceContext, out var filters) == false) return; //in this case we are not fetching a specific filter, but actually iterating all the filters linked to the context foreach (ref var filter in filters) { var useFilterIDAsEntityID = (uint)filter.combinedFilterID.filterID; //entities found in each filter can lie across several groups, so each filter will tell you exactly from which group fetching the components foreach (var (indices, currentGroup) in filter) { //fetch the components from the right group var (matrixComponents, _, _) = entitiesDB.QueryEntities<MatrixComponent, StrideComponent>(currentGroup); //operate only on the filtered entities, this case indices[i] will return the index of the filtered entity in the matrixComponent group array. for (var i = 0; i < indices.count; ++i) { matrices[matrixIndex++] = matrixComponents[indices[i]].matrix; } } } } }
thanks to the filters and these two engines I can add any kind of Svelto Entity linked to a specific mesh without needing to worry about how to organise the memory layout to accommodate entity states and meshes combinations.