Hello. I’d be greateful for any feedback on the piece of code below:
[UpdateAfter(typeof(VoxelChunkVisibilitySystem))]
[BurstCompile]
public partial struct VoxelChunkDestructionSystem : ISystem
{
private EntityQuery _entitiesToDestroyQuery;
public void OnCreate(ref SystemState state)
{
_entitiesToDestroyQuery = new EntityQueryBuilder(Allocator.Temp).WithAll<VoxelChunkAttribution>().Build(state.EntityManager);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
NativeList<Entity> destroyedVoxelChunkEntities = new(100, Allocator.Temp);
NativeList<int3> destroyedVoxelChunkPositions = new(100, Allocator.Temp);
foreach (var (voxelChunk, localTransform, entity) in SystemAPI.Query<RefRO<VoxelChunk>, RefRO<LocalTransform>>().WithAll<VoxelChunk>().WithEntityAccess())
{
if (voxelChunk.ValueRO.visibilityAge > 0)
{
destroyedVoxelChunkEntities.Add(entity);
destroyedVoxelChunkPositions.Add((int3)round(localTransform.ValueRO.Position));
}
}
state.EntityManager.DestroyEntity(destroyedVoxelChunkEntities);
destroyedVoxelChunkEntities.Dispose();
for (int i = 0; i < destroyedVoxelChunkPositions.Length; ++i)
{
VoxelChunkAttribution voxelChunkAttribution = new VoxelChunkAttribution { position = destroyedVoxelChunkPositions[i] };
_entitiesToDestroyQuery.SetSharedComponentFilter(voxelChunkAttribution);
state.EntityManager.DestroyEntity(_entitiesToDestroyQuery);
}
destroyedVoxelChunkPositions.Dispose();
}
}
Basically, there are a lot of entities (hundreds/thousands) generated that correspond to each voxel chunk. To mark the entities that belong to a specific chunk I’m using a shared component (VoxelChunkAttribution). When a voxel chunk becomes invisible (visibilityAge > 0 in my case) I need to destroy that voxel chunk along with all those entities related to it.
My question is: does the whole thing make sense? Am I doing it right or is there a better approach?
So I was doing something like this at one point. I recently changed to a component based destruction system which seems much more performant.
How it works is instead of adding them to native lists, you just add a component to the thing you want to destroy. Then in another system you use an EntityQuery to destroy everything with that component. The benefit here is less allocations and you can order the destroy system after whatever systems add the Destroy component.
public void OnCreate(ref SystemState state)
{
//Better way to create an EntityQuery as it is Disposed for you
destroyQuery = state.GetEntityQuery(
new EntityQueryDesc
{
All = new ComponentType[] { typeof(DestroyNow) },
});
}
public void OnUpdate(ref SystemState state)
{
var destroyEntities = destroyQuery.ToEntityArray(Allocator.Temp);
state.EntityManager.DestroyEntity(destroyEntities);
destroyEntities.Dispose();
}
hmm… I can’t agree with that. As far as I understand, adding the “Destroy” component will change the entity’s archetype, which means relocation into a different chunk. To me this operation seems much more costly then adding entity id to the temporary native array (which is what I am doing per-entity that I need to destroy)
It seems to have better performance for my game but probably isn’t a silver bullet. Maybe I will start running into bottlenecks with it as I go
Problems for another day haha
At very least I think one way to improve your current solution would be to use EntityCommandBuffer. Then you can put all this logic into bursted (even parallel) jobs and create only a single syncpoint when you call ECB.Playback.
If you have hundreds of thousands of voxels to check then parallel jobs will significantly improve performance, especially if you batch it to only check a portion of the voxels each frame.
Does it work? My historical problem with using EntityManager directly is it causes “structure changed” (or something like that, couldn’t remember) errors on subsequent jobs. That’s why I always use ECB when making structural changes. ECB also has method of destroying entities by query.
When destroying entities using EntityManager directly you obviously have to make sure that your other jobs, that are iterating through entity queries or whatever have finished first. You basically have to establish your own sync point.
ECB indeed can destroy entities by query, but still it will do so when it is played back, which happens on the main thread using basically those same EntityManager calls.
EntityManager completes the necessary jobs (sync point) itself with that call.
Could be component safety handles for ComponentLookup, ComponentTypeHandle, etc. getting invalidated after structural changes. You would need to use ComponentLookup.Update/ComponentTypeHandle.Update/etc. after the structural changes to get valid safety handles.
1 Like