animancer icon indicating copy to clipboard operation
animancer copied to clipboard

Tips setting up animancer, FSM and Multiplayer

Open Todilo opened this issue 2 years ago • 6 comments

I was hoping someone could share some example setup of how to use animancer, their state machine with a multiplayer solutions.

I am mostly trying to wrap my head around when the state knows how to transition to other states and when the "brain/controller" knows how to do this. And then, to add a layer of complexity either the state change or the state result (animation state, positions etc) needs to be synchronized to clients.

For reference this is (non-multiplayer) states I am trying to build. Currently I have 3 variants of movementstate(standing, crouching, crawling) and a "brain" that sets a new destination.

using Animancer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace Assets._Scripts.States.MovementStates
{
    public class MovementState : CharacterState
    {

        [SerializeField] private ClipTransitionSequence  animationsFromStanding;
        [SerializeField] private ClipTransitionSequence  animationFromCrouching;
        [SerializeField] private ClipTransitionSequence animationsFromCrawling;

        [SerializeField] private LinearMixerTransitionAsset.UnShared idleToMoving;
        [SerializeField] private Character.Posture goalPosture;
        private bool hasFullyTransitioned = false;


        private Vector3? previousVelocity;

        private void OnEnable()
        {
            hasFullyTransitioned = false;
            var agent = Character.Brain.navMeshAgent;
            previousVelocity = agent.velocity;
            agent.isStopped = true;
            agent.velocity = Vector3.zero;

            // Break out
            agent.speed = Character.MaxSpeedFromPosture(goalPosture, false);

            if(Character.CurrentPosture == goalPosture)
            {
                Debug.LogError($"CurrentPosture {Character.CurrentPosture} is the same as goalPosture");
                OnAnimationEnd();
                return;
            }

            if (Character.CurrentPosture == Character.Posture.Standing)
            {

                var state = Character.Animancer.Play(animationsFromStanding);
                    state.Events.OnEnd = OnAnimationEnd;
            }
            else if (Character.CurrentPosture == Character.Posture.Crouching)
            {
                var state =Character.Animancer.Play(animationFromCrouching);
                state.Events.OnEnd = OnAnimationEnd;

            }
            else if(Character.CurrentPosture == Character.Posture.Crawling)
            {
                var state = Character.Animancer.Play(animationsFromCrawling);
                state.Events.OnEnd = OnAnimationEnd;
            }
        }


        private void Update()
        {
            if (Character.Brain.navMeshAgent.destination != default)
            {

                float speedPercentage = Character.Brain.navMeshAgent.velocity.magnitude / Character.MaxSpeedFromPosture(goalPosture, false);

                if (idleToMoving.State == null) return;
                idleToMoving.State.Parameter = speedPercentage;
            }
        }


        void OnAnimationEnd()
        {
            hasFullyTransitioned = true;

            Character.CurrentPosture = goalPosture;
            var state = Character.Animancer.Play(idleToMoving, 0.25f);
            idleToMoving.State.Parameter = 1f;

            var agent = Character.Brain.navMeshAgent;

            agent.velocity = previousVelocity ?? default;
            agent.isStopped = previousVelocity == default;
        }

        public override bool CanExitState => hasFullyTransitioned;

    }
}

And my brain which I am adding attack state to:

using Animancer.FSM;
using Assets._Scripts.Combat;
using UnityEngine;

namespace Assets._Scripts
{
    public class PlayerControllerBrain : CharacterBrain
    {

        public Camera mainCamera;

        [SerializeField] private CharacterState crouch;
        [SerializeField] private CharacterState crawl;
        [SerializeField] private CharacterState stand;
        [SerializeField] private Target currentTarget = null;
        [SerializeField] private CharacterState attack;
        private void Start()
        {

            mainCamera = Camera.main;
        }

        private bool InteractWithMovement()
        {
            RaycastHit hit;
            bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
            if (hasHit)
            {
                if (Input.GetMouseButton(0))
                {
                    navMeshAgent.destination = hit.point;
                    navMeshAgent.isStopped = false;
                }
                return true;
            }
            return false;
        }

        private void Update()
        {

            if (Input.GetMouseButtonDown(0))
            {
                RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());

                foreach (RaycastHit hit in hits)
                {
                    var newTarget = hit.transform.GetComponent<Target>();

                    currentTarget = newTarget;
                    return;
                }
            }

            InteractWithMovement();
            if (Input.GetKeyDown(KeyCode.X))
            {

                if (stand.TryEnterState())
                    Character.StateMachine.DefaultState = stand;
            }

            if (Input.GetKeyDown(KeyCode.C))
            {

                if (crouch.TryEnterState())
                    Character.StateMachine.DefaultState = crouch;
            }
            if (Input.GetKeyDown(KeyCode.V))
            {

                if (crawl.TryEnterState())
                    Character.StateMachine.DefaultState = crawl;
            }


            // Does the brain control this or does the attack state "stop" attacking?
            if (currentTarget == null) return;
            if(!GetIsInRange(currentTarget.transform))
            {
                navMeshAgent.destination = currentTarget.transform.position;
            } else
            {
                attack.TryEnterState();
            }
        }
        private bool GetIsInRange(Transform targetTransform)
        {
            return Vector3.Distance(transform.position, targetTransform.position) < 2f;
        }

        private static Ray GetMouseRay()
        {
            return Camera.main.ScreenPointToRay(Input.mousePosition);
        }
    }
}

Todilo avatar Apr 21 '22 18:04 Todilo

For anyone else reading this, it started with this post which has my reply after it.

KybernetikGames avatar Apr 22 '22 22:04 KybernetikGames

FWIW, I've been using the state machine with my networked game and it works perfectly. When the local player enters a state, it syncs the current state key over the network.

When a client receives this state key, I call ForceEnterState with the key. Basically I never use TryEnterState on remote clients since the assumption is that the condition has already been met on the client that set the state.

pushmatrix avatar Apr 28 '22 18:04 pushmatrix

Thank you @pushmatrix . I think however I went in the other direction. The clients know nothing about the state changes only the server. Then the server pushes any information (such as PlayAnimation, set position etc). Maybe it is because my state is like:

Attacking, Idle, Fleeing. Which doesn't sit well for the client to use since it updates values that it doesn't know everything about. However, it does make it harder to make sure animation times are the same and late-joining players currently don't know that it should be in a certain animation.

Todilo avatar Apr 29 '22 05:04 Todilo

Side question. Do you LinearMixerState parameters? I am trying to use navmesh velocity as parameter for a LinearMixerState but the navmesh is on the server. Synchronizing gives very choppy experience (as updateinvervals are too big ).

I write a Mathf.lerp interpolating between old and new value. It will give Synchronizationtime as "delay" since it interpolates rather than exterpolate , which is fine I guess but was a lot of manual work.

Todilo avatar May 04 '22 18:05 Todilo

Animancer v8.0 is now available and includes a new Animation Serialization sample which may help with this sort of thing.

KybernetikGames avatar Aug 16 '24 17:08 KybernetikGames