Search Unity

Resolved Instantiating Text in ECS

Discussion in 'Graphics for ECS' started by xindexer2, Sep 6, 2021.

  1. xindexer2

    xindexer2

    Joined:
    Nov 30, 2013
    Posts:
    78
    I need to instantiate several hundred labels for my project. So far I've been able to build my project with 100% ECS and I'd like to continue to do that but I hit a wall with the labels.

    I have been able to convert a GO TextMeshPro using convert and inject gameobject. I can then manipulate the entity in code but I have two copies, one in ECS and one in Mono - and the Mono GO is drawn where I originally placed it and I can't delete it or the entity disappears. I don't understand the point of having it in both environments - but I have done very little research into the conversion process as I haven't needed to move any prefabs over. Convert and destroy doesn't work either.

    I also have not been able to make a copy of this in ECS. Is this a 1-1 ratio? One GO for one entity? Is my only solution to instantiate everything in Mono and then convert it to ECS? There was a pure ECS UI package developed 2 years ago that appeared to do what I'm looking for but it was built on the older entities code base and hasn't been touched since. (https://github.com/supron54321/DotsUI)

    This post (https://forum.unity.com/threads/textmesh.1137778/) talks about using this code to convert:

    Code (CSharp):
    1. public class TextMeshProCompanion : MonoBehaviour, IConvertGameObjectToEntity
    2.     {
    3.         public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    4.         {
    5.             conversionSystem.AddHybridComponent(GetComponent<TextMeshPro>());
    6.             conversionSystem.AddHybridComponent(GetComponent<RectTransform>());
    7.             conversionSystem.AddHybridComponent(GetComponent<MeshRenderer>());
    8.         }
    9.     }
    Does this do anything different than just the convert and inject code?

    And one of the suggestions a little further down said to use
    Code (CSharp):
    1. Unity.Transforms.CopyTransformToGameObject
    to push the transform back to the GO. How do you use this function? and again - do I really need to maintain two copies of everything?

    I have been putting a lot of work into the jobs and rewriting my code to be able to use the threads and I guess I just don't understand the conversion process very well.

    Thanks.
     
  2. Srokaaa

    Srokaaa

    Joined:
    Sep 18, 2018
    Posts:
    169
    Unity.Transforms.CopyTransformToGameObject
    is not a function, it is a component. Just add it to your object and it will automatically synchronize your GO transform with entity transform.

    No, not everything but there are some parts of Unity (line TextMeshes) that didn't get their entities counterparts so that's when you use hybrid components
     
  3. xindexer2

    xindexer2

    Joined:
    Nov 30, 2013
    Posts:
    78
    Ok, this took a lot of effort but I have it working. I couldn't find any tutorial on how to do this so here is how I got it to work. But first, my setup: Entities 0.17 preview 42, Unity 2020.3.16f1 HDRP project, TMP 3.0.6 (modified with new shaders from here - https://forum.unity.com/threads/plans-for-hdrp-compatibility-for-tmp.660598/page-2#post-7081120). I moved the TextMeshPro folder out of the "Library" folder and into the "Packages" folder, then deleted all of the shaders from 3.0.6 and replaced them with the ones from the forum - if you leave the 3.0.6 shaders in the project then you will get some errors. I was unable to get the 3.2.0 preview tmp package to work.

    If you only need a handful of labels then what I describe below is overkill. If you only need a few then do this:

    Create a TMP Gameobject, and have it "Convert and Inject Game Object" this will generate two copies of the TMP, one in the mono and one in the ECS wold. If you only need to translate the label (i.e. no rotation) then you can add the "CopyTransformToGameObject" IComponentData as talked about above and then use "Translation" in the ECS world and the two copies will sync up. If you need to rotate then you need to do a little more work.

    First add a tag:

    Code (CSharp):
    1. using Unity.Entities;
    2. [GenerateAuthoringComponent]
    3. public struct TextTag : IComponentData {}
    4.  
    The TMP GameObject uses a RectTransform instead of a Transform and the ECS Rotation does not work. In order to rotate the Gameobject (and keep the ECS and GO in sync), you need to gain access to the RectTransform and then rotate that manually. They talk about not doing this in the docs but it's the only way I could make it work.

    Code (CSharp):
    1. Entities.WithAll<TextTag>().ForEach((Entity entity) =>
    2. {
    3. EntityManager.RemoveComponent<TextTag>(entity);          
    4. var tmp= EntityManager.GetComponentObject<TextMeshPro>(entity);
    5. tmp.text = "hello world";
    6. tmp.fontSize = 128;
    7. var recT = EntityManager.GetComponentObject<RectTransform>(entity);
    8. recT.position = new float3(0, t, 0);
    9. recT.rotation = Quaternion.Euler(0, 0, 1.578f);
    10. }).WithStructuralChanges().WithoutBurst().Run();
    This works for a small number of TMP's but if you need hundreds then do this:

    The basic concepts for the solution came from the "StressTests/HybridComponent" folder in the ECCS sample projects where they are injecting a non-ECS-supported object into the ECS world (a gizmo in their case).

    Create the TextTag from above
    Create this IComponentData file:

    Code (CSharp):
    1. using Unity.Entities;
    2.  
    3. namespace Optkl.Data
    4. {
    5.     public struct TextMeshHybrid : IComponentData
    6.     {
    7.         public Entity prefab;
    8.     }
    9. }
    Create an empty gameobject (Spawner), set it to "Convert and Destroy" and attach this script to it:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Optkl.Data;
    3. using Unity.Entities;
    4. using UnityEngine;
    5.  
    6. // ReSharper disable once InconsistentNaming
    7. [AddComponentMenu("Spawner")]
    8. [ConverterVersion("joe", 1)]
    9. public class TextMeshAuthoringHybrid : MonoBehaviour, IConvertGameObjectToEntity, IDeclareReferencedPrefabs
    10. {
    11.     public GameObject prefab;
    12.  
    13.     public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    14.     {
    15.         dstManager.AddComponentData(entity, new TextMeshHybrid()
    16.         {
    17.             prefab = conversionSystem.GetPrimaryEntity(prefab)
    18.         });
    19.     }
    20.  
    21.     public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    22.     {
    23.         referencedPrefabs.Add(prefab);
    24.     }
    25. }
    26.  
    this code will give you access to the "Prefab" in ECS than you can create copies from. You can add this script to your empty gameobject by selecting the "AddComponent" button in the inspector and then selecting "Spawner" or you can change the [AddComponentMenu...] to [GenerateAuthoringScript] and drag it onto the empty gameobject.

    Add this to your system folder:
    Code (CSharp):
    1. using TMPro;
    2. using Unity.Entities;
    3. using UnityEngine;
    4.  
    5. namespace Optkl.Utilities
    6. {
    7.     [ConverterVersion("Flash", 1)]
    8.     public class TextMeshConversion :  GameObjectConversionSystem
    9.     {
    10.         protected override void OnUpdate()
    11.         {
    12.             Entities.ForEach((Entity entity, TextMeshPro textMeshPro, RectTransform rectTransform, MeshRenderer meshRenderer) =>
    13.             {
    14.                 AddHybridComponent(textMeshPro);
    15.                 AddHybridComponent(rectTransform);
    16.                 AddHybridComponent(meshRenderer);
    17.             });
    18.         }
    19.     }
    20. }
    21.  
    This is the hybrid injection process where it takes the Gameobject components and gives you access to them in ECS.

    Finally to instantiate the objects you can do this (you can also access things in an Enties.Foreach Loop if you want but did it with a query):

    Code (CSharp):
    1. var entityTickLabelsQuery = GetEntityQuery( typeof(TextMeshHybrid));
    2. var textPrefabParent = entityTickLabelsQuery.ToEntityArray(Allocator.Temp)[0];
    3. var textPrefab = EntityManager.GetComponentData<TextMeshHybrid>(textPrefabParent);
    4. var _tempTextArray = new NativeArray<Entity>(_tickLabelTranslation.Length, Allocator.Persistent);
    5. EntityManager.Instantiate(textPrefab.prefab, _tempTextArray);
    6. for (var i = 0; i < _tempTextArray.Length; i++)
    7. {
    8. EntityManager.SetComponentData<Translation>(_tempTextArray[i], _tickLabelTranslation[i]);
    9. EntityManager.SetComponentData<Rotation>(_tempTextArray[i], _tickLabelRotation[i]);
    10. var tmp = EntityManager.GetComponentObject<TextMeshPro>(_tempTextArray[i]);
    11. tmp.text = _tickLabel[i].ToString();
    12. tmp.fontSize = 350;
    13. }
    The "_tickLabelTranslation" and "_tickLabelRotation" are NativeLists that I fill in a job using this (partial code):

    Code (CSharp):
    1. var _tickLabelTranslation = new NativeList<Translation>(iris.Length, Allocator.TempJob);
    2. var _tickLabelRotation = new NativeList<Rotation>(iris.Length, Allocator.TempJob);
    3. var _tickLabel = new NativeList<int>(iris.Length, Allocator.TempJob);
    4. var buildTickJobFor = new BuildTickJobFor
    5. {
    6. tickLabelTranslation = _tickLabelTranslation,
    7. tickLabelRotation = _tickLabelRotation,
    8. tickLabel = _tickLabel
    9. };
    10.  
    11. buildTickJobFor.ScheduleParallel(iris.Length, 512, new JobHandle())
    and the job is something like this (partial code):
    Code (CSharp):
    1.  [BurstCompile(CompileSynchronously = true)]
    2. private struct BuildTickJobFor : IJobFor
    3. {
    4. [ReadOnly] (a bunch of readonly variables)
    5.  
    6. [NativeDisableParallelForRestriction] [WriteOnly] public NativeList<Translation> tickLabelTranslation;
    7.          
    8. [NativeDisableParallelForRestriction] [WriteOnly] public NativeList<Rotation> tickLabelRotation;
    9.          
    10. [NativeDisableParallelForRestriction] [WriteOnly] public NativeList<int> tickLabel;
    11.  
    12. void IJobFor.Execute(int i)
    13. {
    14. ....
    15. tickLabelTranslation.Add(new Translation
    16. {
    17.     Value = new float3((radius + 125) * math.cos(theta), (radius + 125) * math.sin(theta), 0)
    18.  });
    19.              
    20. tickLabelRotation.Add(new Rotation()
    21. {
    22.     Value = quaternion.Euler(0, 0, theta - math.PI / 2)
    23. });
    24. tickLabel.Add(i);
    25. }
    26. .....
    27.  
    I removed a bunch of the code from the job system but I wanted to give you an idea of where those variables came from.

    Hope this helps somebody out.
     
    Last edited: Sep 13, 2021
    apkdev likes this.