com.unity.netcode.gameobjects icon indicating copy to clipboard operation
com.unity.netcode.gameobjects copied to clipboard

Network Animator Override

Open EuNextti opened this issue 2 years ago • 1 comments

DESCRIPTION: My project is a Multiplayer 2D Game with sprite based animations and it uses AnimatorOverrideController to create a runtime character customization (change hair, use weapons and more). It use OwnerNetworkAnimator and the animations works, but when i override the character animator by changing it skin color for example, it change only for the owner himself.

THE CODE:

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

public class Player : NetworkBehaviour
{
    [SerializeField] private float speed = 3f;

    private Rigidbody2D rb;
    private Animator animator;

    [SerializeField] private CharacterBody characterBody;
    [SerializeField] private BodyPart[] parts;

    [SerializeField] private string[] partTypes;
    [SerializeField] private string[] directions;
    [SerializeField] private string[] states;

    private AnimatorOverrideController overrideController;
    private AnimationClipOverrides defaultClips;
    private AnimationClip animationClip;

    private void Start()
    {
        if(!IsOwner)
        {
            return;
        }

        rb = gameObject.GetComponent<Rigidbody2D>();
        animator = gameObject.GetComponent<Animator>();

        overrideController = new(animator.runtimeAnimatorController);
        animator.runtimeAnimatorController = overrideController;

        defaultClips = new(overrideController.overridesCount);
        overrideController.GetOverrides(defaultClips);
    }

    private void Update()
    {
        if(!IsOwner)
        {
            return;
        }

        float inputX = Input.GetAxisRaw("Horizontal");
        float inputY = Input.GetAxisRaw("Vertical");

        Vector2 moveDirection = new Vector2(inputX, inputY).normalized;

        DoMovement(moveDirection, inputX, inputY);

        ChangeSkinColor();
    }

    private void DoMovement(Vector2 moveDirection, float inputX, float inputY)
    {
        ChangeAnimatorParams(moveDirection, inputX, inputY);
        rb.velocity = new Vector2(moveDirection.x * speed, moveDirection.y * speed);
    }

    private void ChangeSkinColor()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            characterBody.characterBodyParts[0].bodyPart = parts[0];
            UpdateParts();
        }
        if (Input.GetKeyDown(KeyCode.E))
        {
            characterBody.characterBodyParts[0].bodyPart = parts[1];
            UpdateParts();
        }
    }

    private void ChangeAnimatorParams(Vector2 moveDirection, float inputX, float inputY)
    {
        if (moveDirection.magnitude > 0)
        {
            animator.SetFloat("moveX", inputX);
            animator.SetFloat("moveY", inputY);
            animator.SetBool("isWalking", true);
        }
        else
        {
            animator.SetBool("isWalking", false);
        }
    }

    private void UpdateParts()
    {
        for (int x = 0; x < partTypes.Length; x++)
        {
            string partType = partTypes[x];
            string partID = characterBody.characterBodyParts[x].bodyPart.bodyAnimationId.ToString();

            for (int y = 0; y < states.Length; y++)
            {
                string state = states[y];

                for (int z = 0; z < directions.Length; z++)
                {
                    string direction = directions[z];

                    animationClip = Resources.Load<AnimationClip>("Player Animations/" + partType + "/" + partType + "_" + partID + "_" + state + "_" + direction);
                    defaultClips[partType + "_" + 0 + "_" + state + "_" + direction] = animationClip;
                }
            }
        }
        overrideController.ApplyOverrides(defaultClips);
    }

    public class AnimationClipOverrides : List<KeyValuePair<AnimationClip, AnimationClip>>
    {
        public AnimationClipOverrides(int capacity) : base(capacity) { }

        public AnimationClip this[string name]
        {
            get { return this.Find(x => x.Key.name.Equals(name)).Value; }
            set
            {
                int index = this.FindIndex(x => x.Key.name.Equals(name));
                if (index != -1)
                    this[index] = new KeyValuePair<AnimationClip, AnimationClip>(this[index].Key, value);
            }
        }
    }
}

REFERENCE TO AnimationClipOverrides

HOW TO REPRODUCE: Just clone this repo and build.

ENVIRONMENT:

  • OS: Win 10
  • Unity Version: 2021.3.24f1
  • Netcode Version: 1.4.0

EuNextti avatar Jun 06 '23 17:06 EuNextti

Hi @EuNextti

NetworkAnimator doesn't synchronize that level of detail in regards to changes... it synchronizes changes to the Animator states only. Not having your project to test I can't validate whether the below modifications to your script will work, but this is along the lines of what you would need to do in order to synchronize the skin update and make sure that the update synchronizes properly with late joining players:

public class Player : NetworkBehaviour
{
    [SerializeField] private float speed = 3f;

    private Rigidbody2D rb;
    private Animator animator;

    [SerializeField] private CharacterBody characterBody;
    [SerializeField] private BodyPart[] parts;

    [SerializeField] private string[] partTypes;
    [SerializeField] private string[] directions;
    [SerializeField] private string[] states;

    private AnimatorOverrideController overrideController;
    private AnimationClipOverrides defaultClips;
    private AnimationClip animationClip;

    // Create a NetworkVariable in order to assure that the state of the skin selection is synchronized with late joining clients
    private NetworkVariable<int> m_PartIndex = new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner);

    private void Start()
    {
        rb = gameObject.GetComponent<Rigidbody2D>();
        animator = gameObject.GetComponent<Animator>();

        overrideController = new(animator.runtimeAnimatorController);
        animator.runtimeAnimatorController = overrideController;

        defaultClips = new(overrideController.overridesCount);
        overrideController.GetOverrides(defaultClips);
    }

    public override void OnNetworkSpawn()
    {
        if (!IsOwner)
        {
            // This assures that non-owner late joining clients will set the model's skin to the right index when
            // they first connect and synchronize with the network session state.
            UpdateParts();

            m_PartIndex.OnValueChanged += OnPartIndexChanged;
        }

        base.OnNetworkSpawn();
    }

    private void OnPartIndexChanged(int previousIndex, int newIndex)
    {
        UpdateParts();
    }

    private void Update()
    {
        if (!IsOwner)
        {
            return;
        }

        float inputX = Input.GetAxisRaw("Horizontal");
        float inputY = Input.GetAxisRaw("Vertical");

        Vector2 moveDirection = new Vector2(inputX, inputY).normalized;

        DoMovement(moveDirection, inputX, inputY);

        ChangeSkinColor();
    }

    private void DoMovement(Vector2 moveDirection, float inputX, float inputY)
    {
        ChangeAnimatorParams(moveDirection, inputX, inputY);
        rb.velocity = new Vector2(moveDirection.x * speed, moveDirection.y * speed);
    }

    private void ChangeSkinColor()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            // Set the new part skin index to use
            m_PartIndex.Value = 0;
            // Update to the new skin locally
            UpdateParts();
        }
        if (Input.GetKeyDown(KeyCode.E))
        {
            // Set the new part skin index to use
            m_PartIndex.Value = 1;
            // Update to the new skin locally
            UpdateParts();
        }
    }

    private void ChangeAnimatorParams(Vector2 moveDirection, float inputX, float inputY)
    {
        if (moveDirection.magnitude > 0)
        {
            animator.SetFloat("moveX", inputX);
            animator.SetFloat("moveY", inputY);
            animator.SetBool("isWalking", true);
        }
        else
        {
            animator.SetBool("isWalking", false);
        }
    }

    private void UpdateParts()
    {
        // Use the m_PartIndex value to set the body part
        characterBody.characterBodyParts[0].bodyPart = parts[m_PartIndex.Value];
        for (int x = 0; x < partTypes.Length; x++)
        {
            string partType = partTypes[x];
            string partID = characterBody.characterBodyParts[x].bodyPart.bodyAnimationId.ToString();

            for (int y = 0; y < states.Length; y++)
            {
                string state = states[y];

                for (int z = 0; z < directions.Length; z++)
                {
                    string direction = directions[z];

                    animationClip = Resources.Load<AnimationClip>("Player Animations/" + partType + "/" + partType + "_" + partID + "_" + state + "_" + direction);
                    defaultClips[partType + "_" + 0 + "_" + state + "_" + direction] = animationClip;
                }
            }
        }
        overrideController.ApplyOverrides(defaultClips);
    }

    public class AnimationClipOverrides : List<KeyValuePair<AnimationClip, AnimationClip>>
    {
        public AnimationClipOverrides(int capacity) : base(capacity) { }

        public AnimationClip this[string name]
        {
            get { return this.Find(x => x.Key.name.Equals(name)).Value; }
            set
            {
                int index = this.FindIndex(x => x.Key.name.Equals(name));
                if (index != -1)
                    this[index] = new KeyValuePair<AnimationClip, AnimationClip>(this[index].Key, value);
            }
        }
    }
}

All instances need to have the script in the Start method to be invoked so they can apply the changes locally.

Give that modified version a spin and let me know if it works for you?

Cheers!

👍

NoelStephensUnity avatar Jun 30 '23 01:06 NoelStephensUnity