Search Unity

RobotArms, a functional style Entity Component System for Unity

Discussion in 'Scripting' started by dkoontz, Jun 21, 2015.

  1. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    I got tired of trying to endlessly fight the hydra that is inter-component dependencies. As much as I tried, it always seemed that component A would eventually acquire a reference to component B and start making assumptions about it or worse, reaching in and mutating it. I discovered entity component systems such as Artemis but I wanted something that was native to Unity so my team wrote RobotArms. This is a very simple library (450 lines including comments and whitespace), but one that pretty radically changes how you structure your logic in your game. If the above problems sound like things you've seen in your projects, give RobotArms a look.
     
    d1favero, sluice, Tzan and 2 others like this.
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    This is really cool and eerily similar to my Flash game framework, right down to "process" (here's a snippet):
    Code (csharp):
    1.  
    2.   public class HeroControlSystem extends SingleListSystem
    3.    {
    4.      protected var _ceiling : Number;
    5.      
    6.      public function HeroControlSystem(ceiling : Number) : void
    7.      {
    8.        super();
    9.        
    10.        _list = new EntityList(HeroControlComponent, VelocityComponent, TransformComponent);
    11.        
    12.        _ceiling = ceiling;
    13.      }
    14.      
    15.      override protected function process(deltaTime : Number, entity : Entity) : void
    16.      {
    17.        var transform : TransformComponent = entity.retrieve(TransformComponent);
    18.        var velocity : VelocityComponent = entity.retrieve(VelocityComponent);
    19.        var control : HeroControlComponent = entity.retrieve(HeroControlComponent);
    20.        
    21.        // horizontal      
    22.        velocity.x = 0;
    23.        
    24.        if (_input.isDown("left") == true && _input.isDown("right") == false)
    25.        {
    26.          velocity.x = -control.speed;
    27.          
    28.          transform.scaleX = -1;
    29.        }
    30.        if (_input.isDown("right") == true && _input.isDown("left") == false)
    31.        {
    32.          velocity.x += control.speed;
    33.          
    34.          transform.scaleX = 1;
    35.        }
    36.        
    37.        // now vertical                
    38.        if (_input.isDown("jump") == true && control.currentFuel > 0)
    39.        {
    40.          control.currentFuel -= 20 * deltaTime;
    41.  
    42.          velocity.y = -150;
    43.          
    44.          if (control.currentFuel <= 0)
    45.            velocity.y = 0;
    46.        }
    47.        
    48.        if (transform.x < 0)
    49.          transform.x = 0;
    50.        if (transform.x > Constants.ARENA_WIDTH)
    51.          transform.x = Constants.ARENA_WIDTH;
    52.        if (transform.y < _ceiling)
    53.          transform.y = _ceiling;      
    54.      }
    55.    }
    56.  
    Have you considered building component "collections" via reflection instead of using GetComponent? It might have a more upfront cost on entity creation but have savings over time by avoiding repeated GetComponent calls.

    Something like:
    Code (csharp):
    1.  
    2. class ShipProcessorCollection
    3. {
    4.      public PlayerInput playerInput;
    5.      public Ship ship;
    6.      public VectoredMovement vectoredMovement;
    7.      public Trigger2D trigger;
    8.      public Destroyable destroyable;
    9. }
    10.  
    Then use a list of those in the ShipProcessor system, created via reflection whenever a new entity is created.

    Really cool work - I'll have to dig deeper through your examples. Not quite sure how you add all the Processor systems to the "World", is it done through the editor and not script?
     
  3. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    The first thing people latch onto is always the calls to GetComponent :) I believe this comes from the fact that getting the Transform component's position and such must be calculated by walking the tree from the root up to the specific GameObject you need the position of (I think internally only the localPosition is actually stored on the Transform and the world transform is derived). GetComponent itself however is not actually slow. I myself at one point tried to cache calls to it via a Dictionary and ended up making things slower not faster, which was very surprising to me. Here is a benchmark of creating 1000 cubes that simply move a slight amount each update done via RobotArms and via traditional Update() on a MonoBehaviour. You can see the call to GetComponent is a non-factor.

    As for the processors, they are instantiated at startup via reflection by the only thing you need to add to your scene which is the RobotArmsCoordinator component.
     
  4. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I definitely agree about premature optimization but your benchmark isn't exactly fair -- one GetComponent call versus the dozen+ that you're already making in your test asteroids game. What about larger games where it starts to become 50 or 100 or 500 GetComponent calls per object, per frame? Have you tested to make sure it's sustainable at those sizes or is the framework aimed specifically at small games / prototyping?
     
  5. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    Interesting approach.

    How is this better than just creating a single Data object to be your entity and having all the behavior scripts reference the single object?

    - PlayerData
    - MovePlayer
    - PlayerCollisions
    - etc.

    I find scripts are supposed to be small and specialized and sharing code among multiple object types creates an mess after a while.

    For the sake of simplicity, I often take shortcuts: like hide objects when out of visible range automatically in the Move behavior. But this is easy to deal with when scripts are not shared among multiple objects.

    The biggest problem I could see with RobotArms is the amount of boilerplate code required for each unique logic. This might cause a developer to expand and reuse existing logic (introducing deeper complexity) instead of creating a new script (which maintains shallow complexity).

    What is your experience on a large project?

    Have you tried the alternatives of using a single DataComponent?
     
  6. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    While this is an interesting approach and I have to say, looking at the code quite well written and designed. I do agree with Grozzler regarding the GetComponent calls. Infact by doing that in Update (or Process as you have called it) you are going against what seems to be a rule within Unity - avoid GetComponent in update. In smaller projects, like your test project, and even in smallish games - I cant see any issues with it. But larger projects will definitely suffer from it. I think a larger test is required.

    Also, interfaces also solve a lot of the issues with interconnected components. I haven't come across a tightly coupled design that interfacing hasnt reduced yet.
     
  7. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    @Korno

    How do you access a component with an interface (I tried that back in Unity 4 and it wasn't supported)?

    What if you have multiple components of that interface type?


    I assume GetComponent<T> works with interfaces in Unity 5 now, are there any pitfalls or specific architecture patterns you have found work well?
     
  8. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    Unfortunately, get component still doesn't work for interfaces but it does work with abstract base classes - which I suppose we can just treat as if they were interfaces. Although you do lose the ability to have a component implement multiple interfaces in one script.

    I use this technique quite a lot in my code - for example

    Code (CSharp):
    1. // my player script
    2. if(Physics.Raycast(SourceCamera.transform.position, SourceCamera.transform.forward, out hit, DistanceToCheck, LayersToCheck))
    3.            {
    4.                 a = hit.transform.gameObject.GetComponent<InteractableObject>();
    5. a.Interact();
    6.            }
    7.  
    8. // the base class
    9. public abstract class InteractableObject : MonoBehaviour
    10.     {
    11. public abstract void Interact();
    12. // ...
    13. }
    14.  
    15. // simple example class
    16. public class TestActivator : InteractableObject
    17.     {
    18.         public override void Interact()
    19.         {
    20.             Debug.Log(name + " activated");
    21.         }
    22.     }
     
  9. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    @Korno Ah, I see. Too bad interfaces don't actually work...
     
  10. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    Perhaps I'm not being very clear in the benchmark. In once case there are 1000 objects each with a MonoBehaviour setting transform.position once in their Update.

    In the other case there is one MonoBehavior (the RobotArmsCoordinator) that runs the RobotArmsJiggleProcessor once for each of the 1000 objects that have the RobotArmsJiggle component. In that processor GetComponent<RobotArmsJiggle> is called and then transform.position is set.

    In both cases, the same work is done, the transform of the game object is updated based on the Vector3 value on the component (MonoBehaviorJiggle and RobotArmsJiggle respectively). 1000 GetComponent calls are made each frame in the case of RobotArms, 1000 calls to Update are made each frame in the case of the MonoBehavior. The benchmark shows that these end up with roughly equivalent performance. Are you saying that's unfair because in a real application you would need to call GetComponent more often? In that case I would disagree, each call to GetComponent in a processor is the equivalent of what would have been an Update on the component if the logic had been placed there as with traditional Unity style.
     
  11. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    Well you're free to make the components as granular or broad as is appropriate for your application. Having 1 giant data object seems pretty hard to maintain to me though. In our game Emberscape (I don't have great video, here is a work in progress one:

    )

    we have TONS of data, generally pretty specialized and broken out into lots of little components. Stuff like the player's stats (move speed, hp, mana regen rate, etc.) are fairly large and generally contain sub-objects for organization. Other things like the proximity alert when enemies get close to the exit have just a few fields. Since processors only have access to a single game object with the components they declare their interest in, you are pretty much guaranteed to be focusing on a small slice of functionality within that processor. We don't have things like PlayerProcessor that updates movement, stats, hit detection, etc. Instead we have an ActorMovementProcessor, ActorStatsProcessor, TrapBuilderProcessor, PathfindToProcessor, etc. Each of these processors is responsible for just one thing, doesn't know about the other processors, and functions just on the data in the components it is interested in. In this way, multiple processors can use the same data for different tasks without any conflict.

    So your example of turning off an object that's out of range would be extremely trivial in RobotArms. You would write a processor that looks at some component you've added to the game objects you want to have this behavior and then for each entity you would do your test, and if the object is out of range, hide it. Let's say your component is called HideWhenOffscreen, here's the code:

    Code (CSharp):
    1. public class HideWhenOffscreen : RobotArmsComponent { }
    2.  
    3. [ProcessorOptions(typeof(HideWhenOffscreen))]
    4. public class HideWhenOffscreenProcessor : RobotArmsProcessor {
    5.     public override void Process(GameObject entity) {
    6.         var renderer = entity.GetComponent<Renderer>();
    7.         if (!renderer.isVisible) {
    8.             // do whatever "hiding" means here
    9.         }
    10.     }
    11. }
    I find that because processors don't carry any state, and are only acting on the data passed to them, we can write lots more shared utility code than I've been able to do in the standard Unity approach (and I've been trying over the course of several large systems since 2009). Our processors implement the Process method and then every other bit of logic we have goes in static utility methods. These start out in the same class but are trivially moved into a shared utility class if we need the same functionality somewhere else. This has happened many times over the course of our current project. As for boilerplate, you're getting rid of Awake/Start/Update on your MonoBehaviours and adding Initialize/Process on the processor, it's hardly a huge expansion in lines of code.

    I don't know how you define a large project, my current one Emberscape (see linked video above) is fairly ambitious for a small indie studio. We've been extremely pleased with RobotArms which is why we want to share it with the community. I have no illusions that any large amount of Unity programmers will want to switch over, but for those who like me were dissatisfied with the standard Unity approach and have an inclination towards functional programming I think it might be what they are looking for.

    I have not tried a single data component because that would be a ridiculous number of fields for any non-trivial project. For Emberscape I would guess it would be in the high hundreds. Also, I think having a single data object destroys any sort of data partitioning you get with RobotArms, now your processor has to ignore 98% of the data and only deal with the few bits it cares about. It's sort of like making everything a singleton and then hoping your components always only touch the things they're supposed to given their single responsibility. I much prefer a DI style "tell me what you want and I'll give you only that", although RobotArms itself is NOT a traditional DI / IOC approach.
     
    Last edited: Jun 24, 2015
    Sisso and DotusX like this.
  12. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    I don't agree that avoiding calls to GetComponent is a rule. I think calling GetComponent<Transform> repeatedly is problematic, or at least it was pre Unity 5.

    From http://blogs.unity3d.com/2014/06/23/unity5-api-changes-automatic-script-updating/
    People got burned by slow calls to various components in the past, especially on mobile, but I think you overestimate its cost. I tried improving the performance of RobotArms even more by caching the component via a dictionary and found that the retrieval of the component via GetValue, (not even TryGetValue), was slower than just calling GetComponent. That is pretty impressively fast! Of course these little microbenchmarks mean nothing in many cases so all I can say is that my company is using RobotArms on our multiplayer real time action game, which is not AAA but is certainly not a small game. We are betting our rent / food / living money on it and I have no doubts that it is a good decision for the benefits in terms of code maintainability and ease of iteration (and we do LOTS of design iteration) it brings us.

    If you're happy using interfaces and traditional OO design, then great, keep making awesome games! No one is telling you that you shouldn't keep doing what works for you. My experience was different, even with highly decoupled interface based designs (also using MVP / MVVM).
     
    DotusX likes this.
  13. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Well take for example your ShipMovementProcessor which pulls 5 components to perform its logic and does roughly the same thing a "traditional" component would do as well. I think that's a more even comparison for benchmarking because if you had 10 of those types of systems with 100 objects, that's 5000 calls to GetComponent.

    It's great that you have it working in a larger scale project though - that's all the proof of concept you need. Appreciate you taking the time to respond as well. I definitely love this approach, as my very first post showed, and will dig through your project more.
     
    DotusX likes this.
  14. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    That is fair enough. I would like to try a functional design one time. I appreciate your comments on this and will keep an eye on your project.
     
    DotusX likes this.
  15. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76

    Thanks for explaining this.

    It makes me want to try RobotArms.

    I can certainly see the benefits you are expressing. I've used Entity based systems before and even tried to design my own before coming to Unity.

    Also, what do you think of the following idea:


    Code (CSharp):
    1.   using UnityEngine;
    2.    using RobotArms;
    3.  
    4.    [ProcessorOptions(typeof(Ship), typeof(PlayerInput), typeof(VectoredMovement))]
    5.    public class ShipMovementProcessor : RobotArmsProcessor {
    6.      public override void Process (GameObject entity) {
    7.        var input = entity.GetComponent<PlayerInput>();
    8.        var ship = entity.GetComponent<Ship>();
    9.        var movement = entity.GetComponent<VectoredMovement>();
    10.  
    11.        if (ship.Fuel > 0) {
    12.          if (input.Thrust) {
    13.            movement.Velocity += entity.transform.up * ship.ThrustForce * Time.deltaTime;
    14.            ship.Fuel -= ship.FuelConsumptionPerSecond * Time.deltaTime;
    15.          }
    16.  
    17.          entity.transform.Rotate(0, 0, -input.Rotation * ship.RotationSpeed * Time.deltaTime);
    18.        }
    19.      }
    20.    }
    could be:

    Code (CSharp):
    1.    using UnityEngine;
    2.    using RobotArms;
    3.  
    4.    public class ShipMovementProcessor : RobotArmsProcessor {
    5.      public void Process (GameObject entity, PlayerInput input, Ship ship, VectoredMovement movement) {
    6.  
    7.        if (ship.Fuel > 0) {
    8.          if (input.Thrust) {
    9.            movement.Velocity += entity.transform.up * ship.ThrustForce * Time.deltaTime;
    10.            ship.Fuel -= ship.FuelConsumptionPerSecond * Time.deltaTime;
    11.          }
    12.  
    13.          entity.transform.Rotate(0, 0, -input.Rotation * ship.RotationSpeed * Time.deltaTime);
    14.        }
    15.      }
    16.    }

    Since you are already using reflection to iterate through the Processor types to identify what each Processor needs, you can modify that a bit:

    - Reflect over the parameters in any Process methods and use those as the types for the ProcessorOptions
    - Use the untyped GetComponent(Type parameter) to get the Components to pass to the Process method
    - This could be done one time and then wrapped up into a anonymous method which would no longer need to use GetComponent and might therefore justify the cost of method.Invoke()

    Code (CSharp):
    1.     // Given ShipMovementProcessor processor;
    2.     // NOTE: This should be rebuilt if the relevant components change on the game object.
    3.     // This could be stored in an EntityInfo class that the processor engine would use to keep track of game objects: { GameObject gameobject; Action processCallback; }
    4.  
    5.     var type = processor.GetType();
    6.     var method = type.GetMethod("Process");
    7.     var pars = method.GetParameters();
    8.     ...
    9.     // In a loop
    10.     var compA = gameObject.GetComponent(pars[1]);
    11.     var compB = gameObject.GetComponent(pars[2]);
    12.     ...
    13.     // This could be built during the loop above
    14.     var argumentsList = new List<object>();
    15.     argumentsList.add(gameObject);
    16.     argumentsList.add(compA);
    17.     ...
    18.     var arguments = argumentsList.ToArray();
    19.  
    20.     return ()=>{
    21.         // No calls to GetComponent each frame for all the concerned :-)
    22.         method.invoke(processor, arguments);
    23.     };

    After having looked at this more I think RobotArms could be extended to support this while still supporting the Attribute declaration style:

    Code (CSharp):
    1. // In Coordinator:
    2.  
    3. Dictionary<RobotArmsProcessor, List<EntityInfo>> entitiesForProcessors;
    4. Dictionary<RobotArmsProcessor, List<EntityInfo>> entitiesForProcessorsToInitialize;
    5.  
    6. // EntityInfo:
    7.  
    8. public class EntityInfo
    9. {
    10.    public GameObject gameObject {get; private set;}
    11.    public Action processCallback {get; private set;}
    12.    ...
    13. }
    14.  
    15. // RegisterComponent:
    16.  
    17. foreach (var p in processors.Where(p => p.Options.RequiredTypes.Contains(component.GetType()) && p.IsInterestedIn(component.gameObject))) {
    18.    AddEntityInfo(entitiesForProcessors[p], component.gameObject, p);
    19.    AddEntityInfo(entitiesForProcessorsToInitialize[p], component.gameObject, p);
    20. }
    21.  
    22. // AddEntityInfo:
    23.  
    24. public void AddEntityInfo(IList<EntityInfo> list, GameObject gameObject, RobotArmsProcessor processor) {
    25.    if (!list.Any(e=>e.gameObject == elememnt)) {
    26.      list.Add(BuildEntityInfo(element, processor));
    27.    }
    28. }
    29.  
    30. // BuildEntityInfo:
    31.  
    32. public void BuildEntityInfo(GameObject entity, RobotArmsProcessor processor)
    33. {
    34.    return new EntityInfo(entity, processor.BuildProcessCall(entity));
    35. }
    36.  
    37.  
    38. // RobotArmsProcessor constructor:
    39.  
    40. public RobotArmsProcessor() {
    41.    if (GetType().GetCustomAttributes(typeof(ProcessorOptionsAttribute), true).Length == 0) {
    42.    
    43.      var options = BuildOptionsUsingReflection();
    44.      if(options != null){
    45.        Options = options
    46.        return;
    47.      }
    48.  
    49.      throw new Exception(string.Format("RobotArmsProcessor {0} requires a ProcessorOptions attribute", GetType()));
    50.    }
    51.  
    52.    Options = GetType().GetCustomAttributes(typeof(ProcessorOptionsAttribute), true)[0] as ProcessorOptionsAttribute;
    53. }
    54.  
    55. // BuildProcessCall:
    56.  
    57. public Action BuildProcessCall(GameObject entity)
    58. {
    59.    // A processor should only have one process method
    60.    var processMethod = this.GetType().GetMethod("Process");
    61.    var parameters = processMethod.GetParameters();
    62.  
    63.    ...
    64.    // Call entity.GetComponent for each required parameter type and put them all in the arguments array
    65.    ...
    66.  
    67.    return () => {
    68.      processMethod.invoke(processor, arguments);
    69.    };
    70. }
    71.  
    72. // ProcessAll:
    73.  
    74. public virtual void ProcessAll(IEnumerable<EntityInfo> entities) {
    75.    foreach (var entity in entities) {
    76.      if (entity.gameObject != null) {
    77.        entity.processCallback();
    78.      }
    79.    }
    80. }
     
    Last edited: Jun 24, 2015
    Silly_Rollo and eisenpony like this.
  16. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    I see now what you're saying. I would slightly disagree that one big monolithic component that does the same thing that the 5 different parts in the ShipMovementProcessor is doing is directly comparable. Sure you may have a slightly higher cost for GetComponent, but you're getting a huge improvement in the organization of your code (relatively speaking for such a small example). Now if the extra calls to GetComponent make your game go from 60FPS to 30FPS that's a pretty big problem. I took the example scene and turned the one component into 5 by breaking up the Vector3 and adding a few more fields. I did the same to the all-in-one MonoBehaviour as well. The RobotArms version definitely runs slower, I think mainly due to keeping track of so many more components, but when I do a deep profile and I go looking for GetComponent's contribution specifically I find this:

    So while I'm totally fine with the idea that RobotArms add some amount of overhead, I'm still looking for some sort of evidence that lots of GetComponent calls is in any way expensive relative to all the other bits going on in a real game. Once I put in some real geometry with real shaders, the blip on the radar of the GetComponents is going to be very very small. If you have a test scene that shows GetComponent behaving badly or would like my test scene please let me know.
     
    Last edited: Jun 24, 2015
    DotusX likes this.
  17. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    518
    Quick question(s), How does this handle interacting with the physics - OnXXXX methods. Also, how does this work with coroutines? How about if I need to touch physics objects (i.e. in FixedUpdate, when is Process called?) How about the UI, button clicks etc. How about if I wanted to stop using a Processor mid game, or wanted to add a Processor mid game (lets say in a fps where you can control vehicles, you might stop the fps controller and switch to a driving component when the player starts controlling a car)

    I am intrigued how your system can play well with the above stuff. Coroutines in particular - as they are a very powerful feature in Unity and I would hesitate to give them up.
     
  18. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Thanks for sharing! I really like the concept.
    I don't have experience making games yet but I think this will be a tool I use when I start trying.

    I like @Rick Love 's idea of using parameters to indicate what type of gameobjects I'm after. If I'm taking the time to declare that I need them, I will probably need to access them in my code and this seems more expressive. However, the attributes are important to express other choices such as priority and event lifecycle.. Supporting both does seem a bit messy, possibly confusing. Still I think it is a nice bit of syntactical sugar for the experienced user.

    @Korno, I see this library working pretty well either with or beside the concepts you asked about.

    Events (OnXXX methods) are a functional style of programming, so I see this framework as pushing your other code to be more in line with events. By default, there is no filter on objects which trigger events, so your code would need to make sure the GameObject has the correct components manually but perhaps this is something that could be added to the framework in future.

    Coroutines are simply a more general case of subroutines (methods). Coroutines are methods that can run more than once and maintain state. In this way, they can be used to achieve cooperative multitasking, which is how most people use them in Unity. In truth, the Update, FixedUpdate, etc.. methods are just coroutines that never stop. I can't see any inconsistencies between dkoontz's framework and coroutines.

    For physics stuff, if you read through the documentation you'll find this beauty
    So there's no reason not to do physics stuff - this simply saves your Fixedupdate code from calling the processor directly in the GameObject, or looking up the correct GameObjects from the processor. It's just a registration trick.

    The framework is basically just providing a hook for scripts to do two things through declarative code:
    1. Ask to receive all GameObjects at certain times during the existing Unity lifecycle
    2. Ask that the list of GameObjects be filtered to only include "interesting" GameObjects
    It doesn't really change anything fundamental in the Unity pipeline.
     
    DotusX likes this.
  19. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    Thanks Rick for digging into this. Collaborating with people is the entire reason we made RobotArms available and started to promote it.
    I had originally considered doing something like this. In some entity component systems you are only passed the components directly you never do a GetComponent type call. The issues I had with implementing it this way were threefold.

    1) The cost of Invoke is pretty high if you can't convert it to a delegate with Delegate.CreateDelegate which we can't since we wouldn't know the types. I did some benchmarking comparing calling Invoke 1000 times per frame vs calling Process. If you do 5 GetComponent calls in Process the two are roughly equivalent.

    6.37 ms Invoke
    5.55 ms GetComponent x5

    If you only do 3 GetComponent calls however (which is about the norm in my experience) the GetComponent version is faster by quite a bit

    6.11 ms Invoke
    4.37 ms GetComponent x3

    At one GetComponent call the gap is even wider as you might expect

    6.15 ms Invoke
    3.12 ms GetComponent x1

    Clearly Invoke is going to scale better to large number of GetComponent calls, however looking through our codebase that is not a typical usage pattern. Perhaps there are ways of speeding up Invoke that I haven't though of though, that would make it more competitive and a viable way to implement the Process method.

    It is entirely possible to declare interest in an entity that has a component that you only conditionally need. In those cases the passing in of the component is unnecessary.
     
  20. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    Things that are only accessible via Unity's callbacks are a bit of a pain. You have to take the data the way Unity provides it and turn it into simple data such as a list of Triggers/Colliders. Here is our Trigger component

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using RobotArms;
    4.  
    5. namespace Emberscape {
    6.     public class Trigger : RobotArmsComponent {
    7.  
    8.         public List<Collider> Colliders = new List<Collider>();
    9.  
    10.         public void OnTriggerEnter(Collider other) {
    11.             Colliders.Add(other);
    12.         }
    13.  
    14.         public void OnTriggerExit(Collider other) {
    15.             Colliders.Remove(other);
    16.         }
    17.  
    18.         public new void OnDisable() {
    19.             base.OnDisable();
    20.             Colliders.Clear();
    21.         }
    22.     }
    23. }
    So this is a component that does have some functionality but only the bare minimum to allow the rest of the system to treat it as a flat list of Triggers.

    As for coroutines, you can start a coroutine just fine from a processor. You have a game object (the entity) so you can start the coroutine on that.

    As for starting/stopping processors, we have discussed being able to turn them on and off, which is certainly doable but not implemented. More likely you'd simply disable any game objects with your driving related components which would remove them from the list to be processed and then re-enable them, which would register them again as is done in Awake.
     
  21. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76

    Ok, that's interesting about the benchmark.

    Ahaa! What about having some generic helper classes to achieve the same?

    Code (CSharp):
    1. using UnityEngine;
    2. using RobotArms;
    3.  
    4. public class ShipMovementProcessor : RobotArmsProcessor<PlayerInput, Ship, VectoredMovement> {
    5. public void Process (GameObject entity, PlayerInput input, Ship ship, VectoredMovement movement) {
    6.  
    7.   if (ship.Fuel > 0) {
    8.     if (input.Thrust) {
    9.     movement.Velocity += entity.transform.up * ship.ThrustForce * Time.deltaTime;
    10.     ship.Fuel -= ship.FuelConsumptionPerSecond * Time.deltaTime;
    11.     }
    12.  
    13.     entity.transform.Rotate(0, 0, -input.Rotation * ship.RotationSpeed * Time.deltaTime);
    14.   }
    15. }
    16. }
    17.  
    18. // This would require a few generic RobotArmsProcessor classes:
    19.  
    20.  
    21. public class RobotArmsProcessor<T1,T2,T3> : RobotArmsProcessor
    22. {
    23.    private T1 _comp1;
    24.    private T2 _comp2;
    25.    private T3 _comp3;
    26.  
    27.    public override void Process( GameObject entity )
    28.   {
    29.      if( _comp1 == null){
    30.        _comp1 = entity.GetComponent<T1>();
    31.        _comp2 = entity.GetComponent<T2>();
    32.        _comp3 = entity.GetComponent<T3>();
    33.      }
    34.  
    35.      Process(entity, _comp1, _comp2, _comp3);
    36.    }
    37.  
    38.    public abstract void Process( GameObject entity, T1 component1, T2 component2, T3 component3 );
    39. }
    40.  
    You could make as many of these as needed (up to 7 components is surely enough).

    Cached components and friendlier syntax!

    The only modification needed in RobotArms is to parse the GenericTypeArguments as an alternative to the attribute.

    This would be great to specify the RequiredComponents. Any other options: tag, phase priority can still be specified in the Attribute if needed. Optional components and other data is still available directly through the GameObject.

    Bonus: Visual Studio will generate the Process method for you when you choose "Implement Abstract Method".

    So, this eliminates a bunch of the boilerplate and let's Visual Studio give you the structure as soon as you declare the base class types.
     
    Last edited: Jun 24, 2015
    DotusX and GroZZleR like this.
  22. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    I don't like having to add a bunch of stuff to each processor but I see what you're getting at here. I'm going to do some poking around to see if I can reduce this down into something very clean that doesn't require the extra methods to be written per-processor.
     
  23. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    Well, you can always keep the current Process method and base class and change over slowly.

    If it were me, I would do a regex find/replace in all files and update all written processors at once, perform testing, benchmark, and then check this in as a single update to the repo.

    I would be interested in hearing the benchmark results (since this will cache your component calls in the default use).
     
  24. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    Ok I've done the changes and checked them in.

    Original AsteroidProcessor

    Code (CSharp):
    1. [ProcessorOptions(typeof(Asteroid), typeof(VectoredMovement), typeof(Destroyable))]
    2. public class AsteroidProcessor : RobotArmsProcessor {
    3.  
    4.     public override void Initialize(GameObject entity) {
    5.         var asteroid = entity.GetComponent<Asteroid>();
    6.         var vectoredMovement = entity.GetComponent<VectoredMovement>();
    7.  
    8.         SetSpriteAndBoundsForCurrentSize(asteroid);
    9.         RandomizeAsteroidMovement(vectoredMovement);
    10.     }
    11.  
    12.     public override void Process(GameObject entity) {
    13.         var asteroid = entity.GetComponent<Asteroid>();
    14.         var vectoredMovement = entity.GetComponent<VectoredMovement>();
    15.         var destroyable = entity.GetComponent<Destroyable>();
    16.         var asteroidRoot = GetBlackboard<Blackboard>().AsteroidRoot;
    17.  
    18.     ...
    19.   }
    20. }
    New style using templated base class

    Code (CSharp):
    1. public class AsteroidProcessor : RobotArmsProcessor<Asteroid, VectoredMovement, Destroyable> {
    2.  
    3.     public override void Initialize(GameObject entity, Asteroid asteroid, VectoredMovement vectoredMovement, Destroyable destroyable) {
    4.         SetSpriteAndBoundsForCurrentSize(asteroid);
    5.         RandomizeAsteroidMovement(vectoredMovement);
    6.     }
    7.  
    8.     public override void Process(GameObject entity, Asteroid asteroid, VectoredMovement vectoredMovement, Destroyable destroyable) {
    9.         ...
    10.     }
    11. }
    I'll do some more benchmarking in a bit. This is uploaded to Bitbucket now.
     
    DotusX likes this.
  25. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    I did some testing in a fairly extreme case, with a processor that wants 5 different components. The performance went from 8.49ms for 1000x calls to the processor to 4.35ms so quite a nice improvement. I still maintain that in a real app all of this is pretty much a teeny tiny blip. Free perf is great though since it came with a nice syntactic improvement. I'll be going through the Emberscape codebase this weekend and implementing this same change on all 51 of our processors.
     
    DotusX likes this.
  26. dkoontz

    dkoontz

    Joined:
    Aug 7, 2009
    Posts:
    198
    So the Emberscape codebase is now converted, everything went over very smoothly and the game runs just fine. I should probably put a version number on the thing and call this 1.0.
     
    eisenpony likes this.
  27. Rick Love

    Rick Love

    Joined:
    Oct 23, 2014
    Posts:
    76
    Great! I'm glad that was helpful.

    I'm still thinking about how I would design an ideal architecture to achieve the goals
     
    DotusX likes this.
  28. Tzan

    Tzan

    Joined:
    Apr 5, 2009
    Posts:
    736
    I looked through Artemis last year but because of its non Unity nature I let it sit a bit.
    Then 4.6 was released and I got distracted. :)

    Looking forward to going through this, Thanks!
     
  29. sluice

    sluice

    Joined:
    Jan 31, 2014
    Posts:
    416
    Will definitely be checking this out, thanks for sharing! :cool:
     
  30. sluice

    sluice

    Joined:
    Jan 31, 2014
    Posts:
    416
    Yes it does, try it out. It's works fine in Unity 5.

    Yes it was but you couldn't use generics:
    Code (csharp):
    1. IMyInterface myInterface = (IMyInterface)GetComponent(typeof(IMyInterface));
     
    DotusX likes this.
  31. Ben-BearFish

    Ben-BearFish

    Joined:
    Sep 6, 2011
    Posts:
    1,204
    @dkoontz I began reviewing your framework system, and as I've just began to look at it, I still don't quite understand how'd you'd integrate this framework with Unity's uGUI system. Could you perhaps provide an example for that?
     
  32. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Hey, I'm just now getting around to actually looking at this... looks very interesting indeed.

    But it looks like maybe some of the text of the ReadMe (at https://bitbucket.org/dkoontz/robotarms) hasn't been updated with the new syntax? It's still talking a lot about ProcessorOptions and GetComponent calls that aren't actually there.
     
  33. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    So, having read through the tutorial, I get the benefit of having components that represent nothing but data, and other bits of code that operate on that data — using data as the means of communication between processors.

    What I don't get is why you wouldn't do exactly that, with standard Monobehaviours. In other words, you could just make it a coding standard: some components (perhaps with a naming convention like "xxxData") contain only data, no logic; and other components ("xxxProcessor"?) contain only logic, no data. Add the mix of data and logic that you need to any GameObject. You could even use RequireComponent to ensure that when you add the logic, you have the data they require.

    The advantage of that is that you could still have private data. I actually like private data; some things are implementation detail, and shouldn't be mucked with (or even looked at) by anybody else. The RobotArms approach, as far as I can tell, pretty much requires that all your private parts be right out there for everybody to see. It's the code equivalent of a nudist colony.

    The advantage of RobotArms, if I understand it correctly, is that you don't have to add the processors to anything. They automatically get invoked with any objects that have the appropriate data. That's neat, but I don't know if it's worth the loss of privacy. But are there other advantages I'm not seeing?