Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

UnityECS using ScriptableObjects for object references in the inspector

Discussion in 'Entity Component System' started by WallysWorkshop, Jun 17, 2018.

?

Good or Bad?

Poll closed Jul 4, 2018.
  1. Good

    100.0%
  2. Bad

    0 vote(s)
    0.0%
  1. WallysWorkshop

    WallysWorkshop

    Joined:
    Apr 16, 2018
    Posts:
    2
    I've been looking into the ECS system in Unity for a short while and I am thrilled to be using it. I've been watching videos and going through the example code mostly interested in Pure ECS. I've been bothered that all the examples I've found including Unity's own are using Gameobjects to hold MeshInstanceRenderComponents. It seems the only reason they are needed is to get the Mesh and Material data into the code from within the editor using the inspector. I propose creating the Mesh instance renderer in code, and providing the Mesh and Material data as hard assets using a Single Responsibility Principal to promote modular, reusable data structures.

    What I've found is using Scriptable Objects as interfaces between the editor and the pure ECS code is really easy, and actually promotes good data structure in my humble opinion. By creating generic scriptable objects and then storing an instance of that SO in a Resources folder allows you to load that data reference strictly from code during a load or application startup. The downside obviously is the data used to store the scriptable objects in the Resources directory. I also don't know the long-term viability of using the Resources class in this way, but it is extremely effective and I hope it is not removed, or a better similar system will be created to take its place.

    I have a GitHub repo available with an example of this here: Github repo

    The idea goes something like this, you create a MeshDataObject as a scriptable. It will hold a reference to a Mesh. You have another for the Material (MaterialDataObject). Then a RenderDataObject which holds a reference(s) to MeshDataObjects and MaterialDataObjects. Now you essentially have data components and data systems. They are reusable and modular, you create an instance for each unique object this data is used for. I.E. you create a LevelMeshData as a MeshDataObject instance, it will store the Mesh for the 'Level' object.


    Code (CSharp):
    1.  
    2. [CreateAssetMenu(fileName = "MeshData", menuName = "MeshDataObject")]
    3. public class MeshDataObject : ScriptableObject
    4. {
    5.     public Mesh Value;
    6. }
    7.  
    8. [CreateAssetMenu(fileName = "MaterialData", menuName = "MaterialDataObject")]
    9. public class MaterialDataObject : ScriptableObject
    10. {
    11.     public Material Value;
    12. }
    13.  
    14. [CreateAssetMenu(fileName = "RenderData", menuName = "RenderDataObject")]
    15. public class RenderDataObject : ScriptableObject
    16. {
    17.     public MeshDataObject Mesh;
    18.     public MaterialDataObject Material;
    19. }
    20.  
    21. public class Bootstrap {
    22.  
    23.     public static EntityManager entityManager;
    24.     public static EntityArchetype LevelMapArchetype;
    25.     public static RenderDataObject LevelRenderData;
    26.  
    27.     // Initialize a reference to the world's EntityManager and define Archtypes.
    28.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    29.     public static void Initialize()
    30.     {
    31.         entityManager = World.Active.GetOrCreateManager<EntityManager>();
    32.         DefineArchetypes();
    33.     }
    34.  
    35.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    36.     public static void InitializeWithScene()
    37.     {
    38.         LevelRenderData = Resources.Load("Data/LevelRenderData") as RenderDataObject;
    39.         NewGame();
    40.     }
    41.  
    42.     public static void DefineArchetypes()
    43.     {
    44.         // THIS WORKS.
    45.         LevelMapArchetype = entityManager.CreateArchetype(
    46.             typeof(Position), typeof(TransformMatrix));
    47.  
    48.         // DOES NOT WORK THIS WAY!
    49.         //LevelMapArchetype = entityManager.CreateArchetype(
    50.         //  typeof(Position), typeof(TransformMatrix), typeof(MeshInstanceRenderer));
    51.     }
    52.     public static void NewGame()
    53.     {
    54.         var cube = entityManager.CreateEntity(LevelMapArchetype);
    55.         entityManager.SetComponentData(cube, new Position
    56.         {
    57.             Value = new float3(0.0f, 0.0f, 0.0f)
    58.         });
    59.  
    60.         // The line below adds a second MeshInstanceRenderer and will cause an error
    61.         // if you also include it in the Archetype definition above.
    62.         entityManager.AddSharedComponentData (cube, new MeshInstanceRenderer
    63.         {
    64.             mesh = LevelRenderData.Mesh.Value,
    65.             material = LevelRenderData.Material.Value
    66.         });
    67.        
    68.     }
    69. }
    70.  
     
  2. I like the idea.

    Although I have a question, what is your reasoning behind the idea to encapsulate the Material and the Mesh in separate ScriptableObjects? These are assets on their own right, are you planning to add tweak data to the MeshDataObject and/or the MaterialDataObject?

    I would drop those two and simply reference the Material and the Mesh in the RenderDataObject, so the AddSharedComponentData call would only contain the
    Code (CSharp):
    1.             mesh = LevelRenderData.Mesh,
    2.             material = LevelRenderData.Material
    Am I missing something?
     
  3. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    To me this sounds like a great idea and i very much prefer working like this than with dummy gameObjects. Moreover, the new Addressables system for loading resources will probably make this even more elegant

    But I think nothing prevents you from adding the meshInstanceRenderer in your DefineArchetype(), but then later instead of adding a meshInstanceRenderer again, you can simply set its data
     
    Last edited: Jun 17, 2018
    Walter_Hulsebos likes this.
  4. WallysWorkshop

    WallysWorkshop

    Joined:
    Apr 16, 2018
    Posts:
    2
    @PhilSA for the Archetype definition, you are correct, I suspected there might be a way to set the data rather than add it (I just looked at that before i stopped working last night and had a feeling I could approach that differently) Just hadn't gotten around to researching it. I've only been working with the ECS system for a few days, I have a long way to go to get caught up. The dummy game objects were really bothering me though so wanted to find a better solution.

    Now @LurkingNinjaDev, You can use this method of interfacing with the editor in any way you want. You can have one massive datastore with everything in one place, you could separate and have all Materials in a file, and all Meshes in a file or you can separate the data more as I have done. It's really up to you and your project. However, by separating the data elements and the systems of data you can, for instance, reuse a Mesh or Material you have already loaded for multiple RenderDataObject definitions. Not sure I'm saying this right, but basically the data system is like an entity and the elements are like the components, you can mix and match and share the data between multiple objects to prevent multiple instances of similar data being stored. More specifically say you have a low poly game with limited materials, and there is a green color used for various vegetation. you can create different RenderData's that have different meshes but share the material.

    EDIT:
    I'm using very specific examples to demonstrate a broad idea because I am lacking the words to describe it in a generalized fashion at the moment. This isn't limited to Mesh's and Materials and a RenderData system. All manner of data that is normally set in the inspector can be used in this way. Also, the idea of limiting the responsibility of an object helps with debugging. When you do inevitably forget to set a value in the inspector and everything blows up you should, in theory, be able to check the output and be able to determine what blew up from where it blew up. As with any debugging, this gets harder the larger a project becomes.

    Also @PhilSA I just pushed an update cleaning up the Archetype definition, changed AddSharedComponentData to SetSharedComponentData.
     
    Last edited: Jun 17, 2018
    Lurking-Ninja likes this.