👥

👨‍💻

Unity
Unity DOTS (ECS)
Unity Netcode for Entities
C#
Visual Studio
GitHub
Miro

Solution adoptée :

J'ai développé un système hybride qui utilise des buffers dynamiques pour stocker les paramètres d'animation (float, int, bool, trigger) sous forme de composants ECS avec synchronisation réseau via GhostComponent. Sur le serveur, un système collecte les changements d'état et les stocke dans ces buffers, tandis qu'un autre système les applique à l'Animator Unity traditionnel. Sur le client, le système reçoit automatiquement les données répliquées et met à jour les animations localement. Cette architecture permet une séparation claire entre la logique ECS et le rendu, tout en garantissant une synchronisation fluide des animations entre tous les joueurs connectés.

C# Networked Hybrid Animation System
public class AnimatorReference : IComponentData
{
    public Animator Animator;
}

[GhostComponent]
public struct AnimationFloatBufferElement : IBufferElementData
{
    [GhostField] public FixedString32Bytes Parameter;
    [GhostField] public float Value;
}

[GhostComponent]
public struct AnimationIntBufferElement : IBufferElementData
{
    [GhostField] public FixedString32Bytes Parameter;
    [GhostField] public int Value;
}

[GhostComponent]
public struct AnimationBoolBufferElement : IBufferElementData
{
    [GhostField] public FixedString32Bytes Parameter;
    [GhostField] public bool Value;
}

[GhostComponent]
public struct AnimationTriggerBufferElement : IBufferElementData
{
    [GhostField] public int NetworkId;
    [GhostField] public FixedString32Bytes Parameter;
}

[UpdateInGroup(typeof(SimulationSystemGroup), OrderFirst = true)]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
partial struct ServerClearAnimationBufferSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<AnimatorReference>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (floatBuffer, intBuffer, boolBuffer, triggerBuffer, entity) in SystemAPI
            .Query<DynamicBuffer<AnimationFloatBufferElement>, DynamicBuffer<AnimationIntBufferElement>,
            DynamicBuffer<AnimationBoolBufferElement>, DynamicBuffer<AnimationTriggerBufferElement>>()
            .WithEntityAccess())
        {
            floatBuffer.Clear();
            intBuffer.Clear();
            boolBuffer.Clear();
            triggerBuffer.Clear();
        }
    }
}

[UpdateInGroup(typeof(LateSimulationSystemGroup))]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
partial class ServerAnimationSystem : SystemBase
{
    [BurstCompile]
    protected override void OnCreate()
    {
        RequireForUpdate<AnimatorReference>();
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        foreach (var (animatorRef, floatBuffer, intBuffer, boolBuffer, triggerBuffer) in SystemAPI
            .Query<AnimatorReference, DynamicBuffer<AnimationFloatBufferElement>, DynamicBuffer<AnimationIntBufferElement>,
            DynamicBuffer<AnimationBoolBufferElement>, DynamicBuffer<AnimationTriggerBufferElement>>())
        {
            if (animatorRef.Animator == null) continue;

            AnimationUtils.UpdateFloatParameter(animatorRef.Animator, floatBuffer);
            AnimationUtils.UpdateIntParameter(animatorRef.Animator, intBuffer);
            AnimationUtils.UpdateBoolParameter(animatorRef.Animator, boolBuffer);
            AnimationUtils.UpdateTriggerParameter(animatorRef.Animator, triggerBuffer);
        }
    }
}

[UpdateInGroup(typeof(LateSimulationSystemGroup))]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
partial class ClientAnimationSystem : SystemBase
{
    [BurstCompile]
    protected override void OnCreate()
    {
        RequireForUpdate<AnimatorReference>();
    }

    [BurstCompile]
    protected override void OnUpdate()
    {
        foreach (var (animatorRef, floatBuffer, intBuffer, boolBuffer, triggerBuffer) in SystemAPI
            .Query<AnimatorReference, DynamicBuffer<AnimationFloatBufferElement>, DynamicBuffer<AnimationIntBufferElement>,
            DynamicBuffer<AnimationBoolBufferElement>, DynamicBuffer<AnimationTriggerBufferElement>>())
        {
            if (animatorRef.Animator == null) continue;

            AnimationUtils.UpdateFloatParameter(animatorRef.Animator, floatBuffer);
            AnimationUtils.UpdateIntParameter(animatorRef.Animator, intBuffer);
            AnimationUtils.UpdateBoolParameter(animatorRef.Animator, boolBuffer);
            AnimationUtils.UpdateTriggerParameter(animatorRef.Animator, triggerBuffer);

            floatBuffer.Clear();
            intBuffer.Clear();
            boolBuffer.Clear();
            triggerBuffer.Clear();
        }
    }
}

public class AnimationUtils
{
    // Only used in Animation System
    public static void UpdateFloatParameter(in Animator animator, DynamicBuffer<AnimationFloatBufferElement> floatBuffer)
    {
        foreach (var floatElement in floatBuffer)
        {
            animator.SetFloat(floatElement.Parameter.ToString(), floatElement.Value);
        }
    }

    // Only used in Animation System
    public static void UpdateIntParameter(in Animator animator, DynamicBuffer<AnimationIntBufferElement> intBuffer)
    {
        foreach (var intElement in intBuffer)
        {
            animator.SetInteger(intElement.Parameter.ToString(), intElement.Value);
        }
    }

    // Only used in Animation System
    public static void UpdateBoolParameter(in Animator animator, DynamicBuffer<AnimationBoolBufferElement> boolBuffer)
    {
        foreach (var boolElement in boolBuffer)
        {
            animator.SetBool(boolElement.Parameter.ToString(), boolElement.Value);
        }
    }

    // Only used in Animation System
    public static void UpdateTriggerParameter(in Animator animator, DynamicBuffer<AnimationTriggerBufferElement> triggerBuffer)
    {
        foreach (var triggerElement in triggerBuffer)
        {
            animator.SetTrigger(triggerElement.Parameter.ToString());
        }
    }

    // Only used in Animation System
    public static void SetAnimator(in Animator animator, in Entity entity, in EntityCommandBuffer ecb, in EntityManager entityManager)
    {
        if (entityManager.HasComponent<AnimatorReference>(entity))
        {
            AnimatorReference animatorRef = entityManager.GetComponentObject<AnimatorReference>(entity);

            if (animatorRef.Animator != animator)
            {
                animatorRef.Animator = animator;
            }
        }
    }

    // To be used to set a parameter outside of a job
    [BurstCompile]
    public static void AddFloatCommand(in FixedString32Bytes name, in float value, in Entity entity, in EntityCommandBuffer ecb)
    {
        ecb.AppendToBuffer(entity, new AnimationFloatBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter in a job
    [BurstCompile]
    public static void AddFloatCommandJob(in FixedString32Bytes name, in float value, in Entity entity, 
        in EntityCommandBuffer.ParallelWriter ecb, in int sortKey)
    {
        ecb.AppendToBuffer(sortKey, entity, new AnimationFloatBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter outside of a job
    [BurstCompile]
    public static void AddIntCommand(in FixedString32Bytes name, in int value, in Entity entity, 
        in EntityCommandBuffer ecb)
    {
        ecb.AppendToBuffer(entity, new AnimationIntBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter in a job
    [BurstCompile]
    public static void AddIntCommandJob(in FixedString32Bytes name, in int value, in Entity entity, 
        in EntityCommandBuffer.ParallelWriter ecb, in int sortKey)
    {
        ecb.AppendToBuffer(sortKey, entity, new AnimationIntBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter outside of a job
    [BurstCompile]
    public static void AddBoolCommand(in FixedString32Bytes name, in bool value, in Entity entity, 
        in EntityCommandBuffer ecb)
    {
        ecb.AppendToBuffer(entity, new AnimationBoolBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter in a job
    [BurstCompile]
    public static void AddBoolCommandJob(in FixedString32Bytes name, in bool value, in Entity entity, 
        in EntityCommandBuffer.ParallelWriter ecb, in int sortKey)
    {
        ecb.AppendToBuffer(sortKey, entity, new AnimationBoolBufferElement
        {
            Parameter = name,
            Value = value,
        });
    }

    // To be used to set a parameter outside of a job
    [BurstCompile]
    public static void AddTriggerCommand(in FixedString32Bytes name, in Entity entity, 
        in EntityCommandBuffer ecb)
    {
        ecb.AppendToBuffer(entity, new AnimationTriggerBufferElement
        {
            Parameter = name,
        });
    }

    // To be used to set a parameter in a job
    [BurstCompile]
    public static void AddTriggerCommandJob(in FixedString32Bytes name, in Entity entity, 
        in EntityCommandBuffer.ParallelWriter ecb, in int sortKey)
    {
        ecb.AppendToBuffer(sortKey, entity, new AnimationTriggerBufferElement
        {
            Parameter = name,
        });
    }
}