Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to animate in ECS

Discussion in 'Entity Component System' started by Krajca, Apr 3, 2018.

  1. Krajca

    Krajca

    Joined:
    May 6, 2014
    Posts:
    347
    Is there any way to animate in pure ECS? I know that at some point unity will have it's old animator adapted or something but can I do it now somehow?
     
  2. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
  3. laurentlavigne

    laurentlavigne

    Joined:
    Aug 16, 2012
    Posts:
    6,225
    If you want to use the Animator, you maintain a dictionary anims = (index, animator) outside ECS, create an animator system that outputs animTriggers = (index, animator hash that's triggered) and at the end of the ECS chain have the main thread go through the output and SetTrigger(anims[index], animTriggers[index])
    In other word, wait for Animator api to be exposed :D
     
    alexchesser likes this.
  4. SubPixelPerfect

    SubPixelPerfect

    Joined:
    Oct 14, 2015
    Posts:
    224
    define an animation curve mathematically on in any other way, in update method of your animation system sample that curve and adjust property you animate according to it
     
  5. recursive

    recursive

    Joined:
    Jul 12, 2012
    Posts:
    669
    For baked animations, as mentioned by @Spy-Shifty above, you'd obviously go a custom route.

    For a hybrid ECS, where we're still dealing with the current Animator component but we don't need to read from it as much, you could use the "Entities as events" pattern and write systems around that.

    So you'd have something like this:
    Code (CSharp):
    1.  
    2. public struct AnimatorEvent : ISharedComponentData
    3. {
    4.         public Entity Target;
    5. }
    6.  
    7. public struct SetAnimatorBool : IComponentData
    8. {
    9.         public bool1 Value;
    10.         public int ParameterHash;
    11. }
    12.  
    13. public struct SetAnimatorFloat : IComponentData
    14. {
    15.         public float Value;
    16.         public int ParameterHash;
    17. }
    18.  
    19. public struct SetAnimatorInt : IComponentData
    20. {
    21.         public int Value;
    22.         public int ParameterHash;
    23. }
    24.  
    25. public struct SetAnimatorTrigger : IComponentData
    26. {
    27.         public int ParameterHash;
    28. }
    Implementing a transfer system is left as an exercise to the reader.
     
  6. MadeFromPolygons

    MadeFromPolygons

    Joined:
    Oct 5, 2013
    Posts:
    3,967
    I would look into GPU skinning like on the nvidia papers. Thats the real way to do this, and would enable 1000s of uniquely animated instanced characters at once

    Like this: https://github.com/chengkehan/GPUSkinning
     
    Bas-Smit and recursive like this.
  7. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,114
    Are u adding those ComponentData to entity by code or u implement ComponentDataWrapper then attach to GameObject?
     
  8. recursive

    recursive

    Joined:
    Jul 12, 2012
    Posts:
    669
    You could do it either way, this was just a quick example.
     
  9. Krajca

    Krajca

    Joined:
    May 6, 2014
    Posts:
    347
  10. purpl3grape

    purpl3grape

    Joined:
    Oct 18, 2015
    Posts:
    8
    I lose sleep because of the wait..
     
  11. wang37921

    wang37921

    Joined:
    Aug 1, 2014
    Posts:
    102
    Do you remove the event-ComponentData after the system used it.
    how to check entity have a IComponentData in job? or just subtract these event-ComponentData type in event trigger system?
     
  12. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    One approach is to mass destroy all events at the end frame but it will create problem which the system that runs before an event in a frame will not get a chance to handle the event. (But that system should have UpdateAfter the event invoker anyways as the correct design) This allows multiple system to handle one event like a subscriber on a bus.

    An another approach is to destroy event entity after handling it, the problem is that only one system can handle one event.

    An another approach is to have the system that already used an event attach its own ISystemStateComponentData to the event saying that it had handled the event. Then you could subtractive this component

    To check for existence of component in a job with events I think this would be quite efficient :
    1. All events no matter what the type should additionally have Event : IComponentData in its archetype.
    2. Use chunk iteration : query chunks with Event : IComponentData. You then get as many chunks as currently exist event types. Each chunk surely has Event but not sure which specific type.
    3. Bring ArchetypeChunkComponentType<T> of the event type you want to handle in a job.
    4. In an IJobChunk use archetypeChunk.Has with that ACCT, if false you skip that chunk by returning immediately. This way it works in parallel per event type since IJobChunk compute 1 chunk per worker thread. It is like you can simultaneously if for the correct event from a pile of events.

    Back to animate topic, it is quite usable if your animation can be made into a timeline asset. Timeline turns animation from state machine fire and forget style into a data oriented style animation. That is, the whole thing's appearance can be determined purely from data, the timeline's time value (+ evaluating that time)

    You then just need a hybrid system which copy one running float from ECS to PlayableDirector.
     
    Last edited: Nov 29, 2018
    recursive likes this.
  13. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    There is another principle!
    The creator of an event should also be responsive for destroying an event.

    This could look like this:
    Code (CSharp):
    1.  
    2. public class FireEventSystem : JobComponentSystem {
    3.  
    4.     public void FireEvent(EventData eventData) {
    5.         eventQueue.Enqueue(eventData);
    6.     }
    7.  
    8.     NativeQueue<EventData> eventQueue = new NativeQueue<EventData>();
    9.  
    10.     struct DestroyEventsJob : IJobProcessComponentDataWithEntity<EventData> {
    11.         public EntityCommandBuffer.Concurrent ComponentBuffer;
    12.         public void Execute(Entity entity, int index, [ReadOnly] ref EventData data) {
    13.             ComponentBuffer.DestroyEntity(index, entity);
    14.         }
    15.     }
    16.  
    17.     struct CreateEventsJob : IJob {
    18.         public EntityCommandBuffer ComponentBuffer;
    19.         public NativeQueue<EventData> EventQueue;
    20.         public void Execute() {
    21.             while (EventQueue.TryDequeue(out EventData eventData)) {
    22.                 ComponentBuffer.CreateEntity();
    23.                 ComponentBuffer.AddComponent(eventData);
    24.             }
    25.         }
    26.     }
    27.  
    28.     protected override void OnCreateManager() {
    29.         if (!eventQueue.IsCreated) {
    30.             eventQueue = new NativeQueue<EventData>(Allocator.Persistent);
    31.         }
    32.     }
    33.  
    34.     protected override void OnDestroyManager() {
    35.         if (eventQueue.IsCreated) {
    36.             eventQueue.Dispose();
    37.         }
    38.     }
    39.  
    40.     [Inject] EndFrameBarrier EndFrameBarrier;
    41.     protected override JobHandle OnUpdate(JobHandle inputDeps) {
    42.         var destroyEventsJob = new DestroyEventsJob {
    43.             ComponentBuffer = EndFrameBarrier.CreateCommandBuffer().ToConcurrent(),
    44.         };
    45.         var createEventsJob = new CreateEventsJob {
    46.             ComponentBuffer = EndFrameBarrier.CreateCommandBuffer(),
    47.             EventQueue = eventQueue,
    48.         };
    49.         inputDeps = destroyEventsJob.Schedule(this, inputDeps);
    50.         inputDeps = createEventsJob.Schedule(inputDeps);
    51.         return inputDeps;
    52.     }
    53. }
    54.  
    55.  
    56. //To create an event:
    57. World.Active.GetExistingManager<FireEventSystem>().FireEvent(new EventData());
    58.  
    59. struct EventData : IComponentData {
    60.    public int someValues;
    61. }
    62.  
    63.  
    This way every system can see an event! And you don't have to worry about destroying an event!
     
    Last edited: Dec 6, 2018
  14. recursive

    recursive

    Joined:
    Jul 12, 2012
    Posts:
    669
    @Spy-Shifty 's suggestion is exactly what I've migrated to for most event use cases that need to be invoked from outside the ECS world.
     
    Spy-Shifty likes this.
  15. wang37921

    wang37921

    Joined:
    Aug 1, 2014
    Posts:
    102
    Cool!
    But how to keep DestroyEventsJob.Execute after all the other System?
    System can use [UpdateAfter] attribute, but job's execute order, how to keep it?
     
    Last edited: Nov 30, 2018
  16. wang37921

    wang37921

    Joined:
    Aug 1, 2014
    Posts:
    102
    Thanks, you're very active in this topic.
    I havent learn about ArchetypeChunkComponentType, I need check the reference. It can be used in Job?
    And use timeline to drive animation is nice.
     
  17. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Yes, the whole point of ACCT is to provide a compact container for a type which can go to the job with you. Kind of like ComponentType, but this one is baked. Nothing inside it should contact the TypeManager class for more info. ArchetypeChunk understands ACCT and can get real data of the correct type. An another advantage of ACCT is that it uses generic, this pattern propagates the "type safety" inside your job works.

    The pattern he propose destroy the events at EndFrameBarrier, that barrier is implied to run after all systems like the name says. At the same time fire all new events queued up.

    Note that this pattern means that systems running immediately after the system that fires an event might have to wait for one more round of update. For example if D fires an event and you expect system F to receive an event before D can update again, that is wrong since events are queued up to be created at EFB. In some case this might results in a bug if you expect D's logic to do something that F produces as a result of that event.

    [ A > B > C > D > E > F > G > EFB]

    One more thing is that EFB has [UpdateBefore(typeof(Initialization))] and I expect it to be in the Intialization step. In the debugger sometimes it is but sometimes it is not (and is *around* the end of Update step but not really at the end) maybe it is a bug.

    My pattern works like this :
    1. MessageDestroyerSystem (MDS) with no update order. Injects a component common to all messages and destroy them all. (If a destroy chunk command exist in the future this should be fast, I would query chunks with that common message component and destroy them all)
    2. Message invoker use their own ECB to fire event (Be it PostUpdateCommand, or some other barrier scattered around the game loop). I can expect systems after that barrier and before MDS to be able to handle the event right away, this is an advantage. But disadvantage is the need of their own barriers. This will often breaks potential parallelism you could have had.
    3. Message invoker must declare [UpdateBefore(MDS)]
    4. Message receiver must declare [UpdateBefore(MDS)] and [UpdateAfter(*message invoker's barrier or just the message invoker in the case of PostUpdateCommand*)]
    5. Systems before the message invoker have no chance to handle event. This is a weakness of this pattern, but I order the system carefully to mitigate this.
    6. If you follow 3. and 4. correctly, MDS with no update order should be at the correct place, at around the end frame. It is a pain to type UpdateBefore requirement for all event users but I believe the auto arranged system loop should give me good parallelism.
    7. Message receivers may derive from a custom class with [UpdateBefore(MDS)] since attribute tag can be inherited to subclass. Along with this I added some helper methods to get the correct message (event).
     
    Last edited: Nov 30, 2018
  18. wang37921

    wang37921

    Joined:
    Aug 1, 2014
    Posts:
    102
    Thanks, I get help from your expatiation:p
     
  19. Spy-Shifty

    Spy-Shifty

    Joined:
    May 5, 2011
    Posts:
    546
    One more weekness of creating an entity right away and not in the EndFrameBarrier (EFB) is, that you force all your jobs to synchronize with the main thread at this point. Everytime you create/remove components or entities you force all jobs to sync with the main thread.

    Thats why I use EFB! I think there are rare cases there you really need a just in time reaction...

    Edit:
    Anyway, with "jobification" in mind you have to go away from such "just in time constructions".
    They force you to run your code in sequence and not in parrallel.
     
    wang37921 likes this.
  20. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Actually not all, barriers only complete all returned inputDep handles collected from JobComponentSystem that [Inject] them. So one barrier won't interfere other barrier's handle unless one JCS [Inject] more than one barrier. But still sync is bad.
     
    wang37921 and Spy-Shifty like this.