Search Unity

architecture with Scriptable Objects and ECS

Discussion in 'Entity Component System' started by ElBuho, Sep 23, 2018.

  1. ElBuho

    ElBuho

    Joined:
    Mar 9, 2015
    Posts:
    32
    Hi all.

    Lately I’ve been wondering if this new ECS Architecture is combinable with the one based on Scriptable Objects and events explained by Ryan Hipple and Richard Fine some time ago. (nore information on the following link: https://unity3d.com/how-to/architect-with-scriptable-objects).
    They are using Scriptable Objects to separate the data from the logic, which is what ECS pretends (among other things). I love the architecture described by Ryan because it’s based on the SOLID principles and it’s very flexible and reusable.

    If I’m not mistaken, Scriptable Objects aren’t game objects, so I suppose that they can be combined with ECS, but I don’t know what should be its place; I mean, how they should be used? as a components because they store data? But they can also contain methods, so.... Or they must be used in the same way as always, simply as an asset. Or maybe I'm wrong and they shouldn't be combined.

    What do you think?

    As you can see I’m mixed up, so any help will be appreciated ;)

    Thanks in advance.
     
    nanobot_games and davtam like this.
  2. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    ECS doesn't pretend to separate data from logic. It separates data from logic.
    Scriptable objects are not game objects and neither are components. Scriptable objects get serialized for you. They don't get tired to any particular scene which makes them powerful for working with Unity's systems.

    I use SO's for organising data. They hold lists of prefabs I can instantiate, or define properties of an enemy. They are fantastic data blobs with some easy Unity functionality. Do you know specifically where and why you want to use them with ECS?
     
  3. Simeon

    Simeon

    Joined:
    Sep 26, 2012
    Posts:
    50
    I use scriptable objects quite often for properties of objects, like items and enemies.

    I've found that the best way to use them with ECS is to store structs of data in them and use the new Addressables system. You can use a Hash128 as a pointer in IComponentData. You can also use ISharedComponentData to keep a cached reference to a scriptable object and then just pass its data structs to a job directly.

    I bet you could even use a scriptable object to hold component data without needing a game object with a transform and then just creating entity archetypes from it. One would need to write a custom editor and a serialization callback for that though.
     
  4. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Recently I have been using the Scriptable objects as scene settings and letting my systems read from that without having objects in the scene outside of the environment.
     
  5. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    Would be nice to actually see more use of scriptable objects in general. I believe them to be criminally under-utilised.
     
  6. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    Well the new Addressable system is a blessing. I use it to get all my Prefabs and ScriptableObjects into the ECS.
    Well you still need a Monobehaviour and therefore a Sceneobject to load it. But you can use it in ECS everythere...

    Code (CSharp):
    1.  
    2. public static class ArchetypeDatabase {
    3.  
    4.     static ArchetypeDatabase() {
    5.         new GameObject("AssetLoader", typeof(AssetLoader));
    6.     }
    7.  
    8.     public static EntityArchetype TurnEndedCommand { get; private set; }
    9.     public static EntityArchetype AnimatorTriggerEvent { get; private set; }
    10.  
    11.     public static Dictionary<string, GameObject> ChessFigureDic { get; private set; } = new Dictionary<string, GameObject>();
    12.  
    13.     public static bool Loaded { get; private set; } = false;
    14.     public static GameObject GetAsset(string type) {
    15.         if(!ChessFigureDic.TryGetValue(type, out GameObject gameObject)) {
    16.             return null;
    17.         }
    18.         return gameObject;
    19.     }
    20.  
    21.     public static IEnumerator LoadDatabase() {      
    22.         if (!Loaded) {
    23.  
    24.             EntityManager entityManager = World.Active.GetExistingManager<EntityManager>();
    25.             TurnEndedCommand = entityManager.CreateArchetype(typeof(TurnEndedCommand));
    26.             AnimatorTriggerEvent = entityManager.CreateArchetype(typeof(AnimatorEvent), typeof(SetAnimatorTrigger));
    27.  
    28.             yield return Addressables.LoadAssets<GameObject>("default", op => {
    29.                 Debug.Log(op.Result.name);
    30.                 ChessFigureDic.Add(op.Result.name, op.Result);
    31.             });
    32.             Loaded = true;
    33.         }
    34.     }
    35. }
    36.  
    37. public class AssetLoader : MonoBehaviour {
    38.     // Use this for initialization
    39.     IEnumerator Start() {
    40.         yield return ArchetypeDatabase.LoadDatabase();
    41.         Destory(gameObject)
    42.     }
    43. }
    44.  
    45.  
    46. //Usage in Systems:
    47. GameObject prefab = ArchetypeDatabase.GetAsset("MyPrefab");
    48.  
    49.  
    The thing you could do to work with ScriptableObjects inside ECS:

    Code (CSharp):
    1.  
    2. struct MyData : IComponentData {
    3.      //... Data
    4. }
    5. class MyScriptableAsset :  ScriptableObject {
    6.      public MyData MyData;
    7. }
    Now you can add the data of your ScriptableObject directly to an entity;
    You can also write Systems / Editor that will sync your assets with your entities when something changed on the assets.

    Maybe I'll implement such thing later on...

    Some info about how you get the Addressable System
     
    Last edited: Sep 26, 2018
  7. Simeon

    Simeon

    Joined:
    Sep 26, 2012
    Posts:
    50
    It would be great if unity adds an interface like the ISerializationCallbackReceiver but one that allows you to write to a buffer or a yaml dictionary. That would be a godsend for all generic or inherited data serialization, instead of storing it in a string and messing up version control.
     
    Last edited: Sep 28, 2018
    Walter_Hulsebos likes this.
  8. ElBuho

    ElBuho

    Joined:
    Mar 9, 2015
    Posts:
    32
    Thank you all for giving me this information.

    I'm going to use it and start working on Scriptable Objects and ECS. If I get some really interesting result I'll share it with all of you.

    Thank you again ;)
     
    hopeful and Spy-Shifty like this.
  9. roboryantron

    roboryantron

    Joined:
    Apr 2, 2014
    Posts:
    5
    Using Scriptable Objects to store small data elements as I described it in my talk would not achieve ECS benefits. The significant performance gains of ECS come from linear memory access. ScriptableObjects are allocated "randomly" on the heap so it is not possible to leverage locality of reference. Additionally, anything inheriting from UnityEngine.Object has a lot of extra data that would fill up the cache pretty fast. Data must be a value type (int, struct, float) in order to leverage ECS.
     
  10. ElBuho

    ElBuho

    Joined:
    Mar 9, 2015
    Posts:
    32
    Hello Ryan.

    Thank you very much for participating in this thread.
    Considering what you say, I'm going to forget completely the Scriptable Objects.
    By the way, if you have some tips to give, please, do not hesitate to share them with us;)

    Thanks again and greetings.
     
  11. Krajca

    Krajca

    Joined:
    May 6, 2014
    Posts:
    347
    But still you can have SO in project and convert it to more appropriate data at the start of the app.
    SO are, in my opinion, best option for cooperation with designers and all non-developer team members.
     
    closingiris, Leonidas85 and msfredb7 like this.
  12. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    There is definitely a time and a place for everthing, as they say. No reason to write off SO's completely just because it was mentioned that they aren't the best thing to use in all situation. In the situations which they can be useful, they are great. I need to start using them more myself, I keep forgetting about them, lol.
     
    Krajca likes this.
  13. Leonidas85

    Leonidas85

    Joined:
    Mar 11, 2015
    Posts:
    14
    I find ScriptableObjects are great for configuration, especially if you don't want to clutter up your scene with 'configuration' gameObjects.
    It's important to make a distinction between authoring data and ECS data though; it's fine to expose human-friendly data structures on ScriptableObjects for easy editing but when using that data with ECS you should turn that authoring data into ComponentData structs and never use a reference to the actual ScriptableObject, otherwise ECS can't optimize appropriately.

    ScriptableObjects require some creativity when combined with ECS though, especially if you want to use entity prefabs with them, but it's definitely possible.

    For example I wanted to use ScriptableObjects to configure the available player prefabs so that I can spawn a specific prefab by an enum value.
    Of course you want those player prefabs to be spawned as entities, so there's a bit of difficulty involved since the ECS example (6. SpawnFromEntity) that explains spawning from an entity prefab uses a game object in the scene that references the entity prefab and has the IConvertGameObjectToEntity interface implemented, whereas with a ScriptableObject you don't have a scene object to do this for you.

    Luckily you can use GameObjectConversionUtility.ConvertGameObjectHierarchy() to convert the prefab to an entity prefab at run-time without a scene object to do so for you. It's my theory that the conversion is relative to the World (scene) and needs to be done seperately for each World the entity prefab is to be used in, which explains the example implementation with the object in the scene. So if you use GameObjectConversionUtility.ConvertGameObjectHierarchy() you'll need to pass World.Active in the second argument. There's also a GameObjectConversionUtility.ConversionFlags argument for which I'm currently only using AssignName, not sure what the other flags do.

    Here's my implementation for a 'PlayerPrefabsLibrary' ScriptableObject using addressables and ECS and for spawning a player prefab with it based on a SpawnpointComponent placed in a loaded level:

    Code (CSharp):
    1.  
    2. using System;
    3. using System;
    4. using System.Threading.Tasks;
    5. using UnityEngine;
    6. using UnityEngine.AddressableAssets;
    7. using UnityEngine.ResourceManagement.AsyncOperations;
    8.  
    9. /// <summary>
    10. /// A configuration to map player entity prefabs to PlayerPrefabType using the Addressables system.
    11. /// </summary>
    12. public class PlayerPrefabLibrary : ScriptableObject
    13. {
    14.     [Serializable]
    15.     public class PlayerPrefabConfiguration
    16.     {
    17.         public PlayerPrefabType Type => type;
    18.         [SerializeField] private PlayerPrefabType type;
    19.  
    20.         public AssetReference PlayerEntityPrefab => playerEntityPrefab;
    21.         [SerializeField] private AssetReference playerEntityPrefab;
    22.     }
    23.  
    24.     [SerializeField] private PlayerPrefabConfiguration[] prefabConfigurations;
    25.  
    26.     public async Task<GameObject> LoadPlayerPrefab(PlayerPrefabType type)
    27.     {
    28.         GameObject result = null;
    29.         foreach(PlayerPrefabConfiguration prefabConfiguration in prefabConfigurations)
    30.         {
    31.             if(prefabConfiguration.Type == type)
    32.             {
    33.                 AsyncOperationHandle<GameObject> handle = prefabConfiguration.PlayerEntityPrefab.LoadAssetAsync<GameObject>();
    34.                 await handle.Task;
    35.                 result = handle.Result;
    36.             }
    37.         }
    38.  
    39.         return result;
    40.     }
    41. }
    Code (CSharp):
    1.  
    2. using System.Threading.Tasks;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8. using UnityEngine.AddressableAssets;
    9. using UnityEngine.ResourceManagement.AsyncOperations;
    10.  
    11. /// <summary>
    12. /// Spawns the player at the next opportune moment.
    13. /// Note that this does not use a job and instead simply executes in OnUpdate() since it is only a single operation.
    14. /// </summary>
    15. [UpdateInGroup(typeof(SimulationSystemGroup))]
    16. public class PlayerSpawningSystem : ComponentSystem
    17. {
    18.     private PlayerPrefabLibrary playerPrefabLibrary;
    19.     private Task<PlayerPrefabLibrary> playerPrefabLibraryLoadingTask;
    20.  
    21.     private BeginInitializationEntityCommandBufferSystem entityCommandBufferSystem;
    22.     private EntityQuery spawnPointQuery;
    23.  
    24.     private int queuedSpawnpointID;
    25.     private GameObject queuedPrefab;
    26.  
    27.     public async Task QueueSpawnPlayer(int spawnpointID, PlayerPrefabType prefabType)
    28.     {
    29.         if(playerPrefabLibrary == null && playerPrefabLibraryLoadingTask == null)
    30.         {
    31.             playerPrefabLibraryLoadingTask = LoadPlayerPrefabLibrary();
    32.         }
    33.         await playerPrefabLibraryLoadingTask;
    34.         playerPrefabLibrary = playerPrefabLibraryLoadingTask.Result;
    35.  
    36.         GameObject result = await playerPrefabLibrary.LoadPlayerPrefab(prefabType);
    37.         if (result == null)
    38.         {
    39.             Debug.LogError($"Attempting to spawn player prefab '{prefabType}' at spawnpoint '{spawnpointID}' but the prefab could not be loaded!");
    40.             queuedPrefab = null;
    41.             return;
    42.         }
    43.  
    44.         queuedPrefab = result;
    45.         queuedSpawnpointID = spawnpointID;
    46.     }
    47.  
    48.     private async Task<PlayerPrefabLibrary> LoadPlayerPrefabLibrary()
    49.     {
    50.         AsyncOperationHandle<PlayerPrefabLibrary> handle = Addressables.LoadAssetAsync<PlayerPrefabLibrary>("Assets/Data/Configurations/PlayerPrefabLibrary.asset");
    51.         await handle.Task;
    52.         return handle.Result;
    53.     }
    54.  
    55.     protected override void OnCreate()
    56.     {
    57.         entityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
    58.         spawnPointQuery = GetEntityQuery(ComponentType.ReadOnly<SpawnPointComponent>(), ComponentType.ReadOnly<LocalToWorld>());
    59.     }
    60.  
    61.     protected override void OnUpdate()
    62.     {
    63.         if (queuedPrefab == null)
    64.         {
    65.             return;
    66.         }
    67.  
    68.         NativeArray<SpawnPointComponent> spawnPoints = spawnPointQuery.ToComponentDataArray<SpawnPointComponent>(Allocator.TempJob);
    69.         NativeArray<LocalToWorld> spawnPointLocations = spawnPointQuery.ToComponentDataArray<LocalToWorld>(Allocator.TempJob);
    70.         bool spawned = false;
    71.         for(int i = 0; i < spawnPoints.Length; i++)
    72.         {
    73.             if (spawnPoints[i].ID == queuedSpawnpointID)
    74.             {
    75.                 Entity entityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy(queuedPrefab, new GameObjectConversionSettings(World.Active, GameObjectConversionUtility.ConversionFlags.AssignName));
    76.                 EntityCommandBuffer commandBuffer = entityCommandBufferSystem.CreateCommandBuffer();
    77.                 Entity playerEntity = commandBuffer.Instantiate(entityPrefab);
    78.                 commandBuffer.SetComponent(playerEntity, new Translation { Value = spawnPointLocations[0].Position + new float3(0, 0.5f, 0) });
    79.                 Debug.Log($"Spawning player prefab at spawnpoint '{queuedSpawnpointID}'");
    80.                 spawned = true;
    81.                 break;
    82.             }
    83.         }
    84.         if (!spawned)
    85.         {
    86.             Debug.LogError($"Attempting to spawn player at spawnpoint '{queuedSpawnpointID}' but the spawn point was not found!");
    87.         }
    88.  
    89.         queuedPrefab = null;
    90.         spawnPoints.Dispose();
    91.         spawnPointLocations.Dispose();
    92.     }
    93. }
    94.  
    Code (CSharp):
    1.  
    2. using Unity.Entities;
    3. using UnityEngine;
    4.  
    5. [SerializeField]
    6. public struct SpawnPointComponent : IComponentData
    7. {
    8.     public int ID;
    9. }
    10.  
    Code (CSharp):
    1.  
    2. public enum PlayerPrefabType
    3. {
    4.     DEFAULT
    5. }
     
    Last edited: Sep 25, 2019
    bb8_1, desper0s, SenseEater and 7 others like this.
  14. typicalturnip

    typicalturnip

    Joined:
    Aug 10, 2018
    Posts:
    11
    New to SO and DOTs: has the new update streamlined the conversion of data and functions on SO to ECS?

    Just discovered SO a little bit ago, and I went through the DOTs documentation earlier today. I am trying to figure out a method to enable editing and value setting to be done via SO but have it convert into the ECS framework, but appear to have missed it.

    I found the GameObject to Entity, and I'm assuming Class ConverToEntity is what I am looking for?
     
  15. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    The methods described in this thread are still the way to go. You create a regular scriptable object then load it in your system using Resources.Load or Addressables. You could also use the conversion system on a "configuration" monobehaviour and access it through queries. The former seems like a better fit for config data.

    You could also use blob assets if you want to explicitly store your data in components.
     
  16. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    You should abuse GetSingleton for managers/settings. Therefore find a way to get your SO into component object type on singleton entity with conversion, and then write a helper method like : EntityManager.GetComponentObject<SO>(GetSingletonEntity<SO>) then you can get your SO in any systems. You can also use RequireSingletonForUpdate<SO> to make a system that not run when settings aren't available. (e.g. not in the scene where it would be converted yet, then exiting the scene you destroy the singleton SO entity)
     
  17. axxessdenied

    axxessdenied

    Joined:
    Nov 29, 2016
    Posts:
    33
    This thread is a gold. Thanks for all the info everyone!
     
    desper0s, bb8_1 and Opeth001 like this.
  18. Awltux

    Awltux

    Joined:
    Apr 4, 2021
    Posts:
    1
    I know this is an old thread, but I found my way here looking for ideas on how to integrate "Unity Atoms and ECS", so others may end up here too.
    This article seems to show a good example of how to use SO and ECS together: ECS working with scriptable objects for crafting
    I've not tried to use it yet, but after a read through, it seems that it might work with Unity Atoms - after some hacking.
     
    bb8_1 likes this.