Search Unity

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

Discussion in 'Cinemachine' started by Adrenaline008, Sep 22, 2019.

  1. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    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!
     
  2. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,711
    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.
     
  3. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    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). Screen Shot 2019-09-27 at 6.22.16 PM.png


    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?
     
    Last edited: Sep 28, 2019
  4. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,711
    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:
    Code (CSharp):
    1.         class StateCollector
    2.         {
    3.             public List<int> mStates;
    4.             public List<string> mStateNames;
    5.             public Dictionary<int, int> mStateIndexLookup;
    6.             public Dictionary<int, int> mStateParentLookup;
    7.             public void CollectStates(AnimatorController ac, int layerIndex)
    8.             {
    9.                 mStates = new List<int>();
    10.                 mStateNames = new List<string>();
    11.                 mStateIndexLookup = new Dictionary<int, int>();
    12.                 mStateParentLookup = new Dictionary<int, int>();
    13.                 mStateIndexLookup[0] = mStates.Count;
    14.                 mStateNames.Add("(default)");
    15.                 mStates.Add(0);
    16.                 if (ac != null && layerIndex >= 0 && layerIndex < ac.layers.Length)
    17.                 {
    18.                     AnimatorStateMachine fsm = ac.layers[layerIndex].stateMachine;
    19.                     string name = fsm.name;
    20.                     int hash = Animator.StringToHash(name);
    21.                     CollectStatesFromFSM(fsm, name + ".", hash, string.Empty);
    22.                 }
    23.             }
    24.             void CollectStatesFromFSM(
    25.                 AnimatorStateMachine fsm, string hashPrefix, int parentHash, string displayPrefix)
    26.             {
    27.                 ChildAnimatorState[] states = fsm.states;
    28.                 for (int i = 0; i < states.Length; i++)
    29.                 {
    30.                     AnimatorState state = states[i].state;
    31.                     int hash = AddState(Animator.StringToHash(hashPrefix + state.name),
    32.                         parentHash, displayPrefix + state.name);
    33.                     // Also process clips as pseudo-states, if more than 1 is present.
    34.                     // Since they don't have hashes, we can manufacture some.
    35.                     var clips = CollectClips(state.motion);
    36.                     if (clips.Count > 1)
    37.                     {
    38.                         string substatePrefix = displayPrefix + state.name + ".";
    39.                         foreach (AnimationClip c in clips)
    40.                             AddState(
    41.                                 CinemachineStateDrivenCamera.CreateFakeHash(hash, c),
    42.                                 hash, substatePrefix + c.name);
    43.                     }
    44.                 }
    45.                 ChildAnimatorStateMachine[] fsmChildren = fsm.stateMachines;
    46.                 foreach (var child in fsmChildren)
    47.                 {
    48.                     string name = hashPrefix + child.stateMachine.name;
    49.                     string displayName = displayPrefix + child.stateMachine.name;
    50.                     int hash = AddState(Animator.StringToHash(name), parentHash, displayName);
    51.                     CollectStatesFromFSM(child.stateMachine, name + ".", hash, displayName + ".");
    52.                 }
    53.             }
    54.             List<AnimationClip> CollectClips(Motion motion)
    55.             {
    56.                 var clips = new List<AnimationClip>();
    57.                 AnimationClip clip = motion as AnimationClip;
    58.                 if (clip != null)
    59.                     clips.Add(clip);
    60.                 BlendTree tree = motion as BlendTree;
    61.                 if (tree != null)
    62.                 {
    63.                     ChildMotion[] children = tree.children;
    64.                     foreach (var child in children)
    65.                         clips.AddRange(CollectClips(child.motion));
    66.                 }
    67.                 return clips;
    68.             }
    69.             int AddState(int hash, int parentHash, string displayName)
    70.             {
    71.                 if (parentHash != 0)
    72.                     mStateParentLookup[hash] = parentHash;
    73.                 mStateIndexLookup[hash] = mStates.Count;
    74.                 mStateNames.Add(displayName);
    75.                 mStates.Add(hash);
    76.                 return hash;
    77.             }
    78.         }
    79.  
    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.
     
    Adrenaline008 likes this.
  5. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    @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.
     
    Gregoryl likes this.
  6. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    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):

    Code (CSharp):
    1. //Idling state
    2. stateCam.m_Instructions[0].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[0];
    3.         stateCam.m_Instructions[0].m_ActivateAfter = 3;
    4.         stateCam.m_Instructions[0].m_MinDuration = 0;
    5.  
    6.         //Running state
    7.         stateCam.m_Instructions[1].m_FullHash = GetAnimationClipHash("Running");
    8.         stateCam.m_Instructions[1].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[1];
    9.         stateCam.m_Instructions[1].m_ActivateAfter = 0;
    10.         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:

    Code (CSharp):
    1. var runtimeController = playerAnim.runtimeAnimatorController;
    2.         playerAnimController = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(UnityEditor.AssetDatabase.GetAssetPath(runtimeController));
    3.  
    4.         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.
     
  7. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,711
    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?
     
  8. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    @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
     
    Last edited: Oct 3, 2019
    Gregoryl likes this.
  9. Gregoryl

    Gregoryl

    Unity Technologies

    Joined:
    Dec 22, 2016
    Posts:
    7,711
    Glad to hear it's working! Would you like to share your script here? It sounds really interesting.
     
  10. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    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.
     
    Gregoryl likes this.
  11. Adrenaline008

    Adrenaline008

    Joined:
    Dec 21, 2018
    Posts:
    7
    @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.

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using Cinemachine;
    5. using static Cinemachine.CinemachineStateDrivenCamera;
    6. using UnityEditor.Animations;
    7.  
    8. public class CinemachineMaster : MonoBehaviour
    9. {
    10.     Player player;
    11.     CinemachineStateDrivenCamera stateCam;
    12.     Animator playerAnim;
    13.     AnimatorController playerAnimController;
    14.  
    15.     private void Awake()
    16.     {
    17.         player = FindObjectOfType<Player>();
    18.         stateCam = FindObjectOfType<CinemachineStateDrivenCamera>();
    19.         playerAnim = player.GetComponent<Animator>();
    20.  
    21.         //be sure to tag your scene's State-driven Camera in the Inspector to match this tag value
    22.         if (gameObject.tag == "State-driven Camera")
    23.         {
    24.             //set the State-driven Camera's Follow field to Player
    25.             stateCam.Follow = FindObjectOfType<Player>().transform;
    26.  
    27.             //set the State-driven Animator Target field to Player
    28.             stateCam.m_AnimatedTarget = playerAnim;
    29.  
    30.             //see this method below for all the juicy details
    31.             PopulateStates();
    32.         }
    33.         //be sure to tag all of your scene's Virtual Cameras in the Inspector to match this tag value
    34.         else if (gameObject.tag == "Virtual Camera")
    35.         {
    36.             //set all of the Virtual Cameras' Follow Override fields to Player
    37.             gameObject.GetComponent<Cinemachine.CinemachineVirtualCamera>().Follow = FindObjectOfType<Player>().transform;
    38.  
    39.             //set all of the Virtual Cameras' Confiner fields to Background's PolygonCollider2D
    40.             //be sure to tag your Background gameobject in the Inspector to match this tag value
    41.             gameObject.GetComponent<Cinemachine.CinemachineConfiner>().m_BoundingShape2D = GameObject.FindGameObjectWithTag("Background").GetComponent<PolygonCollider2D>();
    42.         }
    43.     }
    44.  
    45.     //this is where all the cool stuff happens!
    46.     void PopulateStates()
    47.     {
    48.         //get a reference to the player's runtimeAnimatorController, required for the StateCollector's functions
    49.         var runtimeController = playerAnim.runtimeAnimatorController;
    50.         playerAnimController = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(UnityEditor.AssetDatabase.GetAssetPath(runtimeController));
    51.  
    52.         //use StateCollector class (below) to gather all of the player's animation states
    53.         StateCollector collector = new StateCollector();
    54.         //I use a value of 0 for the layerIndex because my player's animator controller only has 1 layer: Base Layer
    55.         collector.CollectStates(playerAnimController, 0);
    56.  
    57.         //populate the array of "instructions" so we can set all of the State-driven Camera values here
    58.         //if you'd like to include an extra "default" state, do not subtract 1 from the m_Instructions array length
    59.         GameObject.FindGameObjectWithTag("State-driven Camera").GetComponent<CinemachineStateDrivenCamera>().m_Instructions = new Instruction[collector.mStates.Count - 1];
    60.  
    61.         //settings for the player's first state, "Idling"
    62.         //Idling
    63.         stateCam.m_Instructions[0].m_FullHash = collector.mStates[1];
    64.         stateCam.m_Instructions[0].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[0];
    65.         stateCam.m_Instructions[0].m_ActivateAfter = 3;
    66.         stateCam.m_Instructions[0].m_MinDuration = 0;
    67.  
    68.         //settings for the player's second state, "Running"
    69.         //Running
    70.         stateCam.m_Instructions[1].m_FullHash = collector.mStates[2];
    71.         stateCam.m_Instructions[1].m_VirtualCamera = gameObject.GetComponent<Cinemachine.CinemachineStateDrivenCamera>().ChildCameras[1];
    72.         stateCam.m_Instructions[1].m_ActivateAfter = 0;
    73.         stateCam.m_Instructions[1].m_MinDuration = 0;
    74.     }
    75.  
    76.     //this class was copied directly from the script: CinemachineStateDrivenCameraEditor.cs
    77.     class StateCollector
    78.     {
    79.         public List<int> mStates;
    80.         public List<string> mStateNames;
    81.         public Dictionary<int, int> mStateIndexLookup;
    82.         public Dictionary<int, int> mStateParentLookup;
    83.         public void CollectStates(AnimatorController ac, int layerIndex)
    84.         {
    85.             mStates = new List<int>();
    86.             mStateNames = new List<string>();
    87.             mStateIndexLookup = new Dictionary<int, int>();
    88.             mStateParentLookup = new Dictionary<int, int>();
    89.             mStateIndexLookup[0] = mStates.Count;
    90.             mStateNames.Add("(default)");
    91.             mStates.Add(0);
    92.             if (ac != null && layerIndex >= 0 && layerIndex < ac.layers.Length)
    93.             {
    94.                 AnimatorStateMachine fsm = ac.layers[layerIndex].stateMachine;
    95.                 string name = fsm.name;
    96.                 int hash = Animator.StringToHash(name);
    97.                 CollectStatesFromFSM(fsm, name + ".", hash, string.Empty);
    98.             }
    99.         }
    100.         void CollectStatesFromFSM(
    101.             AnimatorStateMachine fsm, string hashPrefix, int parentHash, string displayPrefix)
    102.         {
    103.             ChildAnimatorState[] states = fsm.states;
    104.             for (int i = 0; i < states.Length; i++)
    105.             {
    106.                 AnimatorState state = states[i].state;
    107.                 int hash = AddState(Animator.StringToHash(hashPrefix + state.name),
    108.                     parentHash, displayPrefix + state.name);
    109.                 // Also process clips as pseudo-states, if more than 1 is present.
    110.                 // Since they don't have hashes, we can manufacture some.
    111.                 var clips = CollectClips(state.motion);
    112.                 if (clips.Count > 1)
    113.                 {
    114.                     string substatePrefix = displayPrefix + state.name + ".";
    115.                     foreach (AnimationClip c in clips)
    116.                         AddState(
    117.                             CinemachineStateDrivenCamera.CreateFakeHash(hash, c),
    118.                             hash, substatePrefix + c.name);
    119.                 }
    120.             }
    121.             ChildAnimatorStateMachine[] fsmChildren = fsm.stateMachines;
    122.             foreach (var child in fsmChildren)
    123.             {
    124.                 string name = hashPrefix + child.stateMachine.name;
    125.                 string displayName = displayPrefix + child.stateMachine.name;
    126.                 int hash = AddState(Animator.StringToHash(name), parentHash, displayName);
    127.                 CollectStatesFromFSM(child.stateMachine, name + ".", hash, displayName + ".");
    128.             }
    129.         }
    130.         List<AnimationClip> CollectClips(Motion motion)
    131.         {
    132.             var clips = new List<AnimationClip>();
    133.             AnimationClip clip = motion as AnimationClip;
    134.             if (clip != null)
    135.                 clips.Add(clip);
    136.             BlendTree tree = motion as BlendTree;
    137.             if (tree != null)
    138.             {
    139.                 ChildMotion[] children = tree.children;
    140.                 foreach (var child in children)
    141.                     clips.AddRange(CollectClips(child.motion));
    142.             }
    143.             return clips;
    144.         }
    145.         int AddState(int hash, int parentHash, string displayName)
    146.         {
    147.             if (parentHash != 0)
    148.                 mStateParentLookup[hash] = parentHash;
    149.             mStateIndexLookup[hash] = mStates.Count;
    150.             mStateNames.Add(displayName);
    151.             mStates.Add(hash);
    152.             return hash;
    153.         }
    154.     }
    155. }
     
    Last edited: Oct 3, 2019
    Nintendo-Silence and Gregoryl like this.
  12. jimmygladfelter

    jimmygladfelter

    Joined:
    May 3, 2020
    Posts:
    83
    If you know the layer name, you can just simply do this:
    Code (CSharp):
    1. Instruction standardLocomotionState = new Instruction();
    2. standardLocomotionState.m_FullHash = Animator.StringToHash("Base Layer.Standard Locomotion");
    That would be the 'full hash' that it is looking for.
     
    Nintendo-Silence likes this.