Search Unity

Ramifications of "newing up" in a ComponentSystem

Discussion in 'Entity Component System' started by Gibbonfiend, Jun 2, 2020.

  1. Gibbonfiend

    Gibbonfiend

    Joined:
    Jul 23, 2015
    Posts:
    7
    I'd like to generally understand the consequences of the following:
    1. newing up objects in the OnUpdate() method of a ComponentSystem. I generally like to make helper classes that I can unit test separately. I'm actually hoping to write some of the code in a separate F# library for instance.
    2. Similarily, just newing up anonymous types or tuples to group things together
    3. Calling ToString() on a NativeString
    4. Any operation that might box the Component or it's members

    All of these things sound to me like they will hurt performance given that they're allocating garbage collectable heap memory and that I should be working with the blittable types that I can create ComponentData from. On the other hand, working with these types is much more cumbersome and it means I can't create a "model" library that is not dependent on Unity, which is my first instinct when writing code.

    Basically, how much does writing ECS ComponentSystem code in a typical .NET way hurt performance?
     
  2. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Everything you say is correct. Objects/boxing generate garbage and will hurt performance if you do it every frame. Users have been making non garbage generating games in Unity before ecs came along though so that's more to do with your own game code than ecs vs OO.

    You can use managed objects with ecs but all you get for that is a different programming paradigm and not the perf benefits inherent with blittable data streams.

    If you're going to use Unity's ecs then obviously you are quite coupled to their unique implementation of it but I'm not sure that rules out making your own separate game libraries. It'd be procedurally based though as opposed to OO based which is perhaps what you're referring to.

    Boilerplate aside, I've found my own game code has gotten cleaner as it's just functions operating on data. Ecs forces a simple coding approach cos it flattens the architecture as opposed to the boundless flexibility of OO.
    It's not all pros with no cons but better or worse, it's a totally different paradigm and I'm not sure it's worth trying to mix the two.
     
  3. Gibbonfiend

    Gibbonfiend

    Joined:
    Jul 23, 2015
    Posts:
    7
    Thanks @jooleanlogic for your thoughtful answer. I've certainly been quite excited about the move away from the object oriented paradigm and the ECS data-oriented approach has certainly made a lot of things easier so far. I agree about the "flattened architecture", which seems to make everything much more scalable. I usually grind to a halt after a certain period of time under an OO approach because mixing data and logic in classes soon leads to code that becomes unmanageable without a great deal of discipline and engineering work to keep it under control, which feels more like work than fun!

    My initial thoughts were to write some core game logic in F# (the idea was this project would be a good opportunity to get better at F#) passing in data from the ComponentSystem. However, I can't see a way to pass the data without boxing or newing up classes/structs. I think I might resign myself to writing in C# but in a functional style but it's a bit of a shame.
     
    Last edited: Jun 2, 2020
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,267
    Unlike C++, newing structs does not necessarily put them on the heap unless you assign the struct to an
    object
    or an interface reference (boxing). I don't know enough about F# and even less about C#/F# interop, but the first thing I would try is having a job pass
    this
    to an F# function in its execute method and see if you can get the job to run with Burst.
     
    Gibbonfiend likes this.
  5. Gibbonfiend

    Gibbonfiend

    Joined:
    Jul 23, 2015
    Posts:
    7
    Thanks @DreamingImLatios. I might look further into what actually happens when you new up a struct and pass it to an F# function by reference. I think you're correct - this will avoid heap allocation. The problem is that the F# project needs to be the place where the "model" types are defined as it can't have a reference back to the Unity project. Also, I want to create my own types to better represent the data. Perhaps an example showing some of the problems I've been getting would be useful. Note that this code is using a ComponentSystem not using a JobComponentSystem as, firstly, I'm still feeling my way around the ECS so keeping it simple for now and, secondly, I think this algorithm actually needs to be done on a single thread. In any case, I don't think the actual implementation of the algorithm affects the problems.

    The code is attempting to run through each crew member and assign the best job to each one. I've put comments next to the issues I've been seeing.

    Code (CSharp):
    1. public class FindJobSystem : ComponentSystem
    2. {
    3.     protected override void OnUpdate()
    4.     {
    5.         //Bring all the jobs into memory at once. Apologies for using the word "job" here. It has nothing to do with the ECS jobs, just a task for the CrewMember to perform.
    6.         var query = Entities.WithAll<JobComponent, CompetencyBufferElement>().ToEntityQuery();
    7.         var jobEntities  = query.ToEntityArray(Allocator.TempJob);
    8.  
    9.         var entityManager = World.Active.EntityManager;
    10.  
    11.         //FIRST PROBLEM - I've newed up an anonymous object here to group the entity, JobComponent and Competencies.
    12.         //I also then instantiate it to a List
    13.         var jobs = jobEntities.Select(selector: job => new
    14.         {
    15.             JobEntity = job,
    16.             Job = entityManager.GetComponentData<JobComponent>(job),
    17.             Competencies = entityManager.GetBuffer<CompetencyBufferElement>(job).Select(competency => competency.Competency)
    18.         }).ToList();
    19.  
    20.         //SECOND PROBLEM - another List
    21.         var jobsToRemove = new List<Entity>();
    22.         Entities
    23.             .WithAll<CrewMemberComponent, CompetencyBufferElement>()
    24.             .WithNone<JobComponent>()
    25.             .ForEach((Entity crewMember, DynamicBuffer<CompetencyBufferElement> crewMemberCompetenciesBuffer) =>
    26.             {        
    27.                 //Note Pseudocode here as this is what I want to write not what actually works!
    28.                 //THIRD PROBLEM - Need to new up a bunch of "model" job ojects.
    29.                 //The ToModel() extention methods would also be doing things like ToString() on NativeString etc.
    30.                 var fSharpJobs = jobs.Select(j => new MyFSharpProject.Job(j.Job.ToModel(), j.Competencies.ToModel());
    31.                 var fSharpCrewMemberCompetencies = crewMemberCompetenciesBuffer.ToModel();
    32.                 var job = MyFSharpProject.GetBestJob(crewMemberCompetenciesBuffer.ToModel(), fSharpCrewMemberCompetencies);
    33.                 jobs.Remove(job);
    34.                 PostUpdateCommands.AddComponent(crewMember, job.Job);
    35.                 jobsToRemove.Add(job.JobEntity);
    36.             });
    37.  
    38.  
    39.         foreach (var jobToRemove in jobsToRemove)
    40.         {
    41.             PostUpdateCommands.DestroyEntity(jobToRemove);
    42.         }
    43.  
    44.         jobEntities.Dispose();
    45.     }
    46. }
    Basically, my strategy is to convert the unwieldy and disparate ECS bits of data into nice model objects, which can be passed cleanly to method in a Unity-agnostic library, where they can be tested in isolation. But I don't want to shoot myself in the foot by realising further down the road that performance sucks. I think I'm likely to have a lot (thousands) of these entities at some point.
     
    Last edited: Jun 3, 2020
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,267
    You can use F# for algorithms but you can't really use it to design logical structure without referencing Unity.Entities. Really you will probably need to write a type that wraps NativeArray and use chunk iteration in C# to extract NativeArrays of components to then pass onto F#. Maybe if you are clever enough you can wrap ArchetypeChunk as well and get creative with NativePtr. But really, I don't think what you want to do fits with the ECS memory and typing models.