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.
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.
I’ve been wrestling with this for days and I have been making progress, but not all the way there.
I’ve managed to:
dynamically assign the player as the animated target
dynamically expand the array of states
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).
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.
@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!
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.
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 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;
}
}
}