Unity-耦合動畫和導(dǎo)航
本文檔的目標(biāo)是指導(dǎo)您設(shè)置人形角色的導(dǎo)航以使用導(dǎo)航系統(tǒng)進行移動。
我們將使用 Unity 的內(nèi)置動畫和導(dǎo)航系統(tǒng)以及自定義腳本來實現(xiàn)這一目標(biāo)。
本文假設(shè)您已熟悉 Unity 和 Mecanim 動畫系統(tǒng)的基礎(chǔ)知識。
此處提供了一個示例項目,因此無需從頭開始添加腳本或設(shè)置動畫和動畫控制器:
NavigationAnimation_53.zip?適用于 Unity 5.3+
創(chuàng)建動畫控制器
為了獲得響應(yīng)迅速且多功能的動畫控制器(涵蓋各種動作),我們需要一組向不同方向移動的動畫。有時將其稱為掃射集 (strafe-set)。
除了移動動畫,我們還需要一段站立角色的動畫。
我們繼續(xù)將掃射集組織在 2D 混合樹中;選擇混合類型:2D Simple Directional,并使用?Compute Positions > Velocity XZ?放置動畫
為進行混合控制,我們添加兩個浮點參數(shù)?velx?和?vely,并將它們分配給混合樹。
在這里,我們將放置 7 段奔跑動畫,每段都有不同的速度。除了前進(+左/右)和后退(+左/右),我們還使用了原地奔跑的動畫剪輯。后者在下面的 2D 混合圖的中心位置進行了突出顯示。采用原地奔跑動畫有兩個原因,首先,該動畫可在與其他動畫混合時保持奔跑風(fēng)格;其次,該動畫可以防止混合時出現(xiàn)腳滑。

然后,我們在空閑節(jié)點 (Idle) 本身中添加空閑動畫剪輯。 我們現(xiàn)在有兩個獨立動畫狀態(tài),我們將它們與 2 個過渡耦合。

為了控制移動狀態(tài)和空閑狀態(tài)之間的切換,我們添加一個布爾值控制參數(shù)?move。然后,對過渡禁用?Has Exit Time?屬性。如此便可在動畫期間的任何時間觸發(fā)過渡。為獲得快速響應(yīng)的過渡,過渡時間應(yīng)設(shè)置為約 0.10 秒。

現(xiàn)在將新創(chuàng)建的動畫控制器放在要移動的角色上。
按 Play 并在?Hierarchy 窗口中選擇該角色。現(xiàn)在可在?Animator 窗口中手動控制動畫值,并更改移動狀態(tài)和速度。
下一步是創(chuàng)建其他控制動畫參數(shù)的方法。
導(dǎo)航控制
在角色上放置一個?NavMeshAgent?組件,調(diào)整半徑和高度,并匹配角色(另外更改速度屬性以匹配動畫混合樹中的最大速度)。
為放入角色的場景創(chuàng)建導(dǎo)航網(wǎng)格。
接下來,我們需要告訴角色要導(dǎo)航的目標(biāo)。此設(shè)置通常與具體應(yīng)用有非常大的關(guān)聯(lián)性。在這里,我們選擇“單擊進行移動”(click to move) 行為:根據(jù)用戶點擊屏幕的位置,角色移動到世界中的相應(yīng)點。
// ClickToMove.cs?
using UnityEngine;?
using UnityEngine.AI;?
[RequireComponent (typeof (NavMeshAgent))]?
public class ClickToMove : MonoBehaviour {
? ?
RaycastHit hitInfo = new RaycastHit();
? ?
NavMeshAgent agent;
? ?
void Start (){
? ? ? ?
agent = GetComponent<NavMeshAgent> ();
? ?
}
? ?
void Update (){
? ? ? ?
if(Input.GetMouseButtonDown(0)) {
? ? ? ? ? ?
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
? ? ? ? ? ?
if (Physics.Raycast(ray.origin, ray.direction, out hitInfo))
? ? ? ? ? ? ? ?agent.destination = hitInfo.point;
? ? ? ?
}}}
現(xiàn)在按下 Play,然后在場景中單擊,便會看到角色在場景中移動。但是,動畫與動作完全不符。我們需要將代理的狀態(tài)和速度傳達給動畫控制器。
為了將代理的速度和狀態(tài)信息傳輸?shù)絼赢嬁刂破鳎覀儗⑻砑恿硪粋€腳本。
// LocomotionSimpleAgent.cs?
using UnityEngine;?
using UnityEngine.AI;?
?[RequireComponent (typeof (NavMeshAgent))]?
[RequireComponent (typeof (Animator))]?
public class LocomotionSimpleAgent : MonoBehaviour {
Animator anim;
NavMeshAgent agent;
Vector2 smoothDeltaPosition = Vector2.zero;
Vector2 velocity = Vector2.zero;
void Start (){
anim = GetComponent<Animator> ();
agent = GetComponent<NavMeshAgent> ();
? ? ? ?
// 不要自動更新位置
? ? ? ?
agent.updatePosition = false;
}
void Update (){
? ? ? ?
Vector3 worldDeltaPosition = agent.nextPosition - transform.position;
? ? ? // 將"worldDeltaPosition"映射到局部空間
float dx = Vector3.Dot (transform.right, worldDeltaPosition);
float dy = Vector3.Dot (transform.forward, worldDeltaPosition);
Vector2 deltaPosition = new Vector2 (dx, dy);
// 對 deltaMove 進行低通濾波
float smooth = Mathf.Min(1.0f, Time.deltaTime/0.15f);
smoothDeltaPosition = Vector2.Lerp (smoothDeltaPosition, deltaPosition, smooth);
// 如果時間推進,則更新速度
if (Time.deltaTime > 1e-5f)
????velocity = smoothDeltaPosition / Time.deltaTime;
????bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius;
// 更新動畫參數(shù)
anim.SetBool("move", shouldMove);
anim.SetFloat ("velx", velocity.x);
? ? ? ?
anim.SetFloat ("vely", velocity.y);
? ? ? ?
GetComponent<LookAt>().lookAtTargetPosition = agent.steeringTarget + transform.forward;}
? ?
void OnAnimatorMove (){
// 將位置更新到代理位置
transform.position = agent.nextPosition;
? ?
}
}
對于此腳本,需要進行一點說明。此腳本放置在角色上,而角色已附加?Animator?和?NavMeshAgent?組件以及上面的 click to move 腳本。
首先,腳本告訴代理不要自動更新角色位置。我們處理腳本中最后的位置更新。方向由代理進行更新。
通過讀取代理速度來控制動畫混合。該速度轉(zhuǎn)換為相對速度(基于角色方向),然后經(jīng)過平滑。然后,轉(zhuǎn)換后的水平速度分量將傳遞到?Animator,另外,空閑狀態(tài)和移動狀態(tài)之間的狀態(tài)切換由速度(即速度幅度)進行控制。
在?OnAnimatorMove()
?回調(diào)中,我們更新角色的位置以便與?NavMeshAgent?匹配。
再次播放場景顯示動畫在最大限度上與動作匹配。
提高導(dǎo)航角色的質(zhì)量
為了提高動畫和導(dǎo)航角色的質(zhì)量,我們將探索幾個可能性。
注視
讓角色注視和轉(zhuǎn)向興趣點對于表現(xiàn)注意力和期待效果十分重要。我們將使用動畫系統(tǒng) lookat API。因此需要另一個腳本。
// LookAt.cs?
using UnityEngine;?
using System.Collections;?
[RequireComponent (typeof (Animator))]?
public class LookAt : MonoBehaviour {
public Transform head = null;
public Vector3 lookAtTargetPosition;
public float lookAtCoolTime = 0.2f;
public float lookAtHeatTime = 0.2f;
public bool looking = true;
private Vector3 lookAtPosition;
private Animator animator;
private float lookAtWeight = 0.0f;
void Start (){
if (!head){
Debug.LogError("No head transform - LookAt disabled");
enabled = false;
return;
}
animator = GetComponent<Animator> ();
lookAtTargetPosition = head.position + transform.forward;
lookAtPosition = lookAtTargetPosition;
}
void OnAnimatorIK ()
{
lookAtTargetPosition.y = head.position.y;
float lookAtTargetWeight = looking ? 1.0f : 0.0f;
Vector3 curDir = lookAtPosition - head.position;
Vector3 futDir = lookAtTargetPosition - head.position;
curDir = Vector3.RotateTowards(curDir, futDir, 6.28f*Time.deltaTime, float.PositiveInfinity);
lookAtPosition = head.position + curDir;
float blendTime = lookAtTargetWeight > lookAtWeight ? lookAtHeatTime : lookAtCoolTime;
lookAtWeight = Mathf.MoveTowards (lookAtWeight, lookAtTargetWeight, Time.deltaTime/blendTime);
animator.SetLookAtWeight (lookAtWeight, 0.2f, 0.5f, 0.7f, 0.5f);
animator.SetLookAtPosition (lookAtPosition);
}
}
將該腳本添加到角色,并將 head 屬性分配給角色變換層級視圖中的 head 變換。LookAt 腳本沒有導(dǎo)航控制的概念;所以為了控制注視位置,我們回到?LocomotionSimpleAgent.cs?腳本,并添加幾行代碼來控制注視。在?Update()
?末尾添加:
LookAt lookAt = GetComponent<LookAt> ();
if (lookAt)
????lookAt.lookAtTargetPosition = agent.steeringTarget + transform.forward;
這樣就會告訴?LookAt?腳本將興趣點設(shè)置為沿路徑的大致下一個角點,或者如果沒有角落,設(shè)置為路徑的末端。
自己嘗試一下。
使用導(dǎo)航的動畫驅(qū)動角色
到目前為止,角色完全由代理指定的位置控制。這確保了對其他角色和障礙物的躲避直接轉(zhuǎn)換為角色位置。但是,如果動畫未跟上建議的速度,則可能導(dǎo)致腳滑現(xiàn)象。在這里,我們將稍微放松一下對角色的約束。大體上,我們將用躲避質(zhì)量換取動畫質(zhì)量。
將?LocomotionSimpleAgent.cs?腳本上的?OnAnimatorMove()
?回調(diào)行替換為以下代碼
void OnAnimatorMove (){
? ? ? ? ? ?
// 根據(jù)動畫移動情況使用導(dǎo)航表面高度來更新位置
? ? ? ? ? ?
Vector3 position = anim.rootPosition;
? ? ? ? ? ?
position.y = agent.nextPosition.y;
? ? ? ? ? ?
transform.position = position;
}
嘗試運行此代碼時,您可能會注意到,角色現(xiàn)在可以游離于代理位置(綠色線框圓柱體)。您可能需要限制該角色動畫游離問題。為此,可將代理拉向角色,或者將角色拉向代理位置。在?LocomotionSimpleAgent.cs?腳本上的?Update()
?方法末尾添加以下代碼。
// 將角色拉向代理
? ? ? ? ? ? ? ?
if (worldDeltaPosition.magnitude > agent.radius)
????transform.position = agent.nextPosition - 0.9f*worldDeltaPosition;
或者,如果希望代理跟隨角色,請?zhí)砑右韵麓a。
// 將代理拉向角色
? ? ? ? ? ? ? ?
if (worldDeltaPosition.magnitude > agent.radius)
?agent.nextPosition = transform.position + 0.9f*worldDeltaPosition;
具體哪種方法最合適取決于具體的用例。
結(jié)論
我們已經(jīng)設(shè)置一個使用導(dǎo)航系統(tǒng)移動的角色并相應(yīng)地設(shè)置了動畫。調(diào)整混合時間數(shù)字、注視權(quán)重等可以改善視覺效果,也是進一步探索此設(shè)置的好方法。