Can you add and define a state on a state-driven camera via c#?

Can you add and define a state on a state-driven camera via c#?

I am trying to create a camera prefab that automatically locates the Player as the Animated Target (I’ve already done this part), and then populates the list of States from my Player’s states (this is the part I can’t figure out).

I want to be able to define the state, camera, wait and min fields via a script.

I have read through a bunch of documentation and searched many forums already but have not found what I am looking for.

Thank you in advance!

I think the best way is to look at the code for the StateDrivenCamera editor. It pretty much does that in order to build the UX that allows you to map vcams to animation states.

Thank you for the response @Gregoryl .

I’ve been wrestling with this for days and I have been making progress, but not all the way there.

I’ve managed to:

  1. dynamically assign the player as the animated target
  2. dynamically expand the array of states
  3. dynamically set the camera, wait and min values for each item in the array

But I have not figured out how to set the state value itself for each item in the array.

From what I’ve gathered by exploring the CinemachineStateDrivenCamera.cs file, it has something to do with “…m_Instructions*.m_FullHash*”. I’ve tried setting that to the hash value of my desired state, but it’s not producing desired results. Presumably because that is only an integer value—not actually tied to the the state of my player character.

I feel like I’m close to the solution, but I’ve struggle quite a bit on this.

Any pointers would be greatly appreciated!

PS: Attached is a screenshot of my state fields in the Inspector when the game is being played. I am trying to populate the areas that currently read as (default).

Edit:
I’m starting to think it has to do with the internal struct ParentHash. Is this true? If so, am I out of luck?

StateDrivenCamera maps animation states to vcams. An animation state is represented by a hash code (m_FullHash). If you put the right hash code in Instruction.m_FullHash, that should do the job.

Inside CinemachineStateDrivenCameraEditor.cs, you’ll find this bit of code, that extracts all the animation states and their hash codes:

        class StateCollector
        {
            public List<int> mStates;
            public List<string> mStateNames;
            public Dictionary<int, int> mStateIndexLookup;
            public Dictionary<int, int> mStateParentLookup;
            public void CollectStates(AnimatorController ac, int layerIndex)
            {
                mStates = new List<int>();
                mStateNames = new List<string>();
                mStateIndexLookup = new Dictionary<int, int>();
                mStateParentLookup = new Dictionary<int, int>();
                mStateIndexLookup[0] = mStates.Count;
                mStateNames.Add("(default)");
                mStates.Add(0);
                if (ac != null && layerIndex >= 0 && layerIndex < ac.layers.Length)
                {
                    AnimatorStateMachine fsm = ac.layers[layerIndex].stateMachine;
                    string name = fsm.name;
                    int hash = Animator.StringToHash(name);
                    CollectStatesFromFSM(fsm, name + ".", hash, string.Empty);
                }
            }
            void CollectStatesFromFSM(
                AnimatorStateMachine fsm, string hashPrefix, int parentHash, string displayPrefix)
            {
                ChildAnimatorState[] states = fsm.states;
                for (int i = 0; i < states.Length; i++)
                {
                    AnimatorState state = states[i].state;
                    int hash = AddState(Animator.StringToHash(hashPrefix + state.name),
                        parentHash, displayPrefix + state.name);
                    // Also process clips as pseudo-states, if more than 1 is present.
                    // Since they don't have hashes, we can manufacture some.
                    var clips = CollectClips(state.motion);
                    if (clips.Count > 1)
                    {
                        string substatePrefix = displayPrefix + state.name + ".";
                        foreach (AnimationClip c in clips)
                            AddState(
                                CinemachineStateDrivenCamera.CreateFakeHash(hash, c),
                                hash, substatePrefix + c.name);
                    }
                }
                ChildAnimatorStateMachine[] fsmChildren = fsm.stateMachines;
                foreach (var child in fsmChildren)
                {
                    string name = hashPrefix + child.stateMachine.name;
                    string displayName = displayPrefix + child.stateMachine.name;
                    int hash = AddState(Animator.StringToHash(name), parentHash, displayName);
                    CollectStatesFromFSM(child.stateMachine, name + ".", hash, displayName + ".");
                }
            }
            List<AnimationClip> CollectClips(Motion motion)
            {
                var clips = new List<AnimationClip>();
                AnimationClip clip = motion as AnimationClip;
                if (clip != null)
                    clips.Add(clip);
                BlendTree tree = motion as BlendTree;
                if (tree != null)
                {
                    ChildMotion[] children = tree.children;
                    foreach (var child in children)
                        clips.AddRange(CollectClips(child.motion));
                }
                return clips;
            }
            int AddState(int hash, int parentHash, string displayName)
            {
                if (parentHash != 0)
                    mStateParentLookup[hash] = parentHash;
                mStateIndexLookup[hash] = mStates.Count;
                mStateNames.Add(displayName);
                mStates.Add(hash);
                return hash;
            }
        }

You can duplicate that into your own code, or use something similar.
mStates contains the hash codes, mStateNames contains the corresponding state names.
Just add an Instruction to the SDC’s Instructions array, with the hash code and the vcam that you want to associate it to. You don’t need to worry about ParentHash.

1 Like

@Gregoryl wow thank you, this is exactly what I was looking for. I looked all over the CinemachineStateDrivenCamera.cs file, but not inside the Editor file!

I will try this tonight and get back to you.

1 Like

Sorry @Gregoryl , I still can’t figure it out!

This is what I’ve been doing to control the other fields (virtual camera, active after, and min duration):

//Idling state
stateCam.m_Instructions[0].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[0];
        stateCam.m_Instructions[0].m_ActivateAfter = 3;
        stateCam.m_Instructions[0].m_MinDuration = 0;

        //Running state
        stateCam.m_Instructions[1].m_FullHash = GetAnimationClipHash("Running");
        stateCam.m_Instructions[1].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[1];
        stateCam.m_Instructions[1].m_ActivateAfter = 0;
        stateCam.m_Instructions[1].m_MinDuration = 0;

I guess I don’t know how to use the class you pasted above. I tried to run CollectStates() first by doing this:

var runtimeController = playerAnim.runtimeAnimatorController;
        playerAnimController = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(UnityEditor.AssetDatabase.GetAssetPath(runtimeController));

        CollectStates(playerAnimController, 0);

I then tried to set stateCam.m_Instructions[0].m_FullHash, but it’s not doing anything.

I feel like you gave me everything I need to figure this out, but I’m struggling.

You should look at StateDrivenCameraEditor.cs to see how the class is used.
One question: why are you trying to do this with code? Why don’t you just set it up with the inspector?

@Gregoryl I figured it out! It’s working now, thank you for all of your help!

To answer your question, I wanted to do this so that I did not have to set up my cameras in every scene. I now have a script that populates the Follow, Animated Target and States fields (with your help) of the State-driven Camera, as well as the Cinemachine Confiner’s Bounding 2D Shape of my Virtual Cameras.

This means that all I need to do is have my Player, my Camera prefab (without any of those fields pre-defined), and my bounding box object in each scene. The script takes care of setting everything!

To be completely honest, I know I could have proceeded without working through this challenge. But once I got pretty close, I became obsessed with seeing it through haha.

Thanks again!

Edit: typo

1 Like

Glad to hear it’s working! Would you like to share your script here? It sounds really interesting.

Yes! I will. It’s very messy right now because it has remnants of a few things I was trying. I’m going to clean it up over the next day or so and then share it. I don’t often have time to work during the week.

1 Like

@Gregoryl here it is! I made it as clean as my abilities allow, with clear comments throughout. Let me know what you think.

Edit: I forgot to mention, this script goes on the SDC as well as each Virtual Camera. The only other setup required is applying a few tags to objects in the scene, which I clearly state in comments below.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
using static Cinemachine.CinemachineStateDrivenCamera;
using UnityEditor.Animations;

public class CinemachineMaster : MonoBehaviour
{
    Player player;
    CinemachineStateDrivenCamera stateCam;
    Animator playerAnim;
    AnimatorController playerAnimController;

    private void Awake()
    {
        player = FindObjectOfType<Player>();
        stateCam = FindObjectOfType<CinemachineStateDrivenCamera>();
        playerAnim = player.GetComponent<Animator>();

        //be sure to tag your scene's State-driven Camera in the Inspector to match this tag value
        if (gameObject.tag == "State-driven Camera")
        {
            //set the State-driven Camera's Follow field to Player
            stateCam.Follow = FindObjectOfType<Player>().transform;

            //set the State-driven Animator Target field to Player
            stateCam.m_AnimatedTarget = playerAnim;

            //see this method below for all the juicy details
            PopulateStates();
        }
        //be sure to tag all of your scene's Virtual Cameras in the Inspector to match this tag value
        else if (gameObject.tag == "Virtual Camera")
        {
            //set all of the Virtual Cameras' Follow Override fields to Player
            gameObject.GetComponent<Cinemachine.CinemachineVirtualCamera>().Follow = FindObjectOfType<Player>().transform;

            //set all of the Virtual Cameras' Confiner fields to Background's PolygonCollider2D
            //be sure to tag your Background gameobject in the Inspector to match this tag value
            gameObject.GetComponent<Cinemachine.CinemachineConfiner>().m_BoundingShape2D = GameObject.FindGameObjectWithTag("Background").GetComponent<PolygonCollider2D>();
        }
    }

    //this is where all the cool stuff happens!
    void PopulateStates()
    {
        //get a reference to the player's runtimeAnimatorController, required for the StateCollector's functions
        var runtimeController = playerAnim.runtimeAnimatorController;
        playerAnimController = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(UnityEditor.AssetDatabase.GetAssetPath(runtimeController));

        //use StateCollector class (below) to gather all of the player's animation states
        StateCollector collector = new StateCollector();
        //I use a value of 0 for the layerIndex because my player's animator controller only has 1 layer: Base Layer
        collector.CollectStates(playerAnimController, 0);

        //populate the array of "instructions" so we can set all of the State-driven Camera values here
        //if you'd like to include an extra "default" state, do not subtract 1 from the m_Instructions array length
        GameObject.FindGameObjectWithTag("State-driven Camera").GetComponent<CinemachineStateDrivenCamera>().m_Instructions = new Instruction[collector.mStates.Count - 1];

        //settings for the player's first state, "Idling"
        //Idling
        stateCam.m_Instructions[0].m_FullHash = collector.mStates[1];
        stateCam.m_Instructions[0].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[0];
        stateCam.m_Instructions[0].m_ActivateAfter = 3;
        stateCam.m_Instructions[0].m_MinDuration = 0;

        //settings for the player's second state, "Running"
        //Running
        stateCam.m_Instructions[1].m_FullHash = collector.mStates[2];
        stateCam.m_Instructions[1].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[1];
        stateCam.m_Instructions[1].m_ActivateAfter = 0;
        stateCam.m_Instructions[1].m_MinDuration = 0;
    }

    //this class was copied directly from the script: CinemachineStateDrivenCameraEditor.cs
    class StateCollector
    {
        public List<int> mStates;
        public List<string> mStateNames;
        public Dictionary<int, int> mStateIndexLookup;
        public Dictionary<int, int> mStateParentLookup;
        public void CollectStates(AnimatorController ac, int layerIndex)
        {
            mStates = new List<int>();
            mStateNames = new List<string>();
            mStateIndexLookup = new Dictionary<int, int>();
            mStateParentLookup = new Dictionary<int, int>();
            mStateIndexLookup[0] = mStates.Count;
            mStateNames.Add("(default)");
            mStates.Add(0);
            if (ac != null && layerIndex >= 0 && layerIndex < ac.layers.Length)
            {
                AnimatorStateMachine fsm = ac.layers[layerIndex].stateMachine;
                string name = fsm.name;
                int hash = Animator.StringToHash(name);
                CollectStatesFromFSM(fsm, name + ".", hash, string.Empty);
            }
        }
        void CollectStatesFromFSM(
            AnimatorStateMachine fsm, string hashPrefix, int parentHash, string displayPrefix)
        {
            ChildAnimatorState[] states = fsm.states;
            for (int i = 0; i < states.Length; i++)
            {
                AnimatorState state = states[i].state;
                int hash = AddState(Animator.StringToHash(hashPrefix + state.name),
                    parentHash, displayPrefix + state.name);
                // Also process clips as pseudo-states, if more than 1 is present.
                // Since they don't have hashes, we can manufacture some.
                var clips = CollectClips(state.motion);
                if (clips.Count > 1)
                {
                    string substatePrefix = displayPrefix + state.name + ".";
                    foreach (AnimationClip c in clips)
                        AddState(
                            CinemachineStateDrivenCamera.CreateFakeHash(hash, c),
                            hash, substatePrefix + c.name);
                }
            }
            ChildAnimatorStateMachine[] fsmChildren = fsm.stateMachines;
            foreach (var child in fsmChildren)
            {
                string name = hashPrefix + child.stateMachine.name;
                string displayName = displayPrefix + child.stateMachine.name;
                int hash = AddState(Animator.StringToHash(name), parentHash, displayName);
                CollectStatesFromFSM(child.stateMachine, name + ".", hash, displayName + ".");
            }
        }
        List<AnimationClip> CollectClips(Motion motion)
        {
            var clips = new List<AnimationClip>();
            AnimationClip clip = motion as AnimationClip;
            if (clip != null)
                clips.Add(clip);
            BlendTree tree = motion as BlendTree;
            if (tree != null)
            {
                ChildMotion[] children = tree.children;
                foreach (var child in children)
                    clips.AddRange(CollectClips(child.motion));
            }
            return clips;
        }
        int AddState(int hash, int parentHash, string displayName)
        {
            if (parentHash != 0)
                mStateParentLookup[hash] = parentHash;
            mStateIndexLookup[hash] = mStates.Count;
            mStateNames.Add(displayName);
            mStates.Add(hash);
            return hash;
        }
    }
}
2 Likes

If you know the layer name, you can just simply do this:

Instruction standardLocomotionState = new Instruction();
standardLocomotionState.m_FullHash = Animator.StringToHash("Base Layer.Standard Locomotion");

That would be the ‘full hash’ that it is looking for.

1 Like