Hello!
As long as Unity themself dont know how to use their own package can we collectively make perfect guidelines and best practices for the whole pipeline?
Every day people making new posts about how to make basic stuff, how to refactor the code, how to write scripts etc…
Purpose of this topic is to share sample code.
Like best formatting of authoring components, ISystems, SystemBase, IJobEntity etc
How you tie UI to systems.
You get an idea.
So people can discuss what is good and what is bad. What can be changed and improved.
Yes i know there is samples on github and docs exist but there is no one guidelines like in OOP with SOLID.
Right now hundreds of projects are a mess with its own unique ways to do stuff and it is hard to fast read.
I totally agree! Having a unified set of guidelines and best practices would help streamline development and reduce confusion. Sharing sample code and discussing what works best will be valuable for a lot of people specially beginners.
There was an official best practice document somewhere. Not sure if that’s still up.
I don’t think we could agree on a best pattern that works for all cases. It’s always a case to case basis. The API allows us to interact with the framework in different ways and it’s up to you do decide what works for you. The best way to handle this is to ask specific questions and let people here show you one or more solutions.
I completely agree with this. I have refactored my ECS code so many times because I found a better way to do something, only to find an even better way to do it a few days later.
Something as simple as making an EntityQuery can be done in a half dozen different ways, most of which create excessive allocations and disposals. I think I am doing the “best” way now but who knows
//Used in OnUpdate
private EntityQuery exampleQuery;
public void OnCreate(ref SystemState state)
{
//No need to Dispose in OnDestroy, the System cleans it up for you
exampleQuery = state.GetEntityQuery(new EntityQueryDesc
{
All = new ComponentType[] { typeof(ExampleComponent) },
});
}
That’s not the best way. First of all - there is managed array - automatically prevents you from burst compiling OnCreate. Proper and advisable way to get query is use EntityQueryBuilder.
Like this in OnCreate is enough:
using var queryBuilder = new EntityQueryBuilder(Allocator.Temp);
_query = builder.WithAllRW<LocalToWorld>()/*...other things...*/.Build(ref state);
(Alternatively you can use codegen variations (even in OnUpdate) for reduce boilerplate (and code clarity unfortunatelly) which under the hood will build similar code from SystemAPI.QueryBuilder().WithAll<LocalToWorld>().Build() or direct SystemAPI.Query<LocalToWorld>() for mainthread iteration)
Second - you should never dispose query manually if you properly built them, internals will handle it for you. (Only EntityManager.CreateEntityQuery should be disposed, but it’s very not advisable (if you don’t know what you’re doing) to use it as it doesnt write to read/write type fence)
Maybe it’s a good idea to create github repo for that, so you can discuss here, but put it in a readable format in a place suitable for actual information sharing. Something like this: GitHub - bustedbunny/DOTS-Manual
I was doing that before but found that I needed to dispose of the builder after using it, which seems like an extra allocation and disposal. It also wasn’t clear if the query needed to be disposed in OnDestroy with that method. Using GetEntityQuery it specifically warns me not to dispose of the query when I tried, so I know it’s being handled.
Im more concerned about leaks than about burst compiling OnCreate to create some queries. If my OnCreate had some other work then I would consider using your method.
The fact that there is so many different ways to do the same thing is part of the problem.
Generally speaking, ECS handles the lifetime of EntityQueries so you don’t have to do anything.
Also, when allocating an EntityQueryBuilder with Allocator.Temp, there’s no need to dispose it. It will dispose automatically at the frame’s end. This is not a special case for EntityQueryBuilder, but is the general behaviour of Allocator.Temp.
I put together a commented sample script that shows some ways to create an EntityQuery.
using UnityEngine;
using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
public struct ComponentA : IComponentData { }
public struct ComponentB : IComponentData, IEnableableComponent { }
public partial struct SampleSystemA : ISystem
{
[BurstCompile]
void ISystem.OnUpdate(ref SystemState state)
{
/* When in a system, this is the most straightforward way to create an EntityQuery.
*
* Source gen handles everything in the background, so that:
* - The query is actually created in OnCreate (not every frame) and cached in a field on the system. Your local variable then just accesses that field.
* - The QueryBuilder is automatically disposed (because it uses Allocator.Temp behind the scenes).
*/
EntityQuery query = SystemAPI.QueryBuilder().
WithAll<ComponentA>().
WithPresent<ComponentB>().
Build();
}
}
public partial struct SampleSystemB : ISystem
{
EntityQuery query;
[BurstCompile]
void ISystem.OnCreate(ref SystemState state)
{
/* This is pretty much what source gen does in SampleSystemA.
*
* There's no need to dispose the EntityQueryBuilder because anything that uses Allocation.Temp is automatically disposed at the frame's end.
*
* Also, we don't need to dispose the EntityQuery itself (just like in SampleSystemA).
* This is because the Build method takes the SystemState object so that the query is bound to the system's lifespan.
*
* This method can be useful e.g. when needing the query outside of the OnUpdate method.
*/
query = new EntityQueryBuilder(Allocator.Temp).
WithAll<ComponentA>().
WithPresent<ComponentB>().
Build(ref state);
}
[BurstCompile]
void ISystem.OnUpdate(ref SystemState state)
{
//Do whatever you want with the query
}
}
public class SampleMonoBehaviour : MonoBehaviour
{
void Start()
{
/* You can also create an EntityQuery outside of a system. For that, we use the EntityManager.
* Again, no need to dispose the builder because of Allocator.Temp.
* The EntityQuery is automatically bound to the lifespan of the world the EntityManager belongs to.
*/
EntityManager manager = World.DefaultGameObjectInjectionWorld.EntityManager;
EntityQueryBuilder builder = new EntityQueryBuilder(Allocator.Temp).
WithAll<ComponentA>().
WithPresent<ComponentB>();
//Two ways to get the actual EntityQuery
//Either
EntityQuery query = manager.CreateEntityQuery(builder);
//or
query = builder.Build(manager); //This literally just calls the above but arguably looks a bit nicer
//You can also have a look at the overloads of EntityManager.CreateEntityQuery, in case you don't want to use an EntityQueryBuilder (for whatever reason).
}
}
At least these are the ways to create an EntityQuery that I know of.
Personally, I almost always just use the source gen version.
Do you mean this one on Unity Learn? DOTS Best Practices - Part 3.2
I found it to be very useful to get a general idea about what tool(s) I could use in which scenario.
You should use temp allocator for query builder which in fact doesn’t do any allocation (as it’s allocated in andvance memory pool you just reuse part of that memory) and dispose is no-op for this allocator which means - it does nothing.