BehaviorDesigner 實(shí)現(xiàn)NPC 智能機(jī)器人
Created by miccall (轉(zhuǎn)載請(qǐng)注明出處 miccall.tech)
實(shí)現(xiàn)物體跟隨攝像機(jī)視野運(yùn)動(dòng)
1. VR實(shí)現(xiàn)機(jī)器人導(dǎo)航
- 項(xiàng)目里要求一個(gè)機(jī)器人跟隨在角色旁邊嘴拢,一直飛著桩盲,就像一個(gè)精靈一樣,總在主角的身邊席吴,移動(dòng)赌结,轉(zhuǎn)頭,都要移動(dòng)到合適的位置 孝冒。還得讓他面向主角柬姚,今天就來(lái)實(shí)現(xiàn)這個(gè)樣例 。
2. 問(wèn)題思考
- 1. 物體移動(dòng)到某個(gè)給定的位置(target)
- 2. 物體轉(zhuǎn)動(dòng)到面向攝像機(jī)
- 3. 出現(xiàn)在相機(jī)的視野當(dāng)中
- 4. 自定義物體在攝影機(jī)的Screen中的位置
3.實(shí)現(xiàn)以及方法
- 移動(dòng)的話 迈倍,本來(lái)可以用動(dòng)畫(huà)來(lái)實(shí)現(xiàn) 伤靠,因?yàn)閯?dòng)畫(huà)還沒(méi)有做好,我就用一個(gè)cube當(dāng)作那個(gè)機(jī)器人做樣例了 啼染。
- 首先有個(gè)cube之后 宴合,給他放一個(gè)移動(dòng)的腳本。這里我給他命名為PlayerTank 迹鹅。
- 我們的目的就是讓他運(yùn)動(dòng)到某個(gè)target 卦洽,所以我們得給他指定一個(gè)followTransform 。 同時(shí)還有他的移動(dòng)速度和轉(zhuǎn)動(dòng)速度 斜棚。
- 為了使他移動(dòng)不是很突兀阀蒂,我的思路是他先轉(zhuǎn)動(dòng)到面向follow物體,然后在直線移動(dòng)到給物體 弟蚀。所以算法很快寫(xiě)好了
void LookTransform(Transform Mtransform)
{
Vector3 tarPos = Mtransform.position;
Vector3 dirRot = tarPos - transform.position;
Quaternion tarRot = Quaternion.LookRotation(dirRot);
transform.rotation = Quaternion.Slerp(transform.rotation, tarRot, rotSpeed * Time.deltaTime);
}
- 簡(jiǎn)單解釋一下蚤霞,就是先確定物體的位置,然后求出指向他的方向义钉,并用插值的方法昧绣,
讓物體轉(zhuǎn)動(dòng)到面向指定的物體 。
好了捶闸,既然有了朝向的運(yùn)動(dòng)方向夜畴,那么走到這方向拖刃,就很簡(jiǎn)單了。
transform.Translate(new Vector3(0, 0, movementSpeed * Time.deltaTime));
- 那么什么時(shí)候停止運(yùn)動(dòng)呢 贪绘,我想了一下兑牡,決定用位置的差來(lái)判斷
就是
Vector3.Distance(transform.position, followTransform.position);
- 好了,既然停止的方法也有了税灌,最后要解決的問(wèn)題就是朝向攝像機(jī)了均函。
突然一想,這是問(wèn)題么垄琐,對(duì)边酒,這不是問(wèn)題 ,哈哈狸窘,剛剛寫(xiě)的那個(gè)算法墩朦,給一個(gè)攝像機(jī)就解決了嘛 。
然后給出具體的判斷邏輯 翻擒。
//該物體 接近要 到達(dá)的目標(biāo) 指定位置后就停止
if (Vector3.Distance(transform.position, followTransform.position) < 3f)
{
//當(dāng)物體道到位置時(shí) 讓物體面 向攝像機(jī)
LookTransform(Camre);
return;
}
else
{
//讓物體轉(zhuǎn)向 將要運(yùn)動(dòng) 的方向
LookTransform(followTransform);
transform.Translate(new Vector3(0, 0, movementSpeed * Time.deltaTime));
}
- 這樣就解決了物體移動(dòng)到target了氓涣,下一步就是固定target的位置,讓他在攝像機(jī)的固定位置了 陋气。
新建一個(gè)腳本文件CameraView劳吠,掛在攝像機(jī)上。為了方便調(diào)試巩趁,我又用了FPS腳本痒玩,
就是第一人稱(chēng)視角跟隨鼠標(biāo)轉(zhuǎn)動(dòng),就跟cs里面的玩法一樣议慰,(百度一大推代碼)蠢古。
第二個(gè)調(diào)試算法是一個(gè)國(guó)外大牛寫(xiě)的 ,他可以給定一個(gè)距離别凹,畫(huà)出攝像機(jī)的視野范圍
- 這里我畫(huà)了兩個(gè)邊 草讶,一個(gè)是距離攝像機(jī)8.5米 用黃色表示,距離攝像機(jī)12米的用紅色表示炉菲。
應(yīng)為篇幅問(wèn)題和詳略問(wèn)題堕战,這里不多解釋這個(gè)算法,有興趣的可以去研究一下拍霜,這里我們引用一下就行了嘱丢。
Vector3[] GetCorners(float distance)
{
Vector3[] corners = new Vector3[4];
float halfFOV = (theCamera.fieldOfView * 0.5f) * Mathf.Deg2Rad;
float aspect = theCamera.aspect;
float height = distance * Mathf.Tan(halfFOV);
float width = height * aspect;
// UpperLeft
corners[0] = tx.position - (tx.right * width);
corners[0] += tx.up * height;
corners[0] += tx.forward * distance;
// UpperRight
corners[1] = tx.position + (tx.right * width);
corners[1] += tx.up * height;
corners[1] += tx.forward * distance;
// LowerLeft
corners[2] = tx.position - (tx.right * width);
corners[2] -= tx.up * height;
corners[2] += tx.forward * distance;
// LowerRight
corners[3] = tx.position + (tx.right * width);
corners[3] -= tx.up * height;
corners[3] += tx.forward * distance;
return corners;
}
void FindUpperCorners()
{
Vector3[] corners = GetCorners(upperDistance);
// for debugging
Debug.DrawLine(corners[0], corners[1], Color.yellow); // UpperLeft -> UpperRight
Debug.DrawLine(corners[1], corners[3], Color.yellow); // UpperRight -> LowerRight
Debug.DrawLine(corners[3], corners[2], Color.yellow); // LowerRight -> LowerLeft
Debug.DrawLine(corners[2], corners[0], Color.yellow); // LowerLeft -> UpperLeft
}
- debug的時(shí)候敲才,直接調(diào)用FindUpperCorners()就可以了 坦辟。
剛開(kāi)始的時(shí)候 ,我就用的這個(gè)調(diào)試 贿讹,給出一個(gè)位置,然后計(jì)算他的偏移量伐谈,調(diào)試了很久,沒(méi)有一個(gè)良好的效果试疙,我決定換個(gè)思路了 诵棵,為了普遍大眾 ,我還是把這個(gè)調(diào)試方法貼出來(lái)了祝旷,有需要的可以試試 履澳。
第二個(gè)我就去翻api了 ,因?yàn)槲颐菜朴浀糜袀€(gè)屏幕坐標(biāo)和世界坐標(biāo)轉(zhuǎn)化的什么鬼方法來(lái)著怀跛。果然不出我所料距贷,這個(gè)方法的確是相當(dāng)?shù)暮糜玫难?。
試了一下官方給的調(diào)試方法吻谋,畫(huà)了一個(gè)點(diǎn)出來(lái) 忠蝗。
void OnDrawGizmosSelected()
{
Vector3 p = theCamera.ScreenToWorldPoint(new Vector3(100, 200, 8));
Gizmos.color = Color.blue;
//target.position = p;
Gizmos.DrawSphere(p, 1F);
}
- 好了,就連我最后決定用的位置也標(biāo)明了漓拾。
然后阁最,我就寫(xiě)了一個(gè)很簡(jiǎn)單的方法來(lái)達(dá)到目的。
void maketarget()
{
Vector3 p = theCamera.ScreenToWorldPoint(new Vector3(RH, RV, upperDistance));
target.position = p;
}
- 寫(xiě)完我都嚇了一跳 骇两,竟然如此簡(jiǎn)單速种。還是簡(jiǎn)單解釋一下 ,RH 是水平偏移量低千,RV是垂直偏移量配阵,upperDistance是距離攝像機(jī)的一個(gè)平面位置 。
接下來(lái)就是運(yùn)行看效果了 示血。
4.中途出現(xiàn)的小BUG
莫名其妙的做圓周運(yùn)動(dòng) 棋傍,然后我分析了線速度,角速度和半徑的關(guān)系 矾芙, 然后總結(jié)出一個(gè)基本的規(guī)律舍沙,他應(yīng)該是當(dāng)運(yùn)動(dòng)到某個(gè)特定的位置 ,正好滿足了 圓周運(yùn)動(dòng)的關(guān)系剔宪,然后我們調(diào)整movementSpeed 和rotSpeed 的值拂铡,讓他么盡可能 的和Distance消除乘積關(guān)系,這樣出現(xiàn)的幾率就微乎其微了 葱绒。 感想 -- 其實(shí)unity和現(xiàn)實(shí)物理感帅,理論物理 還是有很大的不同。
BehaviorDesigner 介紹以及使用方法
Behavior Designer 是一個(gè)行為樹(shù)插件 他提供了可視化編輯器 和強(qiáng)大的 API 可以輕松的創(chuàng)建 tasks(任務(wù))通過(guò)決策樹(shù)的方式判斷行為地淀,耦合度更高失球,更加方便的打造AI系統(tǒng) 。
本教程不是入門(mén)教程 ,而是通過(guò)一個(gè)引導(dǎo)來(lái)實(shí)現(xiàn)我們的主題 实苞。
- Sequence 隊(duì)列節(jié)點(diǎn)
-- 表示順序執(zhí)行的節(jié)點(diǎn) 此節(jié)點(diǎn)下屬所有節(jié)點(diǎn)依次執(zhí)行直到返回false
- Sequence 隊(duì)列節(jié)點(diǎn)
- Selector 選擇節(jié)點(diǎn)
-- 表示在此節(jié)點(diǎn)下選擇一個(gè)執(zhí)行 此節(jié)點(diǎn)下屬所有節(jié)點(diǎn)依次執(zhí)行直到返回true
- Selector 選擇節(jié)點(diǎn)
我們暫且就用這兩個(gè) 想要了解更多的 豺撑,請(qǐng)參考別的教程
基本任務(wù)
- 判斷是否到達(dá)目的地
- 移動(dòng)到目的地(包含起飛和停止動(dòng)畫(huà))
- 判斷是否面向攝像機(jī)
- 判斷是否執(zhí)相應(yīng)的動(dòng)畫(huà)( 原地浮動(dòng) )
- 判斷是否要面向UI物體
- 面向UI之后 如果操作 執(zhí)行相應(yīng)的動(dòng)畫(huà)(點(diǎn)頭 搖頭 攤手)
1.判斷是否到達(dá)目的地
- 這個(gè)方法在上面的教程已經(jīng)介紹了解決思路以及代碼實(shí)現(xiàn) 簡(jiǎn)單重新陳述一下,首先在攝像機(jī)上掛一個(gè)腳本 這個(gè)腳本用來(lái)測(cè)量攝像機(jī)的范圍黔牵,在范圍內(nèi)畫(huà)一個(gè)點(diǎn)聪轿,讓一個(gè)target(transform)覆蓋這個(gè)點(diǎn) 。
- VR中攝像機(jī)可以根據(jù)頭盔的傳感器猾浦,來(lái)旋轉(zhuǎn)第一人稱(chēng)視角 這個(gè)target就固定在視角的一個(gè)地方隨意移動(dòng) 陆错。
- 那么,機(jī)器人的z軸 (forward) 就一直面向這個(gè)target金赦。然后使用translate移動(dòng)到這個(gè)target附近音瓷。
- 我們要判斷的就是這個(gè)target和機(jī)器人的distance
- 我們自己寫(xiě)一個(gè)腳本來(lái)實(shí)現(xiàn)這個(gè)小功能(Task)
using UnityEngine;
namespace BehaviorDesigner.Runtime.Tasks.Basic.UnityVector3
{
[TaskCategory("Basic/Vector3")]
[TaskDescription("Returns the distance between two Vector3s.")]
public class dis_tance : Action
{
[Tooltip("target Vector3")]
public SharedVector3 firstVector3;
[Tooltip("The distance")]
[RequiredField]
public SharedFloat storeResult;
public SharedGameObject action;
//看代碼就應(yīng)該看到 我們需要一個(gè)target的位置向量 一個(gè)action的GameObject
//還有一個(gè)距離的返回值
public override TaskStatus OnUpdate()
{
storeResult.Value = Vector3.Distance(firstVector3.Value, action.Value.transform.position);
return TaskStatus.Success;
}
public override void OnReset()
{
storeResult = 0;
}
}
}
- 隨后,我們就要判斷這個(gè)距離的值 夹抗,來(lái)給定這個(gè)機(jī)器人 是否是移動(dòng)绳慎,還是靜止
- 我們還是來(lái)寫(xiě)一個(gè)自定義Task 來(lái)實(shí)現(xiàn)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BehaviorDesigner.Runtime.Tasks.Basic.UnityVector3 {
public class comparedis : Action {
public SharedFloat distance;
public SharedFloat compare_dis ;
/*compare_dis 表示一個(gè)范圍 以distance 減去這個(gè)范圍
表示以target為圓心 畫(huà)一個(gè)compare_dis的球 只要進(jìn)入這個(gè)球 ,說(shuō)明已經(jīng)到達(dá)目的地 */
public SharedBool storeResult;
/*我們返回一個(gè)bool值 他的意思是機(jī)器人是否靜止
如果和target的計(jì)算距離大于0 那么就不是禁止 他就應(yīng)該移動(dòng)到target 否則 他是靜止的漠烧,就應(yīng)該做相應(yīng)的動(dòng)畫(huà)
*/
public override TaskStatus OnUpdate()
{
storeResult.Value = distance.Value - compare_dis.Value > 0 ? false : true ;
return TaskStatus.Success;
}
public override void OnReset()
{
}
}
}
-
好了 我們這兩個(gè)Task已經(jīng)寫(xiě)好了 然后用一個(gè)Sequence 連接兩個(gè)任務(wù)
- 做完了這個(gè)偷线,我們又要分情況討論了 一個(gè)是移動(dòng) 一個(gè)是移動(dòng)結(jié)束
2.移動(dòng)到target
移動(dòng)到target我們要用一個(gè)判斷來(lái)做相應(yīng)的事件 首先 我們判斷第一步的變量 isstop 看看是否靜止 如果是false的話,執(zhí)行下面的步驟 否則的話 沽甥,跳轉(zhuǎn)到面向攝像機(jī)
下面一層 我們用一個(gè)selector節(jié)點(diǎn) 來(lái)選擇一個(gè)節(jié)點(diǎn)執(zhí)行
選擇是播放起飛動(dòng)畫(huà)呢 声邦,還是move呢首先播放起飛動(dòng)畫(huà)的條件是 isfirstfly 初始值是true 那么開(kāi)始執(zhí)行飛行動(dòng)畫(huà) ,執(zhí)行結(jié)束 把這個(gè)值設(shè)置false false就不進(jìn)入這個(gè)節(jié)點(diǎn) 而是去選擇執(zhí)行move 節(jié)點(diǎn) 摆舟。
動(dòng)畫(huà)這個(gè)也是一個(gè)坑 我用比較長(zhǎng)的一段詳細(xì)講一下亥曹。收悉的朋友可以跳過(guò)這一段 。
-looktarget前面也講過(guò)了 movetotarget是封裝以后的translate 可以自己動(dòng)腦去實(shí)現(xiàn)一下
3.停止邏輯
4.第一次停止動(dòng)畫(huà)
5.面向攝像機(jī)
- 面向攝像機(jī)以后 要判斷是否已經(jīng)結(jié)束 我們要判斷向量的方向來(lái)決定
- 首先先計(jì)算機(jī)器人的forward向量 然后機(jī)器人位置減攝像機(jī)的位置 恨诱,得到一個(gè)方向向量 最后判斷兩個(gè)向量的單位向量是否相同 就可以了
6.面向UI
- 我規(guī)定 當(dāng)外界代碼控制修改一個(gè)全局變量 就把他設(shè)置成true 然后指定一個(gè)UI物體 那么下一幀就可以面向UI了