前言
不好意思,拖更幾個月了,最近屁股先鋒中毒太深。今天來說說如何在Unity的編輯模式下自動生成動畫狀態機。
為何需要自動生成動畫狀態機?
游戲角色的動畫狀態中有許多相似的狀態,每個角色都需要創建動畫狀態機,創建諸如Idle,Run,Walk,NormalAttak等相似的狀態,甚至狀態機的遷移條件也有雷同。為了不然創建這些相似的狀態機并指定狀態中相對應的動畫這類繁瑣的事全部交給手工操作,自動化生成這些狀態機成為理所當然。
如何自動化生成動畫狀態機?
官方參考
官方文檔中有關于這個的一份參考代碼。你可以從中大致知道這一流程,推薦大家看一看。本文將在此基礎上完整演示從美術資源到生成狀態機乃至角色prefab的整個過程。包含指定動畫融合,狀態機分層等知識點。
實例演示
本文提供的實例演示的所有代碼及資源將提供在github庫中的AnimatorFactory這個項目中。歡迎大家fork。
準備工作
為了模擬從美術資源到產出狀態機的全過程,我們需要準備我們的資源。官方提供的Standard Assets正好可以拿來使用。下載Standard Assets可以在官方網站的如下位置找到:
找到Unity舊版本:
找到對應版本的標準資源
我們創建新的項目,并導入資源:
我們僅保留動畫和模型資源和一些文件結構:
自動化流程
我們在Editor目錄下創建一個名為AnimatorFactory的CSharp文件,我們的自動化流程將一步一步通過完善這個腳本實現。至于為何要在Editor目錄創建,可以參考我的這篇文章:Unity3d開發中的特殊文件夾。
1 準備加載動畫的工具類
在AnimatorFactory腳本中添加如下代碼:
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Animations;
public static class AnimatorFactoryUtil
{
public static AnimationClip LoadAnimClip(string path)
{
return (AnimationClip)AssetDatabase.LoadAssetAtPath(path, typeof(AnimationClip));
}
public static AnimationClip LoadAnimClip(string fbxPath, string animPath)
{
var objs = AssetDatabase.LoadAllAssetsAtPath(fbxPath);
return objs.Where(o => o is AnimationClip && o.name.Equals(animPath)).Select(o => o as AnimationClip).FirstOrDefault();
}
}
我們的動畫資源不在Resources文件夾下,這里使用AssetDatabase的LoadAssetAtPath方法加載動畫。多參數版的函數演示了在同一個fbx文件下加載指定動畫的方法。
2 創建窗口
在AnimatorFactory腳本中添加如下代碼:
public class AnimatorFactory : EditorWindow
{
#region const
private const string AnimatorSavePath = "Assets/Standard Assets/Characters/ThirdPersonCharacter/Animator/";
private const string PrefabSavePath = "Assets/Standard Assets/Characters/ThirdPersonCharacter/Prefabs/";
private const string ModelPath = "Assets/Standard Assets/Characters/ThirdPersonCharacter/Models/";
private const string AnimationPath = "Assets/Standard Assets/Characters/ThirdPersonCharacter/Animation/Humanoid";
private const string AnimatorControllerSuffix = "AnimatorController.controller";
#endregion const
#region private members
private string characterName;
private AnimatorController productController;
private AnimatorStateMachine baseLayerMachine;
private AnimatorStateMachine crouchLayerMachine;
// base layer states
private AnimatorState stateIdle;
private AnimatorState stateMove;
private AnimatorState stateJump;
private AnimatorState stateDeath;
// crouch layer states
private AnimatorState stateWalk;
private AnimatorState stateWalkLeft;
private AnimatorState stateWalkRight;
#endregion private members
#region EditorWindow
[MenuItem("Window/AnimatorFactory")]
public static void OpenWindow()
{
EditorWindow.GetWindow(typeof(AnimatorFactory));
}
void OnEnable()
{
// set default name
characterName = "Ethan";
}
void OnGUI()
{
GUILayout.Label("Animation Settings", EditorStyles.boldLabel);
characterName = EditorGUILayout.TextField("Character Name: ", characterName);
if (GUILayout.Button("Generate Controller"))
GenerateController();
}
#endregion EditorWindow
這樣在Window菜單欄下就會有一個AnimatorFactory的菜單項,點擊會打開這樣一個窗口:
EditorWindow可以用來擴展我們的Unity,開發一些我們自己需要的工具,更多相關信息可以參考Unity的官方文檔:
https://docs.unity3d.com/Manual/ExtendingTheEditor.html
定制編輯器的話題,有機會本站也會繼續和大家交流。
Character Name 參數用于指定我們想要生成的角色的名字,這里是Standard Asset中的Ethan,所以我們在OnEnable函數中指定默認的角色名。
Generate Controller 按鈕用于執行GenerateController函數,生成我們的動畫狀態機。
3 創建AnimatorController
public void GenerateController()
{
if (string.IsNullOrEmpty(characterName))
return;
// create animator controller
CreateController();
// add controller parameters
AddParameters();
// Create Anim States
CreateAnimStates();
// Bind aniamtor controller to prefab
BindControllerToPrefab();
}
這里分四步逐漸創建我們的狀態機并創建人物Prefab。先來看第一步:
/// <summary>
/// Show how to create a controller, and set layer parameters
/// </summary>
private void CreateController()
{
productController = AnimatorController.CreateAnimatorControllerAtPath(AnimatorSavePath + characterName + AnimatorControllerSuffix);
// get base machine in base layer
baseLayerMachine = productController.layers[0].stateMachine;
// set base machine parameters
baseLayerMachine.entryPosition = Vector3.zero;
baseLayerMachine.exitPosition = new Vector3(400f, 200f);
baseLayerMachine.anyStatePosition = new Vector3(0f, 200f);
// add crouch layer to controller
productController.AddLayer("CrouchLayer");
// get a copy from controller's layer
AnimatorControllerLayer[] layers = productController.layers;
// set layer parameters
layers[1].defaultWeight = 1f;
layers[1].blendingMode = AnimatorLayerBlendingMode.Override;
// save layer setting to controller
productController.layers = layers;
// get state machine in crouch layer
crouchLayerMachine = productController.layers[1].stateMachine;
// set crouch machine parameters
crouchLayerMachine.entryPosition = Vector3.zero;
crouchLayerMachine.exitPosition = new Vector3(600f, 200f);
crouchLayerMachine.anyStatePosition = new Vector3(0f, 200f);
}
我特意添加了一個CrouchLayer層級,并演示設置其參數的方法。這里需要注意的是productController.layers這個屬性返回的是一個拷貝而不是引用,所以直接改變層上的defaultWeight等參數不會生效,需要將設置好參數后的層級信息賦回layers。
3 指定參數
/// <summary>
/// Show how to add parameters to the controller
/// </summary>
private void AddParameters()
{
// use AddParameter interface
productController.AddParameter("FloatA", AnimatorControllerParameterType.Float);
productController.AddParameter("FloatB", AnimatorControllerParameterType.Float);
productController.AddParameter("TriggerA", AnimatorControllerParameterType.Trigger);
productController.AddParameter("TriggerB", AnimatorControllerParameterType.Trigger);
productController.AddParameter("TriggerC", AnimatorControllerParameterType.Trigger);
productController.AddParameter("BooleanA", AnimatorControllerParameterType.Bool);
// if you want to set default value
AnimatorControllerParameter playSpeed = new AnimatorControllerParameter();
playSpeed.name = "PlaySpeed";
playSpeed.type = AnimatorControllerParameterType.Float;
playSpeed.defaultFloat = 1.0f;
productController.AddParameter(playSpeed);
}
我們知道動畫狀態機的遷移條件可能需要用到參數,這里展示了添加動畫參數的兩種方式,一種指定名字和類型就可以,另一種可以設置默認值。
4 創建動畫狀態并綁定動畫
private void CreateAnimStates()
{
// Create base layer states
CreateBaseLayerState();
// Create crouch layer states
CreateCrouchLayerState();
}
private void CreateBaseLayerState()
{
CreateIdle();
CreateMove();
CreateJump();
CreateDeath();
SetBaseLayerTransition();
}
private void CreateCrouchLayerState()
{
// Load Animation
string fbxPath = AnimationPath + "Crouch.FBX";
CreateCrouchIdle(fbxPath);
CreateCrouchWalk(fbxPath);
}
由于想要全面展示創建各個層級的狀態機,子狀態機,以及普通狀態,融合樹狀態等各個知識點,這部分內容會比較繁瑣,請耐心往下看:
/// <summary>
/// Show how to add a basic state, And add a behaviour
/// </summary>
private void CreateIdle()
{
// Load Animation
AnimationClip idleClip = AnimatorFactoryUtil.LoadAnimClip(AnimationPath + "Idle.FBX");
// add tree state & set state motion
stateIdle = baseLayerMachine.AddState("Idle", new Vector3(300f, 0f));
stateIdle.motion = idleClip;
// Add behaviour to state
stateIdle.AddStateMachineBehaviour<CharacterIdleState>();
// set to default state
baseLayerMachine.defaultState = stateIdle;
}
CreateIdle 展示了如何創建一個普通的動畫狀態并指定動畫以及給該狀態添加一個Behavior的過程(CharacterIdleState腳本中是一個繼承自StateMachineBehaviour的空實現類)。加載動畫用到了前面準備好的AnimatorFactoryUtil類。
/// <summary>
/// Show how to add a 1D tree
/// </summary>
private void CreateMove()
{
// Load Animation
AnimationClip walkClip = AnimatorFactoryUtil.LoadAnimClip(AnimationPath + "Walk.FBX");
AnimationClip runClip = AnimatorFactoryUtil.LoadAnimClip(AnimationPath + "Run.FBX");
// new a tree
BlendTree tree = new BlendTree();
// Set blendtree parameters
tree.name = "Move";
tree.blendType = BlendTreeType.Simple1D;
tree.useAutomaticThresholds = true;
tree.minThreshold = 0f;
tree.maxThreshold = 1f;
tree.blendParameter = "FloatA";
// Add clip to BlendTree
tree.AddChild(walkClip, 0f);
tree.AddChild(runClip, 1f);
// Add tree to controller asset
if (AssetDatabase.GetAssetPath(productController) != string.Empty)
{
AssetDatabase.AddObjectToAsset(tree, AssetDatabase.GetAssetPath(productController));
}
// add tree state & set state motion
stateMove = baseLayerMachine.AddState(tree.name, new Vector3(600f, 0f));
stateMove.motion = tree;
}
CreateMove 展示了如何創建一維融合樹。這里之所以使用AddObjectToAsset接口是參考了Unity源碼中AnimatorController類中的接口CreateBlendTreeInController。如果直接使用后文將提及的SetStateEffectiveMotion接口或直接指定stateMove的motion屬性,生成controller時一切正常,運行后融合樹就失效了。猜測原因,應該是CreateBlendTreeInController里由于做了AddObjectToAsset操作保留了融合樹,而使用SetStateEffectiveMotion接口或直接指定stateMove的motion屬性沒有這個操作。那我為什么一定要固執的使用這個方法,而不直接CreateBlendTreeInController呢?你們猜?(因為這個接口無法指定該狀態在animator窗口中的位置)。
/// <summary>
/// Show how to add 2D tree
/// </summary>
private void CreateJump()
{
// Load Animation
string fbxPath = AnimationPath + "IdleJumpUp.FBX";
AnimationClip fallClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidFall");
AnimationClip idleJumpUpClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidIdleJumpUp");
AnimationClip jumpUpClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidJumpUp");
AnimationClip midAirClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidMidAir");
// create a tree
BlendTree tree = new BlendTree();
// Set blendtree parameters
tree.name = "Jump";
tree.blendType = BlendTreeType.FreeformDirectional2D;
tree.useAutomaticThresholds = true;
tree.minThreshold = 0f;
tree.maxThreshold = 1f;
tree.blendParameter = "FloatA";
tree.blendParameterY = "FloatB";
// Add clip to BlendTree
tree.AddChild(fallClip, new Vector2(0f, 0f));
tree.AddChild(idleJumpUpClip, new Vector2(0f, 1f));
tree.AddChild(jumpUpClip, new Vector2(1f, 0f));
tree.AddChild(midAirClip, new Vector2(-1f, 0f));
// Add tree to controller asset
if (AssetDatabase.GetAssetPath(productController) != string.Empty)
{
AssetDatabase.AddObjectToAsset(tree, AssetDatabase.GetAssetPath(productController));
}
// add tree state & set state motion
stateJump = baseLayerMachine.AddState(tree.name, new Vector3(300f, -100f));
stateJump.motion = tree;
}
CreateJump 展示了如何創建一棵二維融合樹。
/// <summary>
/// Show how to relate exitState and anyState
/// </summary>
private void CreateDeath()
{
AnimationClip deathClip = AnimatorFactoryUtil.LoadAnimClip(AnimationPath + "WalkTurn.FBX");
stateDeath = baseLayerMachine.AddState("Death", new Vector3(200f, 100f));
productController.SetStateEffectiveMotion(stateDeath, deathClip);
// death to exit
var exitTransition = stateDeath.AddExitTransition();
exitTransition.AddCondition(AnimatorConditionMode.If, 0, "TriggerA");
exitTransition.duration = 0;
// anyState to death
var anyTransition = baseLayerMachine.AddAnyStateTransition(stateDeath);
anyTransition.AddCondition(AnimatorConditionMode.If, 0, "TriggerB");
anyTransition.duration = 0;
}
CreateDeath 展示了如何指定一個狀態機的AnyState的遷移,以及Exit遷移。
/// <summary>
/// Show how to add transitions
/// </summary>
private void SetBaseLayerTransition()
{
var trans = stateIdle.AddTransition(stateMove);
trans.hasExitTime = true;
trans.exitTime = 0.9f;
trans.interruptionSource = TransitionInterruptionSource.Source;
trans.duration = 0;
trans = stateMove.AddTransition(stateIdle);
trans.interruptionSource = TransitionInterruptionSource.Destination;
trans.duration = 0;
trans.AddCondition(AnimatorConditionMode.If, 0, "TriggerA");
}
SetBaseLayerTransition 展示了如何添加狀態遷移,設置exitTime,設置打斷源,設置條件等。到這里,第一層動畫狀態機的演示到此結束。接下來我們開始創建CrouchLayer層的狀態機。
/// <summary>
/// Show how to set speed parameter, And set state motion in other way
/// </summary>
/// <param name="fbxPath"></param>
private void CreateCrouchIdle(string fbxPath)
{
// Load Animation
AnimationClip idleClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidCrouchIdle");
stateIdle = crouchLayerMachine.AddState("Idle", new Vector3(300f, 0f));
stateIdle.speedParameterActive = true;
stateIdle.speedParameter = "FloatA";
// Set state motion,the other way
productController.SetStateEffectiveMotion(stateIdle, idleClip);
// set to default state
crouchLayerMachine.defaultState = stateIdle;
}
CreateCrouchIdle 展示了如何設置動畫播放速度參數,以及另一種指定動畫狀態的動畫的方法,即使用SetStateEffectiveMotion。
/// <summary>
/// Show how to add a child machine
/// </summary>
/// <param name="fbxPath"></param>
private void CreateCrouchWalk(string fbxPath)
{
AnimatorStateMachine walkMachine = crouchLayerMachine.AddStateMachine("Walk", new Vector3(0f, 100f));
walkMachine.entryPosition = Vector3.zero;
walkMachine.anyStatePosition = new Vector3(0f, -200f);
walkMachine.exitPosition = new Vector3(0f, -400f);
AnimationClip walkClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidCrouchWalk");
AnimationClip walkLeftClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidCrouchWalkLeft");
AnimationClip walkRightClip = AnimatorFactoryUtil.LoadAnimClip(fbxPath, "HumanoidCrouchWalkRight");
stateWalk = walkMachine.AddState("Walk", new Vector3(200f, 0f));
stateWalk.motion = walkClip;
stateWalkLeft = walkMachine.AddState("WalkLeft", new Vector3(200f, -200f));
stateWalkLeft.motion = walkLeftClip;
stateWalkRight = walkMachine.AddState("WalkRight", new Vector3(200f, -400f));
stateWalkRight.motion = walkRightClip;
}
CreateCrouchWalk 展示了如何添加一個子狀態機。到這里創建狀態機的各個常用知識點基本都涉及了。
5 綁定狀態機到Prefab
private void BindControllerToPrefab()
{
// Generate prefab first time
var prefab = AssetDatabase.LoadAssetAtPath(ModelPath + characterName + ".fbx", typeof(GameObject)) as GameObject;
var go = Instantiate(prefab);
go.name = characterName + "Prefab";
var animator = go.GetComponent<Animator>() ?? go.AddComponent<Animator>();
animator.runtimeAnimatorController = productController;
PrefabUtility.CreatePrefab(PrefabSavePath + go.name + ".prefab", go);
DestroyImmediate(go);
// If there has been a prefab, just bind controller to it.
//var prefab = AssetDatabase.LoadAssetAtPath(PrefabSavePath + characterName, typeof(GameObject)) as GameObject;
//var go = Instantiate(prefab) as GameObject;
//var animator = go.GetComponent<Animator>() ?? go.AddComponent<Animator>();
//animator.runtimeAnimatorController = productController;
//PrefabUtility.ReplacePrefab(go, prefab);
//DestroyImmediate(go);
}
我們可以在指定的位置生成角色的Prefab,啟用的代碼展示第一次創建Prefab的情況。注釋的代碼展示綁定我們生成的AnimatorController到已存在的指定的Prefab上。來看看我們的成果:
后記
寫了好長,希望對你有幫助,提升創建Prefab的效率。本文的所有演示文件都在我的Github上。對了,也請大家多多支持我的博客,我的文章將會第一時間發表在上面,下次再見。