Game Object Conversion and SubScene

GameObject based data are not ECS compatible. It would be great if it is easy to author the data like before but instead getting ECS data at runtime so that we could work on them efficiently.

Game Object Conversion and SubScene

Games are just about player's input changing data and then see things rendered differently, therefore have fun. Those data are designed on the scene with Unity's WYSIWYG tools. But those GameObject based data are not ECS compatible. It would be great if it is easy to author the data like before but instead getting ECS data at runtime so that we could work on them efficiently.

*This article is based on Entities package version 0.5.1

How the game was made before

Let's think why are we seeing the cubes drawn correctly each frame like this? In classical GameObject, the engine code will compile the entire hierarchy tree and sort and call drawing methods ("draw call") in correct order. Between each call also it need to setup material and mesh to draw ("set pass call") and not set if it could use the same thing as the previous draw. Moreover Unity may attempt to combine meshes to reduce draw call (dynamic batching and static batching) which trade some CPU cycles each frame in the case of dynamic. If static it would be pre-combined at build time ready to draw.

In other words, we use Unity to author the game saved in YAML Scene file. Though the game is not run on YAML, after loading a Scene and following to the Cube .prefab file and also load it, the engine would prepare some C++ memory of Transform so the drawing code could draw according to them. (Where and what to draw)

This is one bulk of the performance each frame. It depends on data, the Transform on each cube and the MeshFilter which tell us what to draw. Unfortunately this sorting, caring about what order of draw call would get correct result, all of them are some form of iteration over data. (At native C++, but C++ doesn't equal fast if they are not arranged well.) And it is not so fast as these data are not lined up in the most optimal shape. You know ECS is good at iteration. And Burst plus ECS helps get the data threadable. So that's why we would like to make a game in ECS instead.

The other bulk is logic. Before getting to the draw, the cube may have Bouncing component that keep changing its Transform, etc. so to make your game exciting. Still, ECS would help this a lot to change data that leads to the draw.

The game is basically just this. You have some data that you change often and may change according to what the player input to the game, and it affect how the frame is drawn.

Both combined, believe it or not the players may already having fun! So in fact everyone is just changing data and see the drawings update and could relax to it. It is important to view the game from data standpoint or else you cannot accept how would one make a game in DOTS because it will feel really weird.

How to make a game in DOTS

If the data is in ECS database instead of GameObject, imagine not only the logic to change data that lead to the draw is fast, also the draw itself would be fast because we could iterate faster, we could sort in jobs and use SIMD instructions.

Let's skip to the end for now. I already have used ECS and now I have 3 transformation matrices I want to draw (In IComponentData, so iteration through it is linear and fast.) plus a Mesh and Material. I would like to see these 3 things now.

We can draw things without any inefficient GameObject data container! See Unity's Graphics API (https://docs.unity3d.com/ScriptReference/Graphics.html) and also this quite new BatchRendererGroup (https://docs.unity3d.com/2019.3/Documentation/ScriptReference/Rendering.BatchRendererGroup.html) which supports NativeArray. You can now draw from data. Just get data with ECS's EntityQuery etc. and draw. You have made a game!

See this Japanese blog post https://virtualcast.jp/blog/2019/10/batchrenderergroup/ demonstrating the power of BatchRendererGroup. Though the "batches" shown is massive and "saved by batching" is absolute zero, the performance is fantastic. Remember that batching is actually combining meshes, it takes work. Draw is just repeatedly calling methods given that nothing changes, these 1000+ "batches" number is not scary at all if you could left the NativeArray memories in BatchRendererGroup like that every frame. ("Set pass call" low number shows that there are not many material switches between draws.)

Hybrid Renderer

Unity made Hybrid Renderer Package (https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@latest) which do that. If you have an Entity with LocalToWorld (this is the matrix), and that chunk has ISharedComponentData of RenderMesh ( Mesh and Material and other stuff) associated, then it would use BatchRendererGroup to draw things for you.

And entire chunk draw the same mesh and same material multiple times according to how many Entity of LocalToWorld you have got. This is already like mini-GPU instancing on CPU, it is just a draw call after draw call that is fast now and this dumb draw may even win over dynamic batching where you pay CPU to combine mesh differently each frame. It is 100% no need to perform set pass call in the same chunk, since a chunk shares the same Mesh and Material. And if your Material has GPU instancing enabled, BatchRendererGroup could really do it instead of rapidly drawing.

It's "hybrid" because BatchRendererGroup is not an ECS API, it's a regular Unity API. But that doesn't mean it is not any less data-oriented. In fact how BatchRendererGroup works is extremely data-oriented. A data of MaterialPropertyBlock, a data of matrices. In NativeArray even! So do not afraid to use it unless you know how to win with Graphics API or use BatchRendererGroup purely on your own. (Maybe to cut out unnecessary steps, like culling.)

Let's make a game in data!

Knowing that the game is all about changing data and see things, with just Hybrid Renderer package we could make a game. Let's say it is a game about seeing bouncing cube. And when you hold space bar it bounces crazier. It could even be kind of fun for kids.

First problem, how do I even get a hold of Mesh and Material in code. So I think I will use Resources.Load to load a dummy prefab holding the asset's reference with this component :

using UnityEngine;

public class AssetHolder : MonoBehaviour
{
    public Mesh myMesh;
    public Material myMaterial;
}

With this system that has no work, just to create required data for Hybrid Renderer.

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;

[UpdateInGroup(typeof(SimulationSystemGroup))]
public class CubeGameSystem : JobComponentSystem
{
    protected override void OnCreate()
    {
        base.OnCreate();
        var myCube = EntityManager.CreateEntity(
            ComponentType.ReadOnly<LocalToWorld>(),
            ComponentType.ReadOnly<RenderMesh>()
        );
        EntityManager.SetComponentData(myCube, new LocalToWorld
        {
            Value = new float4x4(rotation: quaternion.identity, translation:new float3(1,2,3))
        });
        var ah = Resources.Load<GameObject>("AssetHolder").GetComponent<AssetHolder>();
        EntityManager.SetSharedComponentData(myCube, new RenderMesh
        {
            mesh = ah.myMesh,
            material = ah.myMaterial
        });
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return default;
    }
}

And behold! My data-oriented cube at (1,2,3) !

There is nothing in the hierarchy. It just appears.. and it is not pickable no matter how you click. It is just a drawings of a computer. The fact that you could pick things before is because of GameObject data container linking each things you see to each thing you author.

If you look at Entity Debugger right now, you notice quite few more things Hybrid Renderer added. It would cull things you cannot see out before submitting to BatchRendererGroup too.

By the way, this image reads : "You have 1 chunk, this chunk has a capacity of 128 entities (a chunk sized 16kB therefore 1 entity takes 125 bytes) and you have used 1/128 out of this one chunk. You can have 127 more cubes that is continuous in memory. There would be 128 LocalToWorld right next to each other for Hybrid Renderer to take them into BatchRendererGroup.

Transform systems

Now that you have a data and it became a drawing, let's make some gameplay.

I am going to make a gameplay by making it bounce but working with LocalToWorld is a pain. Imagine having to modify the correct slot in the 4x4 matrix. So Unity made us something good. There are tons of systems waiting to be used.

With easier to use Translation, Rotation, Scale/ NonUniformScale, those systems would get them into matrix form LocalToWorld for you. You can even use Parent and say which Entity with TRS LTW stuff is its parent, then the LocalToWorld result you get will be based on parent. e.g. Transform at zero, but has Parent assigned, the resulting LocalToWorld is not zero anymore, but exactly at parent's LTW.

I have added just Translation to the cube. The gameplay system could now modify this instead of LocalToWorld. Some system inside the Transform package would use it to make LTW. I added Cube tag component so it feels more like a cube when I query and not just any Entity.

var myCube = EntityManager.CreateEntity(
    ComponentType.ReadOnly<Translation>(),
    ComponentType.ReadOnly<Cube>(), //Tag
    ComponentType.ReadOnly<LocalToWorld>(),
    ComponentType.ReadOnly<RenderMesh>()
);

Now, the bouncing. I know cosine function would give a bouncing value from 0 to 1 and back to 0 if I give it an increasing value. So let's use Time.ElapsedTime for this.

The beauty of ECS is that you can continue adding systems to the game "horizontally" to build up the gameplay. The pattern is to query data from central ECS great and fast database, do things, and store back. You may feel uneasy about this query and store back ceremony each time you add more systems, but it make things scalable. And Unity ECS lib make sure that repeated query are all fast so you aren't discouraged for slice and dicing logic into tons of systems.

This system, on main thread check if space bar is pressed then declare a variable and also time variable. Capture them into lambda jobs, which get all the Cube with Translation then change their Translation on multithreaded jobs that each thread work on 1 chunk of Cube and Translation. Time is a property maintained by ECS lib that take values from classic Unity Time each frame.

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(TransformSystemGroup))]
public class CubeBouncingSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        int multiplier = Input.GetKey(KeyCode.Space) ? 100 : 20;
        var elapsedTime = Time.ElapsedTime;
        
        JobHandle jh = Entities.WithAll<Cube>().ForEach((ref Translation t) =>
        {
            float3 value = t.Value;
            value.y = math.cos((float) elapsedTime * multiplier);
            t.Value = value;
        }).Schedule(inputDeps);
        return jh;
    }
}

Worker threads are used for this task. When you make games in systems, eventually almost everything would take turn using worker threads so they are not too idle.

This extreme of making a game is painful

You could see some problems. It is already kinda hard to map game problems to data-oriented. (Any other game not about bouncing cube) Now you cannot even use WYSIWYG tools to make a game anymore. Getting LTW + Mesh + Material like this for an entire game will be a nightmare. To make a game all in data is scary though it would be very efficient. Imagine how could you author something that require artistic sense and iteration in only code editor. It's like back to Cocos2D. You want to touch things in editor to plan out.

But remember this cube's drawing is darn fast. If you have more cubes the draw code would serve them into BatchRendererGroup together because they are already together in the same chunk with LocalToWorld lined up memory wise. Duplicating a cube is no longer duplicating GameObject which maybe even costly, it is just make more Entity with LocalToWorld to the same chunk with Mesh and Material already assigned.

Also an Entity that keep updating back to GameObject (the "proxy", or the GameObjectEntity) is no good either. We want pure ECS at runtime, not ECS that must bridge back to MonoBehaviour.

You would like ECS performance but you don't want to make a game like this.

The conversion workflow

So an approach Unity team is going is to use regular GameObject just for authoring. At runtime, there is no GameObject but only ECS data converted from those GameObject.

The reason why I have to prep you about the renderer and transforms before, because all the conversion does is to get you the fast transform and rendering entity ready from GameObject that is easy to author with.

Component object

Before going further, I assume you know about component objects. It is not just IComponentData and friends you can attach to Entity. You can even use MonoBehaviour type.

It is just that you cannot use it in jobs and many API with <T> has IComponentData constraint so you cannot use them. But it is with the Entity for sure and there are API that wants the type T that is not IComponentData constrained. Here are examples of API that supports component objects.

  • EntityManager : AddComponentObject, GetComponentObject. If you just add from thin air and get it back, you would get a null.
  • ComponentType : You can create a type with T that is MonoBehaviour.
  • system.GetEntityQuery, em.CreateArchetype : Therefore, you can use ComponentType of MonoBehaviour to create a query that contains component object.
  • EntityQuery : ToComponentArray which returns T[]. That returns managed array of managed components instead of NativeArray<T> of ToComponentDataArray.

Conversion world, destination world, primary entity

The internal conversion pattern is by creating a special purpose World (therefore you get a new EntityManager, new database for entities) with some special conversion systems in it. This is the conversion world. The conversion world also know one more world, the destination world to store conversion result.

Because conversion world is an ECS World, we would have to start out with some Entity to perform work, and not really start from GameObject in Hierarchy. The importing process from classic GameObject scene is easy thanks to component objects I mentioned earlier. It is possible to get a bunch of Entity that has component objects (class type components) replicating exactly the classical Hierarchy. That is, each entity has Transform ECS component. Has RectTransform ECS component. Has MeshFilter ECS component. Has LineRenderer ECS component. etc. And by having just the Transform, you also retain complete hierarchy information.

These Entity aren't good enough, we want a real entity with no component objects. I want Translation + LocalToWorld and not a class type Transform. Only optimized IComponentData.

By updating the conversion world once, therefore updating all special systems in that world once, it would work on the simply imported component object entities and produce conversion results in the destination world. At least, it would create an empty Entity for each Transform component object found in conversion world.

The resulting Entity A in the destination world is called the "primary entity of GameObject A on the Hierarchy". The reason for this wording instead of something like "resulting entity" is because conversion is not necessarily one to one! It could be structured so that A in the image produce A1 and A2 in the destination world. In that case, one of them is the primary entity.

A special feature while working in the conversion world is that you can "get primary entity" from GameObject. It could be also from any MonoBehaviour component in which case it will just .gameObject and use that instead. For example from Transform of the A Entity in the conversion world. It will be useful in more advanced conversion.

The conversion world along with inefficient Entity with component objects are then destroyed.

GameObjectConversionSystem

The special systems for use inside the conversion world could be marked with :

[WorldSystemFilter(WorldSystemFilterFlags.GameObjectConversion)]

Then they would be ALL scanned and added during conversion. But this attribute is inheritable, and recommended approach is to just subclass from GameObjectConversionSystem which also has that attribute.

For example TransformConversion system. This is what will look for Transform component object in the conversion world, then get you LocalToWorld and Translate / Rotation / NonUniformScale on the primary entity in the destination world. It even know not to add NonUniformScale just yet in the result if the Transform has 1,1,1 scale!

One more built-in system worth looking for is the MeshRendererConversion. This is so MeshFilter and MeshRenderer got migrated into RenderMesh with Mesh and Material from both original component.

Conversion order

As the conversion systems are all scanned and bunched up together, the order is not defined as much. Ordering maybe important, imagine you want to "get primary entity" while in conversion and expect to find the LocalToWorld there, then you have to make sure your conversion system came after Unity's TransformConversion.

It is possible with good old UpdateInGroup attribute. But in conversion world, there are new set of groups.

public class GameObjectDeclareReferencedObjectsGroup : ComponentSystemGroup { }

public class GameObjectBeforeConversionGroup : ComponentSystemGroup { }
public class GameObjectConversionGroup : ComponentSystemGroup { }
public class GameObjectAfterConversionGroup : ComponentSystemGroup { }

public class GameObjectExportGroup : ComponentSystemGroup { }

If you don't say anything then it go to the middle one GameObjectConversionGroup. So if you want to work on conversion on primary entity further from Unity's built-in conversion system, you have to make sure you came after. Note that you can't [UpdateAfter(typeof(TransformConversion))] as that type is not public. But in the screenshot you see that it says it go to before group. Therefore all your conversion system in the middle group can expect to see LocalToWorld and friends waiting on the primary entity. MeshRendererConversion on the other hand goes in the middle group. So if you want to work on RenderMesh ISharedComponentData produced, you must put your systems on the after group.

The declare and export group are special because it came before and after some specific procedures. While all 3 middle groups are just for organization purpose and really run one after another. Therefore, to justify a reason to put in other groups that aren't the middle 3 you have to know what procedures those are. (you will know later)

???
GameObjectDeclareReferencedObjectsGroup.Update()
???
GameObjectBeforeConversionGroup.Update()
GameObjectConversionGroup.Update()
GameObjectAfterConversionGroup.Update()
???
GameObjectExportGroup.Update()

GameObjectConversionMappingSystem

The conversion world will automatically get one more very special system in addition to all available GameObjectConversionSystem, called GameObjectConversionMappingSystem. (From now on I will say just "mapping system") This system is the boss of any conversion world and can do wonders.

It is a pure "tooling system", meaning that it has empty OnUpdate and fully expect other systems to get it and do things manually as a part of other system's OnUpdate. (You can apply this design concept for your own game.)

This is where you could perform GetPrimaryEntity to communicate with the destination world, or create more (secondary, etc.) entities. When you want to perform conversion, you would have to specify the destination world obviously. That world is used as a constructor of mapping system for use while in conversion.

From your conversion systems, you can GetOrCreateSystem to get the mapping system on your own but if you subclass GameObjectConversionSystem that already grab the mapping system for you, along with useful protected methods to use it.

Let's convert with ConvertToEntity

We know enough to perform conversion. Let's just use Unity's built-in conversion systems for now. That is TransformConversion and MeshRendererConversion, therfore make sure the input has Transform, MeshFilter, and MeshRenderer so you see some results.

To select some game objects in the scene to import to conversion world, we can use ConvertToEntity component. On Awake, it will use some criterias to select what to import. So if I just attach one to a GameObject like this, then in the conversion world, I would have an Entity with these ECS component objects : Transform, Hello.

As soon as you enter play mode, on Awake you get :

This is definitely not ready for drawing, as it has no RenderMesh yet. But you see how TransformConversion built-in system see Transform component object and produce LTW / TR in the primary entity before destroying the conversion world.

If I add some scale :

The conversion result I get this Entity instead :

Because TransformConversion conversion system code see that Transform has a non-identity scale, it decided to add additional component to the primary entity.

Let's additionally see MeshRendererConversion in action by having MeshFilter and MeshRenderer on the convert target.

I get a correctly converted cube alongside my pure data bouncing cube! The conversion world could also assign entity name for you because the conversion world knows Transform, it is easy to follow back to .gameObject.name before the conversion world is destroyed.

The "and destroy" original GameObject behaviour is not one of the the conversion world flow. It is just a thing that ConvertToEntity perform additionally. Because it think you wouldn't want duplicate thing on your scene if the conversion gives out an equivalent result. I would have 2 cubes at the same position if not for it destroying, one from MeshFilter and MeshRenderer, and another from Hybrid Renderer rendering from RenderMesh and LocalToWorld.

If you don't like this, you can "just convert" with GameObjectConversionUtility.ConvertGameObjectHierarchy. But that's for later to learn.

Hierarchy conversion

ConvertToEntity actually submits all child GameObject to the conversion world together, unless stopped with ConvertToEntity (Stop).

The conversion world get : A B E F G H. Each one with component objects imitating the original one. Transform component object is the key here because it has .parent, the conversion world would be able to figure out an entire hierarchy tree.

Adding ConvertToEntity to D will produce warning that it is not doing anything. Adding ConvertToEntity to B will not produce any warning but give equivalent result as it is already imported to conversion world by ConvertToEntity at A.

ECS has a component named Parent with a single Entity field to represent transform hierarchy. LocalToWorld calculated will always respect the Parent. If Translation is 0, it is not the case anymore that LocalToWorld will be all 0 at row 1 2 3, column 4, as if it has a Parent it will be at the parent instead.

And the conversion system responsible to populating Parent is none other than TransformConversion system that give us LocalToWorld and friends.. let's see the result :

Among A B E F G H, let's guess how many chunks? (We must be aware of data no matter how magical the conversion seems to be!) Assuming that none of them has any non identity scale in the Transform (so you don't get some with NonUniformScale).

It's 3.. why?

You may expect all of the to have Parent, and in the case of A, it could simply has Entity.Null inside it. But Unity choose to not add a component that is meaningless at all so queries are efficient with ComponentType.Exclude which could benefit in a bigger game. (You can imagine the transform systems could ignore some matrix work if that entity doesn't have any parent in the first place.)

  • No parent, has children : A
  • Has both parent and children : F
  • At leaf, only has parent : B E H G

You see other internal components/system states added : LocalToParent, Child, PreviousParent. Which you don't have to care. They would eventually help you get the right LocalToWorld.

When using conversion it maybe useful to be aware of how many different archetypes that would be produced. For example if you have a huge tree of game objects, and some of them are just empty for wrapping, some of them really has MeshFilter and MeshRenderer for conversion. You could imagine you get at least 6 chunks out of it. (3 no parent/both/no child * 2 has RenderMesh or not) And it would get even more fragmented if you have different mesh and materials, because of ISharedComponentData RenderMesh. Anyways, this kind of thinking is already required in non ECS Unity when you are trying to optimize for dynamic/static batching, or GPU instancing, or SRP batchers. With Entity Debugger, you see more clearly how many things are together efficiently.

The "and destroy" behaviour also changed a bit in a presence of stops, the stopped tree will properly get detached and preserved. Because you don't get an entity equivalent on that part, you probably want to still see them.

Disabled GameObject conversion

You get Disabled component tag added for all GameObject that are disabled. (Not that they wouldn't be converted.) Convert and destroying the top Cube below get you 5 Entity with 3 of them Disabled.

You can also put Convert To Entity (Stop) somewhere inside the disabled chain, the stop component is still able to be existence queried by the conversion code even when the object is not active. If I put stops on Cube(1) and Cube(4), I get 3 Entity out with Cube (2) and Cube (3) Disabled and Cube not Disabled.

Convert and inject

What if :

  • You want to keep the original game object.
  • You also want the component objects exported from the conversion world to the primary entity, before the conversion world is destroyed. (In addition to new components converted)

Before, supposed I have some nonsense such as Hello or LineRenderer here, they would get into conversion world. But since there are no conversion system that would look at Hello or LineRenderer and do anything to the primary entity, they would be lost on conversion world's destruction.

But withh "Convert and inject" mode, this is possible.

So I get LocalToWorld like usual because I have Transform imported into my conversion world. But additionally, Transform Hello and LineRenderer got exported out before the conversion world is disposed. Coupled with a no-destroy behaviour of this mode, it means that you get an Entity that has a way to go back to your original GameObject. If we destroyed the original, em.GetComponentObject<Transform/LineRenderer> on this primary entity would get you null.

So you see the reason now why the previous mode make sense to destroy the original (so you don't see duplicates if the conversion works) and why this inject mode make sense to keep the original (or else the exported component objects would all leads to null and are useless).

And that means if I have a cube with MeshRenderer and MeshFilter but choose the inject mode, I will get 2 cubes rendered at the same position. See this, a cube of 2 * 6 * 2 tris (+2 tris for blue background probably). With convert and destroy, it would still say 26 tris because it got converted to what Hybrid Renderer could use. But with inject mode, it goes up to 50 tris. Because it keep the original and you also get the product from conversion that Hybrid Renderer use.

Therefore this is not a good usage of inject mode. Inject mode is for when you expect the system working on conversion result to be able to check back the original object, which Hybrid Renderer obviously no longer need to.

Manager of everything : Using systems with classic MonoBehaviour

Here's cool thing you could do with inject mode! You can make a hybrid ECS game now. You can view inject mode as turning your object to be system-workable, even if you don't have any good conversion systems. Just the component objects able to escape from conversion world is useful enough. Then you can ditch logic in all MonoBehaviour and use systems to control them instead.

You can add MonoBehaviour to GameObject like they were IComponentData. There maybe a "tag MonoBehaviour" just so you could identify them! Previously you may inherit MonoBehaviour class to get exposed fields for reuse, now you may consider adding multiple components instead like in ECS where struct could not be inherited.

I gain no performance benefit, but great flexibility in adding systems and use ECS's query power.

Maybe you used to have a "manager optimization" where you omit Update() on each object, and instead the manager that do has an Update() has a List of all things and the manager iterate and perform work on them to save Update cost.

The system working on component object is exactly that! Except the query is much more flexible. You can keep pumping out managers that work on subset of GameObject you want. You can divide work easier between your teammates. You can rearrange work order by just changing UpdateBefore/After instead of using the script execution order number hell.

For example this regular UGUI buttons. I want to use systems to give logic to them, like alternating interactable so they appears blinking between greyed out state and back to normal.

All buttons has convert and inject, one of them missing the BlinkingButton "tag" GameObject. It has no code at all but I want to use only the type in the query.

Conversion result, we get 2 chunks, one chunk with 3 entities (the orange bar moved a little bit to the right, so this chunk has 3/160) and one with 1 entity.

Then a system like this could plug into ECS database and get all the buttons with blinking component from the scene. It is very useful that these GameObject could "came from nowhere" with the power of ECS query, gone are the days that you have to declare GameObject[] and collect correct things. It is already a nice tool to make normal Unity game even if you have no interest in ECS.

using Unity.Entities;
using Unity.Jobs;
using UnityEngine.UI;

[UpdateInGroup(typeof(PresentationSystemGroup))]
public class BlinkingButtonSystem : JobComponentSystem
{
    EntityQuery blinkingButtonQuery;
    protected override void OnCreate()
    {
        base.OnCreate();
        blinkingButtonQuery = GetEntityQuery(
            ComponentType.ReadOnly<Button>(),
            ComponentType.ReadOnly<BlinkingButton>()
        );
    }

    //It is not a good idea to have data in system!
    float collectTime;
    bool on;
    
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        collectTime += Time.DeltaTime;
        if (collectTime > 0.2f)
        {
            collectTime -= 0.2f;
            on = !on;
        }

        Button[] buttons = blinkingButtonQuery.ToComponentArray<Button>();
        foreach (var b in buttons)
        {
            b.interactable = on;
        }
        return default;
    }
}

Entities.ForEach could also use component objects, just don't use ref or in, and prevent it from using Burst and use Run instead of Schedule so it is not trying to put reference type on thread. You can ditch the GetEntityQuery this way.

Entities.WithAll<BlinkingButton>().ForEach((Button b) => { b.interactable = on; }).WithoutBurst().Run();

You could now use systems in a game that is not at all migrated to ECS or partially migrated. Just convert with the inject mode so you have Entity for query in the system.

Inject mode conversion importing rule

The previous example has all the buttons receiving inject conversion. What about the Text inside those buttons?

It is a bit different from destroy mode where it submits all children to the conversion world. This time, the children are all ignored even if they also have ConvertToEntity with inject mode. Only the top one get convert and injected. So no Entity produced that would have Text component object.

To demonstrate this rule further, if I also convert the Canvas parent in addition to all the buttons..

I don't get blinking buttons anymore since I seems to only get Canvas! Just for fun to recap the destroy mode child submit, I could try change the mode of the Canvas conversion back to destroy so I get "pure" entity with no component objects..

You see it all the way down to texts, and component objects are all destroyed together with the conversion world without exporting out to primary entity.

IConvertGameObjectToEntity

Making conversion system (subclassing GameObjectConversionSystem) indeed allow you to work on everything in the conversion world. But what if you want a per-type behaviour? Whenever it got converted, something should happen.

Unity has a yet another built-in conversion system called ConvertGameObjectToEntitySystem. This system would iterate through all GameObject followed back from conversion world. Then use GetComponents and see if any got IConvertGameObjectToEntity. Then call .Convert on it. You can put any kind of logic in here.

using Unity.Entities;
using UnityEngine;

public class BlinkingButton : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
    }
}

entity is the primary entity resulting from the GameObject that this component is on. Recall that "primary entity" is associated with one GameObject or any of its MonoBehaviour component. dstManager is the manager of destination world. You can do whatever you like completely unrelated to the conversion. You want 500 useless Entity created per each BlinkingButton converted? You can!

You can use this chance to initialize more data with dstManager.Add / SetComponentData. It's important that you are kind of "remote controlling" the destination world while you are currently working in conversion world while using dstManager. Take the name literally, because there is an another manager here in conversion world.

Writing a conversion system that Entities.ForEach and initialize the same thing maybe more efficient because you could use Burst and jobs, if the initializations are similar. But this way you can use fields serialized on your MonoBehaviour to customize each individual entity generated at will.

The conversionSystem is actually the ConvertGameObjectToEntitySystem that call this Convert in the first place, but casted back to the base type GameObjectConversionSystem (name confusingly similar, watch out). The purpose is so that you can use the mapping systems to perform work. From here on you will meet many methods on the mapping system.

LinkedEntityGroup

LinkedEntityGroup is a dynamic buffer of Entity. Normally it causes :

  • Instantiate on one Entity would be able to instantiate many entities at once according to the list of Entity in that buffer, and also setup similar LinkedEntityGroup again for you. (According to newly instantiated entities, but saying the same relationship as old one.) Notice that instantiation is possible even before we get to know a component named Prefab.
  • DestroyEntity will also destroy them together if the destroy target is the parent one with LinkedEntityGroup. So just like pressing delete on the GameObject having children in the editor.
  • entityManager.SetEnabled to attach/detach Disabled component so it won't be queried anymore, will also do it to everything in the buffer. So it works more like disabling a tree of GameObject in the editor.

It will not work recursively if it found another LinkedEntityGroup while performing instantiation/destroy/disabled on all linked enties.

How it perform operation on all linked entities seems to vary. Instantiate and SetEnabled have new behaviour as soon as the buffer is detected, and perform operation on all members at once, and nothing more. That means the buffer must include itself as one of the "linked entity". (Linked to.. itself) However DestroyEntity works even if the buffer didn't contain itself, it destroy the requested Entity first then iterate through the buffer to add more to destroy.

Note that this is different from Parent which is used to compute LocalToWorld that make sense. (But they are often used together.) That one works recursively, and cyclic dependency is not allowed.

Getting LinkedEntityGroup from non-prefab conversion

So far, we get no LinkedEntityGroup at all with ConvertToEntity on either mode. So destroying the resulting entity (let's say A from the earlier) will not chain-destroy everything. Maybe you are expecting this or not. But they think this should be the default behaviour. (Unlike with Parent, where Unity link up according to hierarchy while converting Transform by default.)

If you want the buffer added and populated, on the mapping system while converting, use :

 public void DeclareLinkedEntityGroup(GameObject gameObject)

The the primary entity of that object will get the buffer, containing all its children (recursively, linearized) at the destination world.

public class CubeConvert : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        conversionSystem.DeclareLinkedEntityGroup(this.gameObject);
    }
}

And here they are. It includes itself in order to support Instantiate and SetEnable as explained.

There are other places that Unity perform the linked processing by default, but not here in ConvertToEntity. For example while converting a prefab asset. (next)

Size alert!

You may think 16kB per chunk was a lot, you can fit as much as 2000 Entity in it. But as soon as you play with conversion that involves hierarchy, LinkedEntityGroup buffer and its system state friends (Child, etc.) could really take up space per entity since each buffer element is an Entity (8 bytes).

This 12 chunks could only hold 45 Entity each (capacity) despite it is just a simple hierarchy with very few custom component! Now that's a lot down from 1000 range.

As of version 0.5.1, the TypeManager.cs source code says any buffer type without [InternalBufferCapacity] (LinkedEntityGroup is like that) will have capacity 128/size. LinkedEntityGroup is a buffer of Entity 8 bytes each, therefore capacity is 128/8 = 16.

This implies :

  • LinkedEntityGroup (or any buffer without capacity specification) take from the get go 128 bytes per Entity. (equivalent to 32 int/float) That's why capacity in a chunk get so small.
  • It's not good to have more than 16 children (counted recursively, not just one level down) in any of your GameObject for conversion, or else the remembered linked entities have to move out of nicely packed chunk to heap memory. Maybe Unity think 16 is the unlikely number and 8 is maybe too likely.
  • LinkedEntityGroup is only added for prefab conversion unless you have a declare linked on normal conversion. Therefore normally, you just need to keep a watch on your converted prefabs how many GameObject it got.
  • Remember that at runtime, all the nested prefab and variant workflow doesn't matter and the system see it as a single prefab. It is not that if you have a nested prefab, they would be excluded from LinkedEntityGroup and begin their own.

Lastly a chunk of 16kB means you can have about 60 chunks in 1 MB, for perspective. Therfore if the capacity get down to about 45 like above example, then you can fit about 2700 converted GameObject in 1 MB. Maybe capacity of 45 is nothing to worry about after all. (It depends)

Creating additional entities

A theme of conversion is that one GameObject results in one Entity that is its "primary entity". On the mapping system, call CreateAdditionalEntity(gameObject) to create more Entity that should came from any GameObject. Those entities are now "secondary entity of that game object", etc.

Supposed that the new design is so whenever GameObject with CubeMultiple get converted, it gets 2 additional entities.

public class CubeMultiple : MonoBehaviour, IConvertGameObjectToEntity
{
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        Entity additional1 = conversionSystem.CreateAdditionalEntity(this.gameObject);
        dstManager.SetName(additional1, $"{this.name}_Add1");
        Entity additional2 = conversionSystem.CreateAdditionalEntity(this.gameObject);
        dstManager.SetName(additional2, $"{this.name}_Add2");
    }
}

Note that Convert call works recursively, the ConvertToEntity is at the top CubeHead.

Result. Note that the additional entity really is newly created. There is no component at all unlike those converted from component objects like Transform. (See the chunk with 2008 capacity.)

You may realize you can do the same thing by commanding the destination entity manager dstManager.CreateEntity(). But it is not just a matter of making things proper.

Query all entities that "came from" a single source

On the mapping system, call GetEntities(gameObject/component). Because we made additional entities and make the conversion world know they came from the same certain thing, it is now possible to ask for them all. For example, if I have a reference to GameObject CubeMultiple and use GetEntities I would be able to get both its primary entity and 2 additional entities.

You don't have to worry about whether they have been created yet / worry about Convert order, because on this kind of call it has a routine to make sure to Convert if not yet. (This also applies to GetPrimaryEntity)

Affecting linked entity group

Recall DeclareLinkedEntityGroup. By creating additonal entity the proper way the conversion system know their association with some GameObject in-conversion. The result is that they are included in the linked entity group generate from the declare.

The previous example where CubeHead has DeclareLinkedEntityGroup on itself, the result now includes additional entities generated from CubeMultiple.

It wouldn't work if you just cheat your way and use dstManager to create entity. By doing that, it sounds more like converting CubeMultiple causes "side effect" of adding more Entity to destination world, rather than CubeMultiple really becomes multiple entities. It looks like it should also make things play well with live link. (explained later)

Declaration

Maybe you want an Entity generated per asset too, not just per things on the scene. Good news because primary entity could also be associated with an asset, not just GameObject (or MonoBehaviour) In other words, if you GetPrimaryEntity inputting those asset to the mapping system you will get a representative Entity of the asset.

Because you can't put ConvertToEntity on an asset file on Project panel, you need some way to "declare" that you want them as an entity after the conversion.

When could you declare them?

Before any conversion take place. This is know as the discovering phase. The API will error otherwise if you do so while in conversion. This means while in conversion, you can nab the primary entity of the converted asset, by using GetPrimaryEntity of the mapping system inputting the original asset, to assign to any component in the Entity of the non-asset stuff. Otherwise it maybe a bit difficult to query them for use later if you are not doing it now in conversion.

IDeclareReferencedPrefabs

In the discovering phase, first all participating GameObject will be scanned if any of its component got IDeclareReferencedPrefabs. In here you can declare by adding prefab assets (not other asset yet) to the given list. You can bring prefab asset by the serialized exposed GameObject field connecting to prefab asset in your Project panel.

Declare within conversion systems

After the scan for that interface, recall the 5 conversion groups. All conversion system in the very first GameObjectDeclareReferencedObjectsGroup will get updated. In here, you have a yet another chance to declare things as much as you like, prefabs and this time any other assets.

public class GameObjectDeclareReferencedObjectsGroup : ComponentSystemGroup { }

public class GameObjectBeforeConversionGroup : ComponentSystemGroup { }
public class GameObjectConversionGroup : ComponentSystemGroup { }
public class GameObjectAfterConversionGroup : ComponentSystemGroup { }

public class GameObjectExportGroup : ComponentSystemGroup { }

Using conversion system meaning that you have the mapping system at hand. There are several methods in the mapping system you can use for declaration. Where could you get Project panel things from system code though? You can query for any component object in the conversion world that may have the asset you want connected to exposed field, for example. But at which point you may start to wonder why didn't you use IDeclareReferencedPrefabs in the first place when you have to make some asset "holder" anyways. Use system-based declaration in more customized cases and stick to IDeclareReferencedPrefabs most of the time.

Then when time comes for normal conversion, you can combo with IConvertGameObjectToEntity to get a hold of the just-converted Entity of an asset/prefab you want by getting its primary entity, using that same before-convert prefab reference as a key to get it.

public class BlinkingButton : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    public GameObject myPrefab;
    
    //This first
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(myPrefab);
    }
    
    // |
    // V
    
    //Then GameObjectDeclareReferencedObjectsGroup <- last chance to declare stuff
    
    //Then GameObjectBeforeConversionGroup
    //Then GameObjectConversionGroup, which contains ConvertGameObjectToEntitySystem that run the following Convert

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        //Get primary entity of the prefab asset, to assign to primary entity converted from this component.
        Entity prefabAssetEntity = conversionSystem.GetPrimaryEntity(myPrefab);
        //Remember it, etc.
    }
    
    // |
    // V
    
    //Then GameObjectAfterConversionGroup
}

If you want to see the error about violating declaration moment rule, notice that at the Convert of IConvertToGameObjectToEntity moment you also have a chance to use mapping system. (To add additional entity, to travel to other primary entity, etc. as said earlier) But if you use this mapping system to declare more things you will get errors. It's too late!

This is getting demystified now :

??? <- IDeclareReferencedPrefabs calls.
GameObjectDeclareReferencedObjectsGroup.Update()
??? <- End of discovery phase, no more declares to assets.
GameObjectBeforeConversionGroup.Update()
GameObjectConversionGroup.Update()
GameObjectAfterConversionGroup.Update()
???
GameObjectExportGroup.Update()

By the way that declare deadline only applies to declaring assets and prefab assets. You can still DeclareLinkedEntityGroup while in conversion.

Declaring ANY asset

On the mapping system, use DeclareReferencedAsset.

You will get an Entity with tag component Asset, plus a component object of exactly that Unity asset type with reference to the asset, inside conversion world. (not destination world)

For example I could have my Entity + Asset + SpriteAtlas entity created from declaring DeclareReferencedAsset(mySpriteAtlas). The primary entity of it on destination world is completely empty though. It is still not clear what is the purpose of this system. Perhaps it expect you do perform Asset to anything conversion on your own or else you get empty entity out.

Declaring prefab asset

Add it to the list as a part of IDeclareReferencedPrefabs. Or in the case of declaring inside GameObjectDeclareReferencedObjectsGroup group, using the mapping system, use DeclareReferencedPrefab.

But this time it is not as dumb as getting Asset and GameObject as a component object with it. This time, you really get a prefab entity on destination world.

A primer what an Entity prefab is. If you attach Prefab to any Entity, it would stop showing up on query unless explicitly asked to, works almost like Disabled except when using Instantiate, it strip out the Prefab tag. (You wouldn't want the instantiate to produce yet another prefab...)

The purpose is different, we hidden them from query not because we want to make them "inactive", but because they aren't supposed to be active. They are just blueprints to create new data fast. On creating a query there are 2 separated mode "include disabled" and "include prefabs". So please use it as the library intended. (Do not use Prefab to hide from query, etc.)

Together with LinkedEntityGroup which affects EntityManager.Instantiate as explained earlier, you can really feel like instantiating a prefab in ECS!

So by declaring a prefab asset, you have added a new primary entity related to that asset with both Prefab and LinkedEntityGroup properly setup at the output.

Note that by placing ConvertToEntity on Hierarchy they would just turned out to a regular Entity from what we have seen so far, because things on hierarchy aren't asset. We don't get any Prefab or any LinkedEntityGroup. No matter if they are blue named on the Hierarchy or not.

Prefabs are asset file, and you can't paste ConvertToEntity to run on assets in Project panel. The ECS library determine if it is a prefab or not by seeing that if it is on the scene or not. And everything pastable with ConvertToEntity are on the scene, obviously.

Prefab asset conversion

You convert a prefab asset by declaring it like explained. I would like to go into details what are differences than when you convert GameObject on the Hierarchy. Instantiated from prefab asset or not, doesn't matter as long as they are on Hierarchy. It is only counted as asset if they link to your Project panel's assets.

Automatic LinkedEntityGroup : for instantiation

With the following setup on the prefab asset :

It is important to realize that CubeHead is an asset, not scene objects.
[GenerateAuthoringComponent]
public struct PrefabConversion : IComponentData
{
    public Entity prefab;
}

We would get :

  • If we convert CubeHead instantiated onto scene, we wouldn't get any  LinkedEntityGroup. This behaviour is only for prefab asset, it's intention is of course to get it ready for instantiation. (And also you get chain-destroy behaviour of the instantiated entity, since instantiation duplicates even this linked entity group.)
  • LinkedEntityGroup contains everyone down the tree and also itself. You may expect it to not contain Cube 3 and 4 which is not the case. Conversion "linearize" the prefab. It is trying so recursive algorithm is not needed, just go through linked entity group linearly when instantiating.
  • The reason that it also include itself in the buffer because mass SetEnable could do it all at once, rather than at the requested Entity first then do it for each linked entity.
  • All entities including the head get Prefab. But we will use just the head for Instantiate. Prefab actually doesn't matter for Instantiate. It's LinkedEntityGroup that make instantiation work in group. You can instantiate any entity.

Automatic LinkedEntityGroup : for chain-enable

The previous section explained that only at the head of prefab you get LinkedEntityGroup for the purpose of instantiation.

Additionally, anywhere in the prefab that it found disabled GameObject, it get a LinkedEntityGroup containing all children recursively.

The purpose of this one is so that you can use SetEnable on the instantiated Entity a single time and remove Disabled from multiple entities at once. Disabled conversion still follow the same rule as when converting a non-asset GameObject, this added LinkedEntityGroup further helps you.

Can't stop won't stop

You cannot put Convert To Entity (Stop) somewhere in the prefab in hope that it will not be included in the result.

Fun quizzes

With all conversion rules so far, see if you could get the conversion result right regarding to following components : Parent (remember that this is for transform systems), Prefab, Disabled, LinkedEntityGroup (and its content). Child and friends will appear according to Parent after a round of update by transform systems.

Cube : Prefab, LinkedEntityGroup (Cube,1,2,3,4)
1    : Prefab, Parent (Cube)
2    : Prefab, Parent (Cube)
3    : Prefab, Parent (2)
4    : Prefab, Parent (2)
Cube : Prefab, Prefab, LinkedEntityGroup (Cube,1,2,3,4)
1    : Prefab, Parent (Cube)
2    : Prefab, Parent (Cube), Disabled, LinkedEntityGroup (2,3,4)
3    : Prefab, Parent (2), Disabled
4    : Prefab, Parent (2), Disabled

Remember that you always get itself in one of the element of LinkedEntityGroup, including for-enable ones. And therefore if you have any disabled GameObject even if it is at the leaf, you always get LinkedEntityGroup of at least 1 element that is itself. (You may think it doesn't matter, but it will fragment chunks a bit more)

Cube : 
1    : Parent (Cube)
2    : Parent (Cube)
3    : Parent (2)
4    : Parent (2)
Cube : 
1    : Parent (Cube)
2    : Parent (Cube)
3    : Parent (2)
4    : Parent (2)

Blue name (linked with prefab asset) or not doesn't matter, it is not an asset. It won't follow LinkedEntityGroup conversion rule.

Cube : 
1    : Parent (Cube)
2    : Parent (Cube), Disabled
3    : Parent (2), Disabled
4    : Parent (2), Disabled

You cannot chain-enable starting from primary entity of Cube (2), because there is no LinkedEntityGroup generated for you. You must declare linked manually.

Remapping behaviour of prefab instantiation

Only when calling Instantiate with an entity with LinkedEntityGroup, an entity remap will take place. (Read more here : https://gametorrahod.com/entity-remapping/) You know converting a prefab asset make LinkedEntityGroup available, perfect.

So it is possible to bake an Entity value somewhere in the prefab entity you get from conversion, and when instantiating, that baked value would came back to life, making sense in the instantiated context.

From the previous situation, if I add a new conversion script to SpecialCube so its Convert get called as a part of making CubeHead prefab entity, an intention is to remember the Entity that would be generated from its Cube (3).

public class SpecialCube : MonoBehaviour, IConvertGameObjectToEntity
{
    public GameObject itsChild;
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponent<LinkedEntityGroup>(entity);
        var leg = dstManager.GetBuffer<LinkedEntityGroup>(entity);
        leg.Add(conversionSystem.GetPrimaryEntity(itsChild));
    }
}

Checking the Prefab entities got from conversion, it is expected that this manual LinkedEntityGroup (I could just bake a single Entity, but any buffer of Entity works too.) has reference to its Prefab peer.

Remapping will take place to this entity hidden in the prefab as long as it could find something to remap to among the LinkedEntityGroup at top where you use with Instantiate. Therefore it is useful to "wire up" the Entity reference while in conversion before conversion turning it into a Prefab, so when you Instantiate it you get a ready to use relationship.

Remapping won't occur if Instantiate to something without LinkedEntityGroup because in that case there is no peer to look for remapping Entity value.

What about GameObjectExportGroup

With 4 out of 5 groups introduced and their purpose, this is the last one. So it looks like that the conversion could be retooled not for use, but for export instead. Normally, the export group is not included in the conversion pipeline. Only when asked to export that they are scanned and collected to be at the end.

If your conversion system is in here, expect not only all conversions has finished, also you can see LinkedEntityGroup available and buffer populated and Prefab tag added to all declared prefabs at this stage, so you can use it. (use for instantiation, etc.) Of course you should not convert anymore in a group named "export group".

What is an "export" remains a mystery, maybe when Unity team made some documentation I could understand. For now you can just think this group is not useful for your everyday conversion. And it has to do something with Asset entity result from declaring assets, explained earlier.

??? <- IDeclareReferencedPrefabs calls.
GameObjectDeclareReferencedObjectsGroup.Update()
??? <- End of discovery phase, no more declares to assets.
GameObjectBeforeConversionGroup.Update()
GameObjectConversionGroup.Update()
GameObjectAfterConversionGroup.Update()
??? <- LinkedEntityGroup added to declared linked, LinkedEntityGroup + Prefab added to declared prefabs back at discovery phase.
GameObjectExportGroup.Update()

What is DeclareDependency on the mapping system

There is yet another unexplained declare. But I don't know about this. It looks like something that make live link (explained later) work better if you do it correctly.

Demo on declaring prefab asset and using it

If I convert and destroy this purely MeshFilter and MeshRenderer tree of cubes, I would get 2 chunks out of it.

But I have made this into a prefab asset and delete it from the scene. I would like to instead convert this to a hidden Prefab entity for use with Instantiate later.

A new plan is the following : a new SpawnHere MonoBehaviour "tag" component should be custom-converted into spawning positions of the entity prefab

What if the code is like this for each (it is fine for 3 GameObject to declare the same prefab, the conversion system knows not to add duplicates) :

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

public class SpawnHere : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    public GameObject forDeclare;
    
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(forDeclare);
    }
    
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
    }
}

So at discovery phase it is declared so the asset's primary entity would already been created, which would looks just like prior result except it has Prefab and LinkedEntityGroup so it is hidden, ready for spawn. Spot the difference from before! (You cannot see this cube, since they are hidden from query of Hybrid Renderer thanks to Prefab though it satisfy the RenderMesh and LocalToWorld conditions.)

After conversion, I could grab the Prefab and instantiate at the Translation of converted 3 spawn heres. Since the SpawnHere is convert and destroyed, it is kinda scary to query from either LocalToWorld or Translation as many other Entity may also have these.

So let's add a tag in conversion to indicate that they really came from SpawnHere :

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

public struct SpawnHereEcs : IComponentData
{
}

public class SpawnHere : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    public GameObject forDeclare;
    
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(forDeclare);
    }
    
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponent<SpawnHereEcs>(entity);
    }
}

Additionally, to avoid having to fumbling for the prefab entity later, let's just put the prefab asset's primary entity to stick with the tag. (So technically not a tag anymore) The key to get that entity is the original prefab used at declaration.

using System.Collections.Generic;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;

public struct SpawnHereEcs : IComponentData
{
    public Entity thatPrefabEntity;
}

public class SpawnHere : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    public GameObject forDeclare;

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(forDeclare);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        Entity prefabEntity = conversionSystem.GetPrimaryEntity(forDeclare);
        dstManager.AddComponentData<SpawnHereEcs>(entity, new SpawnHereEcs {thatPrefabEntity = prefabEntity});
    }
}

Later, a system looking for SpawnHereEcs entities would then spawn at those positions. Also remove SpawnHereEcs so it wouldn't spawn repeatedly.

[UpdateInGroup(typeof(SimulationSystemGroup))]
public class SpawnSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        Entities.ForEach(
                (Entity e, in Translation t, in SpawnHereEcs she) =>
                {
                    Entity instantiated = EntityManager.Instantiate(she.thatPrefabEntity);
                    //Set to the same translation.
                    EntityManager.SetComponentData(instantiated, t);
                    EntityManager.RemoveComponent<SpawnHereEcs>(e);
                })
            .WithStructuralChanges().Run();
        return default;
    }
}
You can also RemoveComponent in chunk, by using WithStoreEntityQueryInField to get EQ out first then use EntityManager.RemoveComponent to it outside the ForEach.

And there you have it!

The instantiated one all have LinkedEntityGroup just like the Prefab. And notice it have a rather nice editor there with Select buttons.

Bonus : Trying to get even smarter, how about no system following to spawn at all but spawn the cubes right at the conversion? This way we don't need the SpawnHereEcs tag, and we know TransformConversion is already done at this point so we know where to spawn from each spawn here's primary entity. So just use the given dstManager to perform instantiation right here. It would feel just like SpawnHere MonoBehaviour converted into prefab instances, instead of previously where they converted into spawn positions, prefab converted into prefab entity, then finally a system use those position to instantiate.

Bonus point if you could figure out why this won't work properly before scrolling down!

using System.Collections.Generic;
using Unity.Entities;
using Unity.Transforms;
using UnityEngine;

public class SpawnHere : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
{
    public GameObject forDeclare;

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(forDeclare);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        Entity prefabEntity = conversionSystem.GetPrimaryEntity(forDeclare);
        
        var spawnPosition = dstManager.GetComponentData<Translation>(entity);

        Entity instantiated = dstManager.Instantiate(prefabEntity);
        dstManager.SetComponentData<Translation>(instantiated, spawnPosition);
    }
}

It almost work. But it should spawn 3 more cubes each. GetPrimaryEntity with the declared prefab asset certainly works at this point since it has already past the discovery phase. GetComponentData<Translation> also should work as Transform has been converted to that since GameObjectBeforeConversionGroup group as we know before.

But recall that the LinkedEntityGroup and Prefab for all the declares are only finalized at :

??? <- IDeclareReferencedPrefabs calls.
GameObjectDeclareReferencedObjectsGroup.Update()
??? <- End of discovery phase, no more declares to assets.
GameObjectBeforeConversionGroup.Update()
GameObjectConversionGroup.Update() <-- YOU ARE HERE
GameObjectAfterConversionGroup.Update()
??? <- LinkedEntityGroup added to declared linked, LinkedEntityGroup + Prefab added to declared prefabs back at discovery phase.
GameObjectExportGroup.Update()

...just before GameObjectExportGroup. And therefore because we are currently in GameObjectConversionGroup, that primary entity is lacking both Prefab (not a problem) and LinkedEntityGroup (a problem, since this cause the magic on instantiation to instantiate things other than only the top one). Therefore, you cannot use the prefab you just declared while in conversion because it is not finalized yet. If you didn't know anything about conversion world and just use the conversion tools, this would be quite hard to reason.

[GenerateAuthoringComponent]

Notice the SpawnHere -> SpawnHereEcs pattern earlier. It is likely that you want a MonoBehaviour component that expose some box on the inspector only to migrate that to an equivalent after the conversion. We call the MonoBehaviour one the authoring component.

We could create a GameObjectConversionSystem that perform adding equivalent component and migrate data. But doing it in IConvertGameObjectToEntity is more succint. Because it is right there with the authoring class file.

using Unity.Entities;
using UnityEngine;

//The real ECS
public struct MyData : IComponentData
{
    public float data1;
    public int data2;
    public bool data3;
}

//For authoring
public class MyDataOnScene : MonoBehaviour, IConvertGameObjectToEntity
{
    // Ugly duplicated code
    public float data1;
    public int data2;
    public bool data3;
    
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        dstManager.AddComponentData<MyData>(entity, new MyData
        {
            // Tedious work
            data1 = data1,
            data2 = data2,
            data3 = data3,
        });
    }
}
Tada

What's even better than this, is to just declare the ECS part and have IL magic make the MonoBehaviour one for you!

using Unity.Entities;

[GenerateAuthoringComponent]
public struct MyData : IComponentData
{
    public float data1;
    public int data2;
    public bool data3;
}

Note that we didn't declare any MonoBehaviour yet we get a component nicely named "My Data Authoring" and even "My Data" that leads to the same thing. This is a generated code and it would have a Convert section similar to what we have written by hand. You cannot double click the MyDataAuthoring to see generated code, it would lead to your ECS struct part. It's basically magic!

Currently your ECS fields must be public for the magic to happen. Something like this is not getting translated to generated mono part yet :

using Unity.Entities;
using UnityEngine;

[GenerateAuthoringComponent]
public struct MyData : IComponentData
{
    [SerializeField] internal float data1;
    [SerializeField] public int data2;
    [SerializeField] public bool data3;
}

There is one more trick to this, it is also able to generate the "declare inspector connected prefab at discovery phase and then grab its primary entity to save into primary entity of the object being converted" pattern I used with SpawnHereEcs written by hand earlier, by just declaring Entity field.

using Unity.Entities;

[GenerateAuthoringComponent]
public struct MyData : IComponentData
{
    public Entity magic;
}

It turned into GameObject exposed field! This is equivalent to writing these manually :

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

//[GenerateAuthoringComponent]
public struct MyData : IComponentData
{
    public Entity magic;
}

public class MyDataAuthoring : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    public GameObject magic;
    
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(magic);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        Entity primaryEntityOfDeclaredPrefab = conversionSystem.GetPrimaryEntity(magic);
        dstManager.AddComponentData<MyData>(entity, new MyData
        {
            magic = primaryEntityOfDeclaredPrefab
        });
    }
}

And of course you can have a mix of Entity and float and others... magic!

Mixing both destroy and inject mode

You may feel like you don't want the inject mode because you get too much component objects and it didn't travel an entire tree. But also the destroy mode cannot convert something down the tree cleanly to ECS. You kinda want something partially injected. And the "stop" component is not answering the need since you do want to convert, not skip that part.

An example : If I build a ship like this out of Transform, MeshFilter, and MeshRenderer, the ConvertToEntity with destroy mode would be able to cleanly convert it to LocalToWorld + RenderMesh.

I remembered the cube bouncing system that just look for Cube with Translation. If I add Cube to the ship's root it should do the trick. Use the magic we just learn to add Cube component as a part of conversion.

using Unity.Entities;

[GenerateAuthoringComponent]
public struct Cube : IComponentData
{
}

But if I have a particle system on its exhaust :

When a component object ParticleSystem get into the conversion world, of course it wouldn't survive. The result I want, since the particle system is on local simulation space, all particles should bounce up and down.

If I add Stop at the particle system then it wouldn't get converted at all and is now unrelated and not even having entity counterpart.

How about adding inject mode inside destroy mode? Now I do get an injected Entity with component object ParticleSystem out. But is it in anyway related to the separately converted with destroy mode ship?

The current result didn't say so.

But however with Entity Debugger, it reveals that it did have Parent and LocalToParent added as a part of being converted together on the same tree with destroy mode. At the same time seeing component object ParticleSystem here showing that it works with injected mode. You have mixed both modes!

From our knowledge, Parent should already get us the correctly following LocalToWorld. But the particles aren't bouncing since there is no one to use that LocalToWorld (Not related to Hybrid Renderer this time) So we can just copy back the LTW to Transform with the copy transform to game object proxy. Doing so will also add GameObjectEntity. You then see a lone Particle System in Hierarchy following ECS-moved ship. This is like you convert 95% of what you could, then glue the remaining 5% to your fully converted part.

Note that if this ship is a prefab asset, then you declare this ship, the resulting prefab will looks like what came out of only the convert and destroy, that is, missing the particle system.

Companion Game Object

The previous approach did sounds like a hack. The Particle System left over in the hierarchy yet it move together with the converted entity screams "glue".

Companion game object is a more "clean" way of convert and destroying a tree that contains bits of unconvertible objects. It will create a new game object to accompany convertible data and have it host select components you want to save. Making it seems like everything converted nicely.

You do so with the mapping system. Therefore, I will create some script so I could have IConvertGameObjectToEntity. There is no ECS counterpart of this Ship. It is just for customizing the convert a little.

using Unity.Entities;
using UnityEngine;

public class Ship : MonoBehaviour, IConvertGameObjectToEntity
{
    public ParticleSystem particleCompanion;
    public ParticleSystemRenderer rendererCompanion;
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        conversionSystem.AddHybridComponent(particleCompanion);
        conversionSystem.AddHybridComponent(rendererCompanion);
    }
}

The name is companion game object, but you config it with components. You call the mapping system AddHybridComponent for it to collect not only what GameObject should become a companion, also what component should remain on that companion to be created (per one primary entity, identified from that component). Therefore I declare public fields of components instead of GameObject.

In this case of ParticleSystem, it actually has one more hidden ParticleSystemRenderer component. So if I forgot the renderer, only ParticleSystem remains on the companion game object and it wouldn't render. (And also waste CPU simulating particles.)

I have removed ALL the hacks from that Particle System! (Removed ConvertToEntity with inject mode, removed copy proxy, removed GameObjectEntity) it is just convert and destroy an entire ship.

Now there is no Particle System left on the Hierarchy this time, YET it is there in the Game view AND also move together. How? Where is it? Even inject mode ConvertToEntity where you get component objects on Entity, they should link back to something on the scene.

This is the primary entity of Particle System (under Cylinder, so its Parent should be Cylinder)

The companion registration of both components automatically leads to their (same) GameObject's primary entity (This Particle System entity) being inject-converted with all component called with AddHybridComponent. (You see ParticleSystem and ParticleSystemRenderer here)

Those component object still link back to things on the scene... but the trick is that they are just being hidden with HideFlags.HideInHierarchy. Finally why it moves, notice the CompanionLink component. That contains the hidden game object on scene with the hide flag. Then there is a system called CompanionGameObjectUpdateTransformSystem that do the same thing as copy transform to game object except the source is from LTW of entity having CompanionLink and target is the game object in the CompanionLink.

So it is the same thing as the glue hack previously after all! But more official and cleaner, and more inline with the conversion system.

It is compatible with prefab!

One thing better than the hacky way is that, it could survive the conversion to Prefab and is fully instantiable by ECS. (The entire thing, including companion particle system)

That means each instantiation, it would still do it fast for the ship's RenderMesh converted from MeshFilter and MeshRenderer. However it see CompanionLink component object in the prefab, that is connected with the HideFlag hidden game object that is also created when you declare and convert the prefab. That will be simply GameObject.Instantiate to produce a yet another hidden one, but companion-linked to your new instantiated Entity.

Basically, it feels like EntityManager.Instantiate somehow perfectly duplicates ParticleSystem that is not even in ECS database but is hidden on the scene. (Magic!)

First turn the Ship into a prefab asset. Notice that the companion definition would be there in the prefab since it reference its children.

Let's use the authoring magic Entity-GameObject we learned earlier :

using Unity.Entities;

[GenerateAuthoringComponent]
public struct ShipAndCo : IComponentData
{
    public Entity shipPrefab;
}

On entering play mode, I have a Prefab entity ready on that ShipAndCo singleton Entity. Since I know it is a singleton, let's use this kind of system to play with it : each time I press A, instantiate in ECS at random position 0~10.

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
using Random = Unity.Mathematics.Random;

[UpdateInGroup(typeof(SimulationSystemGroup))]
public class ShipAndCoSpawnerSystem : JobComponentSystem
{
    Random randomizer;

    protected override void OnCreate()
    {
        base.OnCreate();
        RequireSingletonForUpdate<ShipAndCo>();
        randomizer = new Random();
        randomizer.InitState(seed: 12345);
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            var sac = GetSingleton<ShipAndCo>();
            Entity prefabEntity = sac.shipPrefab;
            Entity instantiated = EntityManager.Instantiate(prefabEntity);
            EntityManager.SetComponentData<Translation>(instantiated, new Translation
            {
                Value = randomizer.NextFloat3() * 10
            });
        }

        return default;
    }
}

I get a duplicated hidden companion particle systems game object as a part of Entity instantiation. This is pretty cool!

The ship appears falling down since random also random the Y axis, then the cube bouncing system make the Y oscillate from 0 with cosine function.. lol

I could see the companion game object being the final piece of puzzle to convert anything to ECS. (Let's ignore the fact that there are some game objects with HideFlag lurking somewhere.)

GameObjectConversionUtility

If you don't like any of these convert and destroy or convert and inject rules, you can "just convert" on your own with this static class.

public static Entity ConvertGameObjectHierarchy(GameObject root, GameObjectConversionSettings settings)

But you have to get your GameObject first, ConvertToEntity is handy since it select that object the component pasted on to import to conversion world, plus additionally its children based on mode. Now you don't have that. Instead you can input only one GameObject to this method.

root is of course what you want to import into the conversion world before updating it. The name says "hierarchy", it knows how to do the following :

  • If this root is a prefab asset, then you get the same thing as declaring a prefab in conversion explained earlier. So if you don't like declaring and have the prefab entity generated together, you can do it one by one like this.
  • If this root is something on the scene, it converts an entire hierarchy like you add ConvertToEntity at the top of the tree, except it also perform LinkedEntityGroup processing as if you declared linked entity.

The destination world is inside the settings, which you could use :

public static GameObjectConversionSettings FromWorld(World destinationWorld, BlobAssetStore blobAssetStore)

To get one. Destination world is probably World.DefaultGameObjectInjectionWorld for most cases, unless you want conversion result elsewhere. ConvertToEntity always use this target internally, true to the "DefaultGameObjectInjectionWorld" name.

BlobAssetStore I guess is when you want the entity produced to have some compoenent with valid BlobAssetReference field. For now you could left it null or just new BlobAssetStore but please also Dispose it.

Other interesting methods on the utility

public static void ConvertScene(Scene scene, GameObjectConversionSettings settings)

Like ConvertGameObjectHierarchy, but do it for everything in the scene. It still won't add any LinkedEntityGroup buffer, except if something in the scene is disabled. Then you will get its disabled representation in Entity style, ready to be chain-enabled with entityManager.SetEnabled to only the top entity with the buffer.

This make it a bit superior than pasting ConvertToEntity on everything in the scene, since that way you cannot have a disabled converted entity result in the destination world. The disabled GameObject with ConvertToEntity wouldn't be Awake to be converted in the first place.

public static World ConvertIncrementalInitialize(Scene scene, GameObjectConversionSettings settings)
public static void ConvertIncremental(World conversionWorld, IEnumerable<GameObject> gameObjects, ConversionFlags flags)

What about these? Notice that the first one returned World. So it seems (I have not used it) when you are expecting something else to come into the scene later, then you can use the not-disposed-yet conversion world to convert all the new things incrementally.

They are used internally with the live link system, explained next.

Subscene

I am planning to stop now because subscene is probably in active development, and the usage change very soon. I will keep it at overview until the real release of Entities package.

You can now author the game in GameObject and get Entity when entering play mode! What else to desire?

  • You want to see the conversion result while in edit mode, even though you have carefully design the conversion to give equal result to the original. (e.g. MeshFilter + MeshRenderer = RenderMesh)
  • You want to convert everything and you are too lazy to paste ConvertToEntity or even use Scene method in the utility.
  • You want the game pre-converted to Entity when shipping. So when loading the scene, it is not loading, then ConvertToEntity / GameObjectConversionUtility, then you get Entity. It is that you load out Entity from the get go. Load chunk memory out and ready for use.
  • You want that, but you don't want to do something like pressing "serialize ECS memory" button in editor's play mode to save the memory out for runtime use, and add memory loader code to the scene's start up, etc.
  • While in play mode, even though they are all convert and destroyed to pure Entity, you don't want them to disappear from Hierarchy like you are in inject mode. YET you don't want the inject mode as you don't want to draw duplicated cube with normal renderer and Hybrid Renderer at the same time. What I am saying that this is scary as you can't touch anything anymore. No cube on hierarchy yet you see things. Is it possible to keep the hierarchy for inspection or some quick changes?
  • And even you may want to modify something (like moving Transform) and have it re-convert on the fly (to LocalToWorld / Translation) after each modification.

These features are possible with the Subscene! The name may say that it is somehow "lesser" than a scene, but that is just because it must be nested in a regular scene. It's ability is much more than regular scene.

To begin, create a GameObject and add Subscene component. You will be asked to add a new Scene asset which store subscene's content. Create one now.

Then you can click Edit and add content, or just double click the subscene scene asset and modify like normal. I have added my 2 cube GameObject that were needed help from ConvertToEntity before. When they are in subscene you can remove it as an entire subscene would be converted. It even has a warning that says so.

Now when you enter play mode, you get the best of both worlds. (no pun intended with ECS World) It is Hybrid Renderer rendered for sure since tris count is not increased from 50 from while in edit mode.

AND it is pickable. Meaning that clicking the Hierarchy could somehow select the preconverted state even though it is already converted and destroyed as you see right now.

Conversely, if you click on the scene view you can also pick the one on Hierarchy!

Notice that orange outline is missing. It is because of I have this option :

SceneView : Live Game State means that I would like the scene view showing converted result. So the scene view is a Hybrid Renderer rendered graphic and is uncapable of showing orange outline. Checking SceneView: Editing State will make the scene view in play mode looks like when you are not in play mode, that is, capable of showing orange outline. Of course the cube still looks the same as MeshFilter + MeshRenderer = RenderMesh was designed to look equal. But if the conversion somehow paint the cube green, by checking editing state it would turn back to grey.

The top choice Live Conversion in Edit Mode deals with only when out of play mode, so you could choose the choice and see converted state in edit mode. The SceneView: Live Game State being on while in edit mode but with Live Conversion in Edit Mode disabled is useless as you want to see live game state on scene view in edit mode but you hadn't allow it to.

If you move the classical Transform while in play mode (possible because subscene left the Hierarchy intact from their pre converted state), it would be updated (reconverted, diff-ed) to the converted state seamlessly. This is called the live link.

Comparing ConvertToEntity to Subscene converted entity

The first one is by ConvertToEntity on the first cube, the second one by placing both in Subscene. Both are based on the destroy mode, so no component object like MeshFilter will be present on the converted entity like inject mode.

EditorRenderData, EntityGuid, SceneSection, SceneTag were added. These are all ISharedComponentData except EntityGuid. So you don't have to worry about chunk fragmentation as everyone in the subscene would get the same thing. It is used for.. various thing.

Neither ConvertToEntity nor subscene would add LinkedEntityGroup buffer to represent the hierarchy inside the subscene. But the subscene way follow the same rule as using ConvertScene on the utility class, where it would add LinkedEntityGroup only if something are disabled, so you could enable back together.

It's not a conversion, it's a scene!

They don't want you to think that sub scene is a bunch of ConvertToEntity, it is really a scene.

That include this behaviour when you change the scene with SceneManager.LoadScene. GO has ConvertToEntity while InSubScene1 has nothing but is inside a subscene.

When enter play mode we get both converted as expected. Subscene and ConvertToEntity are equal to this point.

However when changing to an another scene :

It know to also delete entities related to the subscene! While the ConvertToEntity really means it, just convert to entity then they are completely disconnected from the scene. This could help with entity lifecycle greatly. (So systems that interested in them, all completely stop until you enter the scene, etc.)

I think DeclareDependency should be something that make this work more seamlessly in more complex cases.

Subscene differentiation rules

When you touch your "shadow" Hierarchy while in play mode, sure it performs the conversion all over again. But for example while modifying classical Transform so the reconversion to Translation take place, you see that all other components that the Entity get still remains while inheriting a reconverted transforms.

There must be some merging rules we should know in order to design a conversion code / system code that is "live link compatible". (So it helps making/debugging a game, because on actual runtime you cannot touch Hierarchy.) The reconverted world get "diffed" into the currently running world. It knows the reconverted entity is in fact the same as which one in the running world and pair them up to merge. (The EntityGuid)

Surely there is a limit to this magic. I wouldn't expect everything to work magically when I reparent things or even drag them in/out of sub scene while in play mode...

For now I don't know much, I would add later if I do. But for example if your converted Entity would ended up receiving more Entity as its children, you would want the reconversion to not disrupt those children's relation ship while also replacing subset of components on the parent.


This article is too long now. I would add more to subscenes as more features are added. Have fun authoring ECS game in GameObject!