Done a bit of cleanup recently with code and thought someone might be interested in my alternate approach to defining entities dynamically - using ScriptableObjects! I’ve been using a similar blueprint system to create entities for a while now, even before the ECS package was even released but I’ve converted it to be used with Unity’s pure entities.
Instead of defining archetypes in code, each type of my entity gets it’s own blueprint and a collection of installers, which are all ScriptableObjects. [To achieve this I use Zenject Subcontainers just because I love Zenject]
Note: the following screenshots are from my demo not my actual project, they are a bit stripped back without custom editors and only basic components.
Each blueprint has a collection of installers that add ComponentData, SharedComponentData or even BufferArrays.
This MeshInstanceRender is defined as
[CreateAssetMenu(fileName = "MeshInstanceRenderer", menuName = "BovineLabs/Entities/Component/Mesh Instance Renderer")]
public class MeshInstanceRendererBlueprint : ComponentBlueprint
{
[SerializeField] private Mesh _mesh;
[SerializeField] private Material _material;
public override void Install(DiContainer container)
{
container.BindSharedComponent<MeshInstanceRenderer>().WithValue(new MeshInstanceRenderer
{
mesh = _mesh,
material = _material
});
}
}
The BindSharedComponent creates a binding for a ComponentType which will be used to create an archetype.
The WithValue method is optional, but if used will automatically call EntityManager.Set[Shared]ComponentData with your value once the entity is created.
Now this specific installer only installers 1 component, but they’re not limited to that. Sometimes you might want multiple components installed at once, for instance the parts of a mesh could be defined in 4 buffer arrays.
[CreateAssetMenu(fileName = "Mesh", menuName = "BovineLabs/Entities/Component/Mesh")]
public class MeshBlueprint : ComponentBlueprint
{
public override void Install(DiContainer container)
{
container.BindBufferArray<Face>().WithLength(6);
container.BindBufferArray<Vertex>();
container.BindBufferArray<Uv>();
container.BindBufferArray<Normal>();
container.BindBufferArray<Triangle>();
}
}
WithLength should not be confused with [InternalBufferCapacity(6)]. SetLength will initialize with
buffer.ResizeUninitialized(6) so you don’t need to worry about the length and can use it like a fixed array instead of a List.
Once blueprints are setup, you can create them with the factory, just passing a world (Optional) and a blueprint to the factory.
public Init(EntityFactory factory)
{
var entity1 = factory.Create(_blueprint); // Will default to World.Active
var entity2 = factory.Create(_blueprint, World.Active); // Can choose the your world as well
}
You can obviously also set or add runtime specific component values as well once you’ve created your entity. For example setting the position of a unit that has just spawned.
entityManager.SetComponentData(entity1, new Position {Value = new float3(1, 2, 3)});
Under the hood, this will create an archetype like normal using the components bound in each installer. After the entity is created, it’ll go through and set and default values that were requested.
I do not claim this is an ideal way to define entities but from my experience I really like how it works.
Why use this over Prefabs (ComponentDataWrapper)? Easier to share data between entities and better support for custom inspectors.
Why use this over defining in source? Let’s team members who aren’t developers easily create new entities.
Can you try it?
Sure, I have a test version which is a bit unorganized but it is stripped of my project specific parts and 3rd party plugins (except Zenject as it’s a requirement).
Is this for you?
Probably not unless you want to add Zenject to your project.
I tried the demo, my blueprint list doesn’t look like yours?
I use odin inspector which I obviously can’t include.