Search Unity

  1. Check out the Unite LA keynote for updates on the Visual Effect Editor, the FPS Sample, ECS, Unity for Film and more! Watch it now!
    Dismiss Notice
  2. The Unity Pro & Visual Studio Professional Bundle gives you the tools you need to develop faster & collaborate more efficiently. Learn more.
    Dismiss Notice
  3. Improved Prefab workflow (includes Nested Prefabs!), 2D isometric Tilemap and more! Get the 2018.3 Beta now.
    Dismiss Notice
  4. Improve your Unity skills with a certified instructor in a private, interactive classroom. Watch the overview now.
    Dismiss Notice
  5. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

I get the impression that ECS means I have to write way more code to do the same thing?

Discussion in 'Entity Component System and C# Job system' started by Arowx, Jun 7, 2018.

  1. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    I have to break down objects into processes and their data, miniaturising the processes and data needed by them.

    So high level abstractions that can make programming easier and fun e.g. Ork.Attack(Enemy);

    The ECS version (pseudo code):
    1. CombatManager.AddJob(Attack(ref attacker, ref defender));
    2. HealthManager.AddJob(UpdateHealth(ref attacker));
    3. AnimationManager.AddJob(AttackAnim(ref attacker));
    4. HealthManager.AddJob(UpdateHealth(ref defender));
    5. AnimationManager.AddJob(DefenceAnim(ref attacker));
    6. ParticleManger.AddJob(AttackFX(ref attacker));
    7. ParticleManger.AddJob(DefendFX(ref attacker));
    The updside of ECS is once I have a working framework it should be much more portable between projects, which garners the question will Unity have a default set of template ECS systems the way it has API calls and Monobehaviours in the old version?

    PS: How much more code is needed by ECS than Monobehaviours?
     
    TooManySugar and Torietron like this.
  2. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    ECS is meant to solve problems that not include write less code, btw it includes reuse code. If you are programing as a hobby (like me), you should stick with whatever is more fun to you.

    But even if you write more? code, you spend more time thinking on how to solve problems on your games mechanis and logic. When you write a system you are already working with any entity that has that component data and you write it just once.

    If you have a CombatSystem and you decided that your Ork's little pet will need to attack, you just add an attack component to it. Doesn't need to implement function, change classes and break all existing code. I think this one of the best part of ECS. Changed your mind and Ork's little demon should not attack anymore? Ok, just remove component or even don't add.
     
    dadude123 and PhilSA like this.
  3. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    The thing is this Ork Demon will also need an attack animation, sfx, particle fx, death/damage, animaiton, sfx/fx as well as stats and other information. In the old system most of these things you would build into a couple of components e.g. it could have a combat component to provide it with attack and defence capabilities.

    The old component based system provided larger grained component features just like the ECS system. But the ECS system you will need to add lots of micro systems just to provide the ability to bring in an Ork Demon.

    Lots of the code for these micro systems which are just basically structs that need a System or Manager to process them require a lot more work for the programmer/developer designer.

    For instance how well do these systems scale when you develop your projects complexity. What about chaining ECSJobs into action/reaction event chains that need to run in sequence and can branch e.g. Behaviour trees or Combat outcomes.

    Or quite simply lots of micro systems that need to interact vs fewer objects/components that require a magnitude less interactions.

    For example imagine a 3 element system where the elements all need to interact with each other you have 3 links, at 6 elements you have 6 then you have 15 connections (hint: it's n * (n-1) / 2)

    How many ECS Systems will you need in your game vs How many objects/components.

    For example in spaceship example provided by Unity...

    Wow possible bad example (complexity too low maybe)? 12 scripts for the classic* 13 for the Pure ECS?

    OK what about the code.... Here is the classic shooting code

    Code (CSharp):
    1. // Shooting
    2.             var playerPos = player.GetComponent<Transform2D>().Position;
    3.  
    4.             var state = GetComponent<EnemyShootState>();
    5.  
    6.             state.Cooldown -= Time.deltaTime;
    7.          
    8.             if (state.Cooldown <= 0.0)
    9.             {
    10.                 state.Cooldown = TwoStickBootstrap.Settings.enemyShootRate;
    11.                 var position = GetComponent<Transform2D>().Position;
    12.  
    13.                 ShotSpawnData spawn = new ShotSpawnData()
    14.                 {
    15.                     Position = position,
    16.                     Heading = math.normalize(playerPos - position),
    17.                     Faction = TwoStickBootstrap.Settings.EnemyFaction
    18.                 };
    19.                 ShotSpawnSystem.SpawnShot(spawn);
    20.             }
    Compare that to the new EnemyShoot Code!

    Code (CSharp):
    1. class EnemyShootSystem : JobComponentSystem
    2.     {
    3.         public struct Data
    4.         {
    5.             public int Length;
    6.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    7.             public ComponentDataArray<EnemyShootState> ShootState;
    8.         }
    9.  
    10.         [Inject] private Data m_Data;
    11.  
    12.         public struct PlayerData
    13.         {
    14.             public int Length;
    15.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    16.             [ReadOnly] public ComponentDataArray<PlayerInput> PlayerInput;
    17.         }
    18.  
    19.         [Inject] private PlayerData m_Player;
    20.         [Inject] private ShotSpawnBarrier m_ShotSpawnBarrier;
    21.  
    22.         // [ComputeJobOptimization]
    23.         // This cannot currently be burst compiled because CommandBuffer.SetComponent() accesses a static field.
    24.         struct SpawnEnemyShots : IJob
    25.         {
    26.             public float2 PlayerPos;
    27.             public float DeltaTime;
    28.             public float ShootRate;
    29.             public float ShotTtl;
    30.             public float ShotEnergy;
    31.             public EntityArchetype ShotArchetype;
    32.  
    33.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    34.             public ComponentDataArray<EnemyShootState> ShootState;
    35.  
    36.             public EntityCommandBuffer CommandBuffer;
    37.  
    38.             public void Execute()
    39.             {
    40.                 for (int i = 0; i < ShootState.Length; ++i)
    41.                 {
    42.                     var state = ShootState[i];
    43.  
    44.                     state.Cooldown -= DeltaTime;
    45.                     if (state.Cooldown <= 0.0)
    46.                     {
    47.                         state.Cooldown = ShootRate;
    48.  
    49.                         ShotSpawnData spawn;
    50.                         spawn.Shot.TimeToLive = ShotTtl;
    51.                         spawn.Shot.Energy = ShotEnergy;
    52.                         spawn.Position = Position[i];
    53.                         spawn.Heading = new Heading2D {Value = math.normalize(PlayerPos - Position[i].Value)};
    54.                         spawn.Faction = Factions.kEnemy;
    55.  
    56.                         CommandBuffer.CreateEntity(ShotArchetype);
    57.                         CommandBuffer.SetComponent(spawn);
    58.                     }
    59.  
    60.                     ShootState[i] = state;
    61.                 }
    62.             }
    63.         }
    64.  
    65.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    66.         {
    67.             if (m_Data.Length == 0 || m_Player.Length == 0)
    68.                 return inputDeps;
    69.  
    70.             return new SpawnEnemyShots
    71.             {
    72.                 PlayerPos = m_Player.Position[0].Value,
    73.                 DeltaTime = Time.deltaTime,
    74.                 ShootRate = TwoStickBootstrap.Settings.enemyShootRate,
    75.                 ShotTtl = TwoStickBootstrap.Settings.enemyShotTimeToLive,
    76.                 ShotEnergy = TwoStickBootstrap.Settings.enemyShotEnergy,
    77.                 Position = m_Data.Position,
    78.                 ShootState = m_Data.ShootState,
    79.                 CommandBuffer = m_ShotSpawnBarrier.CreateCommandBuffer(),
    80.                 ShotArchetype = TwoStickBootstrap.ShotSpawnArchetype,
    81.             }.Schedule(inputDeps);
    82.         }
    83.     }
    84. }
    So on top of the potential rising complexity from needing more micro components and systems to process them, there is a lot of boiler plate code that you need to use with the ECS/Job system.

    Maybe it's early days but could Unity not make ECS/Jobs an easier API/system to use.

    It also seems like we could be losing a lot of things we take for granted with the a component OO system e.g. inheritance and aggregation.

    What if Unity wrote a OOP to ECS converter or better yet compiler, where it strips a Monobehaviour class down into a set of ECS systems?

    Sounds complex but think about it Monobehaviour is a game engine component with most of the bits we use in games in it so code written on top of it that uses the Unity API could be easily analysed for it's use of the different gaming systems the could be broken down to ECS systems.

    You could gain the advantages of writing coherent OOP component code and Unity could generate all that boild plate ECS code under the hood, just as it does when it compiles our code to IL or Native.

    *The Classic Example has some scripts that are IMHO redundant and micro components e.g. MoveSpeed, Transform2D, EnemyShotState, Faction. So 9 Classic to 13 ECS or complexity factor 36 vs 78.
     
    Last edited: Jun 8, 2018
    noio likes this.
  4. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Again, ECS is not meant to write less code, it's meant to have better code. Sorry but this seems to be a kind of "convince me to use ECS" post, which I don't think is usefull.

    Your OOP code will run on a single thread, while your ECS code will run at multiple cores, so it is parallel by default. What if you have to have more lines of codes? This seems to be a nice trade-off, write more lines to get performance by default, parallelism, network-ready data and so on. Note that many of these boilerplate code can be solved with a template file, shotcuts and many more things.
     
    PhilSA and dadude123 like this.
  5. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    Code (CSharp):
    1. using System.ComponentModel.Design;
    2. using System.Runtime.CompilerServices;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using Unity.Transforms2D;
    8.  
    9. namespace TwoStickPureExample
    10. {
    11.  
    12.     /// <summary>
    13.     /// Assigns out damage from shots colliding with entities of other factions.
    14.     /// </summary>
    15.     class ShotDamageSystem : JobComponentSystem
    16.     {
    17.         struct Players
    18.         {
    19.             public int Length;
    20.             public ComponentDataArray<Health> Health;
    21.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    22.             [ReadOnly] public ComponentDataArray<PlayerInput> PlayerMarker;
    23.         }
    24.  
    25.         [Inject] Players m_Players;
    26.  
    27.         struct Enemies
    28.         {
    29.             public int Length;
    30.             public ComponentDataArray<Health> Health;
    31.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    32.             [ReadOnly] public ComponentDataArray<Enemy> EnemyMarker;
    33.         }
    34.  
    35.         [Inject] Enemies m_Enemies;
    36.  
    37.         /// <summary>
    38.         /// All player shots.
    39.         /// </summary>
    40.         struct PlayerShotData
    41.         {
    42.             public int Length;
    43.             public ComponentDataArray<Shot> Shot;
    44.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    45.             [ReadOnly] public ComponentDataArray<PlayerShot> PlayerShotMarker;
    46.         }
    47.         [Inject] PlayerShotData m_PlayerShots;
    48.  
    49.         /// <summary>
    50.         /// All enemy shots.
    51.         /// </summary>
    52.         struct EnemyShotData
    53.         {
    54.             public int Length;
    55.             public ComponentDataArray<Shot> Shot;
    56.             [ReadOnly] public ComponentDataArray<Position2D> Position;
    57.             [ReadOnly] public ComponentDataArray<EnemyShot> EnemyShotMarker;
    58.         }
    59.         [Inject] EnemyShotData m_EnemyShots;
    60.  
    61.         [ComputeJobOptimization]
    62.         struct CollisionJob : IJobParallelFor
    63.         {
    64.             public float CollisionRadiusSquared;
    65.  
    66.             public ComponentDataArray<Health> Health;
    67.             [ReadOnly] public ComponentDataArray<Position2D> Positions;
    68.  
    69.             [NativeDisableParallelForRestriction]
    70.             public ComponentDataArray<Shot> Shots;
    71.  
    72.             [NativeDisableParallelForRestriction]
    73.             [ReadOnly] public ComponentDataArray<Position2D> ShotPositions;
    74.  
    75.             public void Execute(int index)
    76.             {
    77.                 float damage = 0.0f;
    78.  
    79.                 float2 receiverPos = Positions[index].Value;
    80.  
    81.                 for (int si = 0; si < Shots.Length; ++si)
    82.                 {
    83.                     float2 shotPos = ShotPositions[si].Value;
    84.                     float2 delta = shotPos - receiverPos;
    85.                     float distSquared = math.dot(delta, delta);
    86.                     if (distSquared <= CollisionRadiusSquared)
    87.                     {
    88.                         var shot = Shots[si];
    89.  
    90.                         damage += shot.Energy;
    91.  
    92.                         // Set the shot's time to live to zero, so it will be collected by the shot destroy system
    93.                         shot.TimeToLive = 0.0f;
    94.  
    95.                         Shots[si] = shot;
    96.                     }
    97.                 }
    98.  
    99.                 var h = Health[index];
    100.                 h.Value = math.max(h.Value - damage, 0.0f);
    101.                 Health[index] = h;
    102.             }
    103.         }
    104.  
    105.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    106.         {
    107.             var settings = TwoStickBootstrap.Settings;
    108.  
    109.             if (settings == null)
    110.                 return inputDeps;
    111.  
    112.             var enemiesVsPlayers = new CollisionJob
    113.             {
    114.                 ShotPositions = m_EnemyShots.Position,
    115.                 Shots = m_EnemyShots.Shot,
    116.                 CollisionRadiusSquared = settings.playerCollisionRadius * settings.playerCollisionRadius,
    117.                 Health = m_Players.Health,
    118.                 Positions = m_Players.Position,
    119.             }.Schedule(m_Players.Length, 1, inputDeps);
    120.  
    121.             var playersVsEnemies = new CollisionJob
    122.             {
    123.                 ShotPositions = m_PlayerShots.Position,
    124.                 Shots = m_PlayerShots.Shot,
    125.                 CollisionRadiusSquared = settings.enemyCollisionRadius * settings.enemyCollisionRadius,
    126.                 Health = m_Enemies.Health,
    127.                 Positions = m_Enemies.Position,
    128.             }.Schedule(m_Enemies.Length, 1, enemiesVsPlayers);
    129.  
    130.             return playersVsEnemies;
    131.         }
    132.     }
    133. }
    134.  
    OK Above is the rather verbose code to trigger damage when shots hit and reduce the hit ships health by an amount or health -= damage;

    Three things to consider here...

    One: ECS is just functional programming with structs so is C# even the best programming language for this type of coding, we are adding lots of Attributes and wrapper code when maybe another language could work better with ECS?

    If you look at the Pure Twin Stick Shooter Example it has a ShotDamageSystem and a RemoveDeadSystem, something that would have been a few lines of code in an OnCollisionEnter() function is now two separate systems weighing in at 133 + 64 = 197 lines of code.

    Two: Imagine introducing different weapon damage types as the example is space based imagine we want 3 different weapon types missiles (M) with a blast radius, photon shots(the default S) and blasters bolts (B photon energy).

    So we can have 2 damage types kinetic and energy with any weapon type.

    And as we have grown our range of weapon types we add appropriate defences shields (d), armour (a)and reflective armour (r).

    So different damage types are reduced by different amounts depending on the defences and the shields/armour are reduced damaged themselves before we hit health.

    How would our ECS systems cope with this raised level of complexity as we now have a potential of 9 damage scenarios before we take health damage?

    B->a
    B->d
    B->r
    M->a
    M->d
    M->r
    S->a
    S->d
    S->r

    Also our designer wants 9 different sets of particle and sound fx for the damage as well as effects for when the shields/armour is destroyed.

    And they will want to be able to change them in editor!

    Three: How well does ECS provide sequential control, in games we have actions that trigger events that trigger effects it's "[UpdateAfter(typeof(ShotSpawnSystem))]" attributes don't really give you fine grained control of the order of things e.g. Can you lock down your ECS systems to a:
    A -> B -> C -> D
    sequence for all things in the game how long before you start needing
    earlyC -> A -> B -> C -> D -> lateA -> lateB

    Now imagine a game with a big set of ECS systems to manage it's complexity would it have tens of systems or even over a hundred ECS systems running to maximise the throughput and control the sequence of events at a fine enough grain to provide an amazing experience e.g. effects and animations happen the same frame as their trigger event.

    IMHO the impacts of ECS almost guarantee complexity as every interaction within the game world between what would have been two components or even a function in one component is now an ECS system.
     
    noio likes this.
  6. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    684
    For me the a lot of design friction and the most difficult to overcome is to let go of the sequential this-then-that mindset in order to use ECS. Even more so when you are porting your existing code, it is tempting to design a system while thinking where in "the chain" it would be.

    But I found that the correct mindset is to rather think "what does it needs" in order to start running. You don't care about execution order when putting Before/After but that will ultimately becomes an execution order. The produced order might not be what you expected, but the behaviour is correct.

    For example my game I used to have a flow like : calculate clickable things position this frame -> get input -> check against those things -> take action, all hand coded in old style.

    Right now I code them in System in terms of requirement rather than execution ordering (checking action requires input processed + position calculated, etc.) I got an unexpected result that I do not have in my head : the get input system runs the first. But is the behaviour what I wanted anyways? Yes.

    The ordering is optimized for parallelism, so it is actually harmful to think about a concrete ordering of systems and try to force them that way. (e.g. Saying I want to insert system Z in between A and B, similar to what you described) In my actual game I already got a lot of unexpected ordering when checking in the Profiler timeline view, but ultimately all of that results in the correct behaviour that I want.
     
  7. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    30
    If the argument was OOP vs ECS it may havebenn something to discuss, but if i understand correctly you compare ECS with monobehavior based component system which just doesn't scale up to something bigger than early prototype. It is easier to setup and use than both proper OOP and ECS, but it becomes unmaintainable really quick
     
  8. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Also, let me point something that you don't see very often when ppl are talking about benefits of ECS: Isolation of Systems. I'm developing a system that gets the object the player is targeting (simple raycast), but there is no physics for ECS yet, so I had to do my own and after developing it, I saw on entity debugger it was taking 20ms to finish (I'm very bad at physics), eating most part of my frame time, so I start to refactoring it, using jobs, getting help from Burst compiler until I got 0,5ms per frame.

    The way ECS is built you know where the problems can be, since only systems contain logic, so only systems can eat your frame time. Since all systems can run isolately, you have a nice a bealtiful performance track tool right out of the box, you don't need profile for most cases. Also, when I open my inventory, which I don't need the object it is point to, I just disable this system and reenable when inventory is closed.

    This can be done for all systems, just because the way of developing things in ECS.
     
  9. avvie

    avvie

    Joined:
    Jan 26, 2014
    Posts:
    65
    my little bit to this is that the more i learn the less i write. I might have systems ready from another experiment. no need to change a thing. just drag and drop. super easy
    At start i had the same look on it because of IComponentData and stuff. Even then the final code, considering what unity does behind the scenes must be less than in ecs than in mono. Its just that in mono you get to drag and drop components. I say must, because the announcement about the small runtimes that can go down to 45kb require pure ecs, instead of the 45mb that monobehavior needs.

    Apart from the paradigm shift, personally i cant find a thing that isnt better to do in ecs. even things that wouldnt require C# jobs, they perform better in ecs than in monobehaviors
     
    Afonso-Lage likes this.
  10. Cyberwiz15

    Cyberwiz15

    Joined:
    Nov 10, 2016
    Posts:
    53
    In all honesty I've found that in the long term while I might be writing a little more boilerplate code, I am writing systems that are a lot clearer in it's intent and the logic that I want to apply. Most of the time when attempting something a little longer winded than just a small jam game I end up with a great ball of spaghetti because of all the dependencies I need to create in order to enforce the behaviour I want. I don't want unreadable code that obfuscating what behaviour is present so I think the extra boilerplate is well worth it. It's also forcing me to really think about the minimum that is required for something to be considered a system.
     
    PhilSA likes this.
  11. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    Even the ECS teams docs state they need to do more work to make the code simpler!

    I've had this conversation before with another developer who used a managed code approach in Unity very similar to ECS in a lot of ways.

    https://forum.unity.com/threads/how...-my-unorthodox-way.413419/page-3#post-2702280
     
    Last edited: Jun 8, 2018
    noio likes this.
  12. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    306
  13. KitsuneX89

    KitsuneX89

    Joined:
    Dec 12, 2014
    Posts:
    2
    If you just took the time to watch one of their videos... all your questions and speculations would be answered. Yes it introduces a (slightly) different way of writing code, a style you might not be used to (it is still C# tho..). However the consideration and motivation for doing this, is performance and re-usability.. Which show impressive numbers, especially when using Burst compiler. I also don't understand how this would be so much more complex, the workflow is very similar to the current workflow.
     
  14. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    OK ECS gives lots of performance but for something that basically chops up what was OOP code into structs then takes their functions and runs them over an array of those structs it seems like a lot of code for a for next loop.

    Code (CSharp):
    1. public struct BitOfObject {
    2.    public int oneBit;
    3.    public float anotherBit;
    4. }
    5.  
    6. public ObjectBits bits[];
    7.  
    8. public OnUpdate() {
    9.    foreach(BitOfObject bit in bits)
    10.    {
    11.       //DoASingleOOPActionHere;
    12.    }
    13. }
    14.  
    Now if Unity could get ECS code this concise and easy to understand that would be amazing but its nowhere near this simple yet.

    Isn't there a more concise way to do this e.g. a lot of programming languages develop more advanced features to make doing repetitive things like this easier for the programmer e.g. Templates/Generics.

    What if Unity allowed an old school OOP object to be ECS'ed by the build process e.g. an [ECS] tag and Unity takes the object and its functions and divides it up into structs used by the functions and builds a complete ECS subsystem that is compiled and run.

    Benefits we can write old school quick and easy OOP and get ECS performance.
     
  15. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    I think that's impossible. OOP vs ECS is not about syntax, but about logic. In order to use ECS you need to change how you see the world as a programmer and you cannot expect Unity to rework your logic, since this is what make you a programmer and what make Unity an engine.

    You can expect Unity to improve the current state of ECS by reducing the amount of boilerplate code.

    I don't want to bring a discussiong about OOP vs Data Oriented, because for me it's like discussing if earth is flat, but please note that OOP vs Data Oriented isn't nothing new, for years many devs communities are trying to get away from OOP, be it for Data Oriented or Functional Programming, but almost any experienced develop agrees that for modern computer, with many cores, OOP isn't the best approach.
     
    avvie likes this.
  16. Cyberwiz15

    Cyberwiz15

    Joined:
    Nov 10, 2016
    Posts:
    53
    I'm also very weary of using something like templates to generate code. Quite often it's hiding what should've been a code smell. My view is that one should be writing the boilerplate as it helps in understanding how to work with the framework, but to also be looking at ways to decrease the amount of boilerplate. If this is just generated you're never going to feel the pain of the architecture and there won't be effort put into thinking about a way to improve it.
     
  17. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    What if we invented a component all the dataItems in a component are stored in an array and a foreachprocess does the actions on the data?

    Code (CSharp):
    1. component {
    2.    dataitem ABitOfData;
    3.    dataitem AnotherBitOfData;
    4.    dataStore[]  dataArray;
    5.  
    6.    foreachprocess {
    7.       //Do some processing on each piece of data in dataArray
    8.    }
    9. }
    Arguably you would not need the dataStore as it would be built into the component.

    The idea here is to bring the way ECS works into the programming language therefore making it easier to work with for programmers.
     
    noio likes this.
  18. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    684
    Is it sounds like `IJobProcessComponentData<T1,T2,...>` ? In each Execute it works like foreachprocess that you described. It comes with `ref` so you could freely change values and have them saved.

    I have made a lib over Unity's ECS that uses generic to influence what's injected. (E7ECS) The idea is that if you have [Inject] in your abstract superclass as anything that is not private (protected/private protected/public), it could be carried over to the subclass. Then you can make <T> go into that struct in your abstract class. Maybe you can exploit this to help reduce the amount of code you write.

    For example my game currently I can do this :

    Code (CSharp):
    1. public class PlayAllAudioSystem : MonoCS<AudioSource>
    2. {
    3.     protected override void OnUpdate()
    4.     {
    5.         foreach(AudioSource aSource in MonoComponents)
    6.         {
    7.             aSource.Play();
    8.         }
    9.     }
    10. }
    MonoCS take the generic T to inject a `ComponentData<T>` and put them in IEnumerable<T> MonoComponents.
     
    noio likes this.
  19. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    115
    ECS kind of forces you to work more structured, so you need to set up a basis for your system which might take a bit more time than just coding away. But once you have the basics of your game down it should become a lot easier to create things on top of that.
     
  20. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    I think my main issue is I'm going to be writing and re-writing lots of tiny structs and foreach loops with a lot of additional boiler plate code and attributes that currently are just confusing to get to grips with.

    If Unity could update the C# compiler with a new set of tokens dedicated to doing Jobs and ECS that could make the whole process a breeze.

    Also looking at the Space Shooter example why have one file with structs and another with the System that uses them, could we not have micro component OOP where the structs are included with the main system that uses them?

    It's like going back to C/C++ with header files and c/cpp files.

    I also think a lot of people will find this approach confusing as it does not work as well with the editor as the old system, e.g. we are not seeing Entities in the IDE other than the Debug viewer. Hopefully this will improve greatly as the system is improved.

    Actually as the ECS system is so similar in structure e.g. struct/foreach would it be an ideal system to implement via visual development tools as they could do most of the boilerplate in the background and let the developers focus more easily on the mechanics of the game?
     
    FROS7 likes this.
  21. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    115
    Systems and Components are still kind of separate, multiple Systems can use the same Component etc.
    If you want to put Components in the same file as their respective Systems you can just do that.

    Well, it is a different approach and it is just a lot less visually and more data driven way of doing things. ECS just work a lot different than classical "OOP" style programming. I don't think everyone will adopt to it and I'm not sure everyone even should.

    It's still kind of a beta in my eyes and I'm looking forward to a whole lot more things. Better visualization in the Editor is one thing but I'm also looking for more functional things. Something like Entitas Reactive System where you can get callbacks when a component is added or removed etc. And for them to open it up a bit more and make it more flexible, currently you can't put any kind of Collection, not even primitive arrays, into IComponentData.

    Over time, as it becomes the core of most Unity internal systems, these Components will probably become similarly supported as Monobehaviours are right now.
     
    optimise likes this.
  22. Cyberwiz15

    Cyberwiz15

    Joined:
    Nov 10, 2016
    Posts:
    53
    In my personal view I've come to think of components as my data schema, almost the database of the game that I'm building. This is done through careful consideration of the types of data that needs to be represented and how to define small groupings so that we don't just end up with a bunch of primitive type values to represent the data.

    Systems can then go forth and provide a good explanation of the type of operations it's performing on the data as well as on what data it's performing. So instead of trying to abstract our data to the point where we're building objects that constantly need to have their data mapped to something, we now end up with a relatively flat interaction between game logic and the data that drives that logic.
     
  23. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    115
  24. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    If they could work on the verbosity of the API like Entitas that would be a great boon to developers IMHO...

    Code (CSharp):
    1. var entities = context.GetEntities(Matcher<GameEntity>.AllOf(GameMatcher.Position, GameMatcher.Velocity));
    2. foreach(var e in entities) {
    3.     var pos = e.position;
    4.     var vel = e.velocity;
    5.     e.ReplacePosition(pos.value + vel.value);
    6. }
    vs

    Code (CSharp):
    1. using Unity.Entities;
    2. //using Unity.Transforms2D;
    3. using Unity.Transforms;
    4. using Unity.Mathematics;
    5.  
    6. using UnityEngine;
    7.  
    8. namespace ECSSpaceInvaders
    9. {
    10.  
    11.     public class EnemyMoveSystem : ComponentSystem {
    12.  
    13.         public struct Data
    14.         {
    15.             public int Length;
    16.             public ComponentDataArray<Position> Position;          
    17.             public ComponentDataArray<Heading> Heading;
    18.             public ComponentDataArray<MoveSpeed> MoveSpeed;
    19.             public ComponentDataArray<Enemy> Enemy;
    20.         }
    21.  
    22.         [Inject] private Data m_Data;
    23.  
    24.         protected override void OnUpdate()
    25.         {
    26.             var settings = SpaceInvadersBootstrap.Settings;
    27.             Rect r = settings.playfield;
    28.  
    29.             r.xMin *= 0.95f;
    30.             r.xMax *= 0.95f;
    31.  
    32.             float dt = Time.deltaTime;
    33.  
    34.             float3 move = dt * settings.invaderMoveDirection * settings.enemySpeed;
    35.             float3 position;
    36.  
    37.             for (int index = 0; index < m_Data.Length; ++index)
    38.             {
    39.                 position = m_Data.Position[index].Value;              
    40.  
    41.                 position += move;
    42.  
    43.                 if (position.x > r.xMax)
    44.                 {
    45.                     position.x = r.xMax;
    46.                     settings.invaderMoveDirection.x = -1f;
    47.                 }
    48.  
    49.                 if (position.x < r.xMin)
    50.                 {
    51.                     position.x = r.xMin;
    52.                     settings.invaderMoveDirection.x = 1f;
    53.                 }
    54.  
    55.                 m_Data.Position[index] = new Position{ Value = position };                              
    56.             }
    57.  
    58.             SpaceInvadersBootstrap.Settings.invaderMoveDirection = settings.invaderMoveDirection;
    59.         }
    60.     }
    61. }
    Compared to setting up the data used then writing an Execute function and a JobHandle wrapper and all those [Inject] and [ReadOnly] and [NativeDisableParallelForRestriction] this is definitely the way Unity ECS should go.
     
    Last edited: Jun 21, 2018
  25. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    115
    Well, despite those two examples clearly having different scopes to begin with so it's hardly a fair comparison.

    You do know this exists?
    Code (CSharp):
    1.     [BurstCompile]
    2.     struct MovementJob : IJobProcessComponentData<Position, Velocity> {
    3.         public float deltaTime;
    4.  
    5.         public void Execute(ref Position position, [ReadOnly]ref Velocity velocity) {
    6.             rotation.Value = position.Value + velocity.Value * deltaTime;
    7.         }
    8.     }
    Sure, you still have to set up the job and hand it the deltatime and it's still not as dynamic yet, but it's not as bad as you make it seem.
     
    Last edited: Jun 21, 2018
    Cynicat, Doug-Wolanick and dadude123 like this.
  26. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    1,780
    The Unity approach to structuring component layout is a lot more performant then most other ECS systems. So you have to take that into account.

    It's fairly simple to add an abstraction yourself so you can in effect call entity.component. Not the ECS entity mind you but a struct you create that looks and acts like a more traditional ECS entity. Here is a copy and paste of what it looks like in a system. In OnUpdate I just create a new AgentSteeringEntity for each ECS entity. You could vary this and put the IComponent fields in your system as well.

    You have all sorts of tools to add the abstractions you need to make your systems clean. And IMO that is better left up to us anyways, because how you structure this can change system to system. I have other systems where I don't do the below, because the logic is such I don't know which components I need to pull out up front. But where I do this cleans things up nicely.


    Code (csharp):
    1.  
    2. public struct AgentSteeringEntity
    3.     {
    4.         public CombatEntityId CombatEntityId;
    5.         public Position Position;
    6.         public FactionId FactionId;
    7.         public Weapon Weapon;
    8.         public GroupInfo GroupInfo;
    9.         public CombatStats CombatStats;
    10.         public CombatTarget CombatTarget;
    11.         public DamageSummary DamageSummary;
    12.         public MoveDestination MoveDestination;
    13.  
    14.         public AgentSteeringEntity(AgentSteeringGroup group, int index)
    15.         {
    16.             CombatEntityId = group.CombatEntityIds[index];
    17.             Position = group.Positions[index];
    18.             FactionId = group.FactionIds[index];
    19.             GroupInfo = group.GroupInfos[index];
    20.             CombatStats = group.CombatStats[index];
    21.             CombatTarget = group.CombatTargets[index];
    22.             DamageSummary = group.DamageSummaries[index];
    23.             MoveDestination = group.MoveDestinations[index];
    24.             Weapon = group.Weapons[index];
    25.         }
    26.     }
    27.  
    28.     public struct AgentSteeringGroup
    29.     {
    30.         [ReadOnly]
    31.         public ComponentDataArray<GroupInfo> GroupInfos;
    32.         [ReadOnly]
    33.         public ComponentDataArray<CombatStats> CombatStats;
    34.         [ReadOnly]
    35.         public ComponentDataArray<MoveDestination> MoveDestinations;
    36.         [ReadOnly]
    37.         public ComponentDataArray<DamageSummary> DamageSummaries;
    38.         [ReadOnly]
    39.         public ComponentDataArray<CombatEntityId> CombatEntityIds;
    40.         [ReadOnly]
    41.         public ComponentDataArray<Position> Positions;
    42.         [ReadOnly]
    43.         public ComponentDataArray<FactionId> FactionIds;
    44.         [ReadOnly]
    45.         public ComponentDataArray<Weapon> Weapons;
    46.         [ReadOnly]
    47.         public ComponentDataArray<CombatTarget> CombatTargets;
    48.  
    49.         public EntityArray Entities;
    50.  
    51.         public int Length;
    52.  
    53.     }
    54.  
     
  27. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    1,780
    Another trick you can do for single value components to make them more elegant is using implicit operators. So Position can be used directly as a float3 in most cases.

    Code (csharp):
    1.  
    2. public struct Position : IComponentData
    3.     {
    4.         public float3 Value;
    5.  
    6.         public static implicit operator float3(Position position)
    7.         {
    8.             return position.Value;
    9.         }
    10.     }
    11.  
     
  28. Arowx

    Arowx

    Joined:
    Nov 12, 2009
    Posts:
    6,561
    Now this is the kind of examples we need how to make ECS work for you and not feel as thought your are doing loads of work just to update an entities position in your game.
     
  29. LurkingNinjaDev

    LurkingNinjaDev

    Joined:
    Jan 20, 2015
    Posts:
    1,175
    just a small correction:

    just to update tens (or hundreds) of thousands of entities positions in your game in a few milliseconds...

    And BTW, really? In the age of modern IDE we are arguing about code verbosity? ROFL.
     
  30. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    115
    Be careful with that though, all it does is allow you to omit the ".Value" in position.Value when using it in an assignment or when wanting to pass the float3 value. It does not allow you to set anything to it more easily and the float that is being used is a value copy of the Value, not in any way bound to the Component.
    It really just saves you from manually assigning the float3 value to a new variable or typing ".Value".
     
  31. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    684
    Also try code snippet to live with verbose code. It is the first time in more than 7 years of coding that I am compelled to use one. For example with VSCode I have these snippet :

    Code (JavaScript):
    1. {
    2.     // Place your snippets for csharp here. Each snippet is defined under a snippet name and has a prefix, body and
    3.     // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
    4.     // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
    5.     // same ids are connected.
    6.     // Example:
    7.     // "Print to console": {
    8.     //     "prefix": "log",
    9.     //     "body": [
    10.     //         "console.log('$1');",
    11.     //         "$2"
    12.     //     ],
    13.     //     "description": "Log output to console"
    14.     // }
    15.     "Protected Override": {
    16.         "prefix": "po",
    17.         "body": [
    18.             "protected override $0",
    19.         ]
    20.     },
    21.     "Native Array Kit": {
    22.         "prefix": "naark",
    23.         "body": [
    24.             "NativeArray<int> $0naar;",
    25.             "protected override void OnCreateManager(int capacity)",
    26.             "{",
    27.             "    naar = new NativeArray<int>(1, Allocator.Persistent);",
    28.             "}",
    29.             "",
    30.             "protected override void OnDestroyManager()",
    31.             "{",
    32.             "    naar.Dispose();",
    33.             "}",
    34.         ]
    35.     },
    36.     "On Create - Destroy": {
    37.         "prefix": "oncredes",
    38.         "body": [
    39.             "protected override void OnCreateManager(int capacity)",
    40.             "{",
    41.             "}",
    42.             "",
    43.             "protected override void OnDestroyManager()",
    44.             "{",
    45.             "}",
    46.         ]
    47.     },
    48.     "Native Array": {
    49.         "prefix": "naar",
    50.         "body": [
    51.             "NativeArray<int> $0naar;",
    52.             "naar = new NativeArray<int>(1, Allocator.Persistent);",
    53.             "naar.Dispose();",
    54.         ]
    55.     },
    56.     "Job PCD": {
    57.         "prefix": "jobpcd",
    58.         "body": [
    59.             "protected override JobHandle OnUpdate(JobHandle inputDeps)",
    60.             "{",
    61.             "    var job = new Job()",
    62.             "    {",
    63.             "    };",
    64.             "    return job.Schedule(this, inputDeps);",
    65.             "}",
    66.             "",
    67.             "struct Job : IJobProcessComponentData<$0>",
    68.             "{",
    69.             "    public void Execute(ref )",
    70.             "    {",
    71.             "    }",
    72.             "}",
    73.         ]
    74.     },
    75.     "Job Parallel For": {
    76.         "prefix": "jobpara",
    77.         "body": [
    78.             "protected override JobHandle OnUpdate(JobHandle inputDeps)",
    79.             "{",
    80.             "    var job = new Job()",
    81.             "    {",
    82.             "    };",
    83.             "    return job.Schedule(len, 1, inputDeps);",
    84.             "}",
    85.             "",
    86.             "struct Job : IJobParallelFor",
    87.             "{",
    88.             "    public void Execute(int i)",
    89.             "    {$0",
    90.             "    }",
    91.             "}",
    92.         ]
    93.     },
    94.     "Job": {
    95.         "prefix": "job",
    96.         "body": [
    97.             "protected override JobHandle OnUpdate(JobHandle inputDeps)",
    98.             "{",
    99.             "    var job = new Job()",
    100.             "    {",
    101.             "    };",
    102.             "    return job.Schedule(inputDeps);",
    103.             "}",
    104.             "",
    105.             "struct Job : IJob",
    106.             "{",
    107.             "    public void Execute()",
    108.             "    {$0",
    109.             "    }",
    110.             "}",
    111.         ]
    112.     },
    113.     "Using Job": {
    114.         "prefix": "usejob",
    115.         "body": [
    116.             "using Unity.Entities;",
    117.             "using Unity.Mathematics;",
    118.             "using Unity.Collections;",
    119.             "using Unity.Jobs;",
    120.             "using UnityEngine.Experimental.PlayerLoop;",
    121.         ],
    122.     },
    123.     "Debug Log": {
    124.         "prefix": "dbl",
    125.         "body": [
    126.             "Debug.Log($\"$0\");"
    127.         ],
    128.     },
    129.     "Read Only Component Data Array": {
    130.         "prefix": "rocda",
    131.         "body": [
    132.             "[ReadOnly] public ComponentDataArray<$0> datas;"
    133.         ],
    134.     },
    135.     "Write Only Component Data Array": {
    136.         "prefix": "wocda",
    137.         "body": [
    138.             "[WriteOnly] public ComponentDataArray<$0> datas;"
    139.         ],
    140.     },
    141.     "Component Data Array": {
    142.         "prefix": "cda",
    143.         "body": [
    144.             "public ComponentDataArray<$0> datas;"
    145.         ],
    146.     },
    147.     "Read Only Component Array": {
    148.         "prefix": "roca",
    149.         "body": [
    150.             "[ReadOnly] public ComponentArray<$0> components;"
    151.         ],
    152.     },
    153.     "Write Only Component Array": {
    154.         "prefix": "woca",
    155.         "body": [
    156.             "[WriteOnly] public ComponentArray<$0> components;"
    157.         ],
    158.     },
    159.     "Component Array": {
    160.         "prefix": "ca",
    161.         "body": [
    162.             "public ComponentArray<$0> components;"
    163.         ],
    164.     },
    165.     "Injection Group": {
    166.         "prefix": "injectgroup",
    167.         "body": [
    168.             "struct InjectedGroup ",
    169.             "{$0",
    170.             "}",
    171.             "[Inject] InjectedGroup injectedGroup;",
    172.         ],
    173.     },
    174.     "Entity Array": {
    175.         "prefix": "entiarr",
    176.         "body": [
    177.             "[ReadOnly] public EntityArray entities;",
    178.         ],
    179.     },
    180.     "Length": {
    181.         "prefix": "len",
    182.         "body": [
    183.             "public int Length;",
    184.         ],
    185.     },
    186.     "Readonly Public": {
    187.         "prefix": "rop",
    188.         "body": [
    189.             "[ReadOnly] public $0",
    190.         ],
    191.     },
    192.     "Entity Command Buffer": {
    193.         "prefix": "enticom",
    194.         "body": [
    195.             "public EntityCommandBuffer command;",
    196.         ],
    197.     },
    198.     "Barrier System": {
    199.         "prefix": "barr",
    200.         "body": [
    201.             "public class Barrier$0 : BarrierSystem { }",
    202.             "[Inject] Barrier barrier;",
    203.         ],
    204.     },
    205.     "Get Barrier into a job": {
    206.         "prefix": "combar",
    207.         "body": [
    208.             "command = barrier.CreateCommandBuffer(),",
    209.         ],
    210.     },
    211.     "Update Before": {
    212.         "prefix": "ube",
    213.         "body": [
    214.             "[UpdateBefore(typeof($0))]",
    215.         ],
    216.     },
    217.     "Update After": {
    218.         "prefix": "uaf",
    219.         "body": [
    220.             "[UpdateAfter(typeof($0))]",
    221.         ],
    222.     },
    223. }
    From 1 month of usage the most used being rocda -> [ReadOnly] public ComponentDataArray<$0> datas; followed by po -> protected override and injectgroup
     
    Last edited: Jun 21, 2018
    starikcetin likes this.
  32. LurkingNinjaDev

    LurkingNinjaDev

    Joined:
    Jan 20, 2015
    Posts:
    1,175
    :D
    Maybe I'm too biased because I work as a Java developer in my day job, so I'm used to it already.

    BTW, as far as I remember, they said that first they develop the system and then they will work on the integration to make it better (I think it will mean that they try to make it less verbose too).
     
  33. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    1,780
    I haven't played around with it yet, but ref returns might make this more interesting.

    And all approaches like this have caveats including the previous example which adds overhead. Overhead I'm ok with in logic in OnUpdate, not so much in a job in a tight loop or parallel for. I'd rather have the default be the most performant like it is now and choose when I think it's ok to add more abstractions.