六、transform組件
🚩transform組件的作用:
- 1、控制3D物體的平移 縮放 旋轉
- 2、維護場景樹
每個MonoBehavior都有一個成員指向當前節點的transform組件
每個MonoBehavior都有成員gameObject
指向該腳本的組件實例所掛載的節點對象
🚩代碼控制屬性
void Start () {
// this.gemeObject 組件實例所掛載的場景的節點對象
Debug.Log(this.gameObject.name);
}
當然 還有別的屬性可以獲取:
Debug.Log(this.gameObject.layer);
Debug.Log(this.gameObject.tag);
🚩可見性
控制可見性:
Debug.Log(this.gameObject.activeSelf); // 自己是否可見
Debug.Log(this.gameObject.activeInHierarchy); // 自己在體系中是否可見
// 設置可見性
this.gameObject.SetActive(true);
在這裏 若game_root節點隱藏了 那麼Cube和Sphere的可見性勾還是打上的 這就是activeSelf
但是他們其實已經不可見了(在game_root這個體系中) 因此 這就是activeInHierarchy
🚩父子關係
場景樹是有父子關係的 若父親不可見 那麼下面全部孩子都不可見
任何組件都會有數據成員指向該節點的transform示例 因此可以通過this來訪問
通過任何一個組件來獲得每一個節點的transform
this.transform
transform組件不能有多個 只能有一個 無法刪除
同理 transform也是一個組件 因此也有一個gemeObject指向其掛載的節點
Debug.Log(this.transform.gameObject.name);
🚩場景樹的構建方式
Unity的場景樹是基於transform構建的
game_root有個transform指向了gameObject
在它下面有棵樹保存了它的孩子 - Cube和Sphere 實際上是Cube的transform和Sphere的transform
其實整個場景樹是由transform組件構成的 每個transform都會指向節點
name其實是這裏的名稱:
🚩Find和FindChild按名稱查找
(類似於HTML的父子節點的查找…)
// 獲取指定的transform
Transform trans=this.transform.Find("Cube"); // 名稱注意大小寫
GameObject cube = trans.gameObject;
Debug.Log(cube.name);
樹形關係是由transform串聯起來的 而並不是節點串聯的
trans=this.transform.FindChild("Sphere");
GameObject sphere=trans.gameObject;
Debug.Log(sphere.name);
Find函數和FindChild函數返回的都是Transform對象
區別:
Find可以去子節點的子節點中查找 (需寫多層路徑 比如Cube/Sphere代表Cube節點下的Sphere子節點)
而FindChild則無此功能
// parent屬性
Debug.Log(trans.parent.gameObject.name);
🚩GetChild按下標獲取子節點
還可以按下標獲取子節點:
Debug.Log(this.transform.GetChild(0).gameObject.name); // Cube
Debug.Log(this.transform.GetChild(1).gameObject.name); // Sphere
任意組件都有gameObject和transform組件 且transform組件是gameObject必帶的
因此 只要獲得transform那麼就能獲取到gameObject了
transform是三維位置定位最重要的組件 節點的位置都是由transform進行控制的
且transform是幫助建立場景樹的
根transform 比如game_root Main Camera 或是Directional Lignt 都是沒有父節點的
除了可以基於transform查找 還可以基於gameObject的全局查找:
// 基於GameObject的查找 查找的是GameObject而不是transform了
Debug.Log(GameObject.Find("game_root/Sphere").name);
相對來說 從子節點查找的性能會比全局查找高一些
🚩tag標籤
可以爲節點打上tag標籤 然後根據標籤查找
(類似於HTML的根據class或id查找…)
點擊的Add Tag…
點擊小加號
然後輸入名稱即可
然後即可選擇標籤了:
在查找的時候 標籤就是標籤 和路徑無關 無論在什麼路徑下 只要打了標籤 那麼直接根據標籤名查找即可
// 基於Tag標籤名的全局查找
Debug.Log(GameObject.FindWithTag("mytag").name); // Cube
若有多個節點都是同樣的tag 那麼先找到哪個帶有該tag的節點 就返回哪個
只會返回一個 而不是返回多個
當然 也有函數可以查找多個
FindGameObjectsWithTag()
返回的是GameObject數組
// 查找多個
GameObject[] gameObjects=GameObject.FindGameObjectsWithTag("mytag");
for (int i=0;i<gameObjects.Length;i++)
{
Debug.Log(gameObjects[i].name);
}
比如 若要查詢敵人集合 可以將所有敵人都打上tag 然後一次性查詢全部敵人
🚩絕對座標系和相對座標系
絕對座標 即世界座標 就是transform組件中的Position
當根節點移動位置後 Cube的座標還是不會改變 但實際上世界座標已經改變了
因此 可以知道 相對座標即爲相對於父節點的位置
而世界座標即相對於世界(整個場景)的位置
例如 家的位置在地球的東經XX北緯XX
而人在家的XX方向XX距離的位置
那麼 將這兩個座標疊加 即爲人的世界座標了
代碼中的座標
在代碼中的相對座標是localPosition
// 在Unity的編輯器中的position是相對座標 而代碼中的position並不是!乃是絕對座標
Debug.Log(this.transform.position);
// 在代碼中的相對座標是localPosition
Debug.Log(this.transform.localPosition);
比如子節點的位置是(0,2,0) 而它的父節點的位置是(0,0,10) 那麼 兩個相加 子節點的世界座標就是 (0,2,10)
Transform cube=this.transform.FindChild("Cube");
// 在Unity的編輯器中的position是相對座標 而代碼中的position並不是!乃是絕對座標
Debug.Log(cube.position); // (1.0,4.0,0.0)
// 在代碼中的相對座標是localPosition
Debug.Log(cube.localPosition); // (0.0,4.0,0.0)
若父節點還有父節點 那麼還會繼續疊加
🚩距上一次刷新的時間 deltaTime/fixedDeltaTime
每個組件都會在遊戲刷新的時候調用Update生命週期 由於是每隔一段時間調用的 因此會產生時間間隔
用Time中的deltaTime
來獲取
若在FixedUpdate生命週期中 則用fixedDeltaTime
來獲取
// 更新
void Update () {
// deltaTime 距離上一次刷新的時間
float dt = Time.deltaTime;
}
// 根據物理時間更新(是固定的)
void FixedUpdate()
{
// 物理引擎固定更新的時間間隔 在FixedUpdate中用的是fixedDeltaTime
float dt = Time.fixedDeltaTime;
}
🚩transform成員變量
- parent - 父級的transform
- childCount - 該transform有多少子節點
- forward up right
- 當點擊一個物體 會出現它的三維座標
三維座標會有其三個方向 綠色是y 紅色是x 藍色是z - forward (z的正方向 正前)
- up (y的正方向 正上)
- right (x的正方向 正右)
- 當點擊一個物體 會出現它的三維座標
對於前後左右上下 只需要三個向量即可 剩餘三個是方向量
返回的是一個座標
Debug.Log(cube.forward);
在遊戲中的向前走 實際上就是在向前的方位上行走
Debug.Log(cube.right);
Debug.Log(cube.up);
🚩方向向量
forward up 和 right都是世界座標系下的單位方向向量 表示的是方向
單位向量*標量(只有大小 沒有方向的量)=該標量在該單位向量的x y z方向上的分解
因此 在下面的例子中 cube_trans.forward就代表了模型在z軸的正方向
若旋轉了 那麼cube_trans.forward也會自動隨之改變
public class game_scene : MonoBehaviour {
Transform cube_trans;
// Use this for initialization
void Start () {
cube_trans = this.transform.Find("Cube");
// 世界座標下的前 右 上方向
// cube_trans.forward;
}
// Update is called once per frame
void Update () {
// 移動距離:10m/s 這是個標量(只有大小 沒有方向的量)
float s = 10 * Time.deltaTime;
// 在該標量的方向上移動s
// 單位向量*標量=該標量在該單位向量的x y z方向上的分解
cube_trans.position += cube_trans.forward * s;
}
}
🚩座標的轉換
局部座標[/相對座標]轉換爲世界座標
若有個球體
要將該球體放置於世界座標0,0,10的位置 那麼只需在代碼中修改該球體的position即可
還有個方法 使用TransformPoint
TransformPoint可以將當前局部座標系下的座標轉換爲世界座標
// 【將局部座標轉換爲世界座標】
// 即爲 在以this.transform爲原點的座標系中將相對座標0,0,0轉換爲世界座標
// Vector3即爲三維座標的意思
Debug.Log(this.transform.TransformPoint(new Vector3(0, 1, 2)));
世界座標轉換爲局部座標[/相對座標]
使用InverseTransformPoint
函數
// 【將世界座標轉換爲局部座標】
// 即 對於世界座標0,6,2 當前世界座標(即this.transform)的相對位置
Vector3 local_post = this.transform.InverseTransformPoint(new Vector3(0, 6, 2));
Debug.Log(local_post);
🚩平移
有Space.World世界空間和Space.Self自己的空間
Space.World和Space.Self
比如 按住Alt然後旋轉畫面 這是以節點爲中心進行旋轉的 這就是Space.Self自己的空間
而Space.World是以世界座標系的原點爲旋轉點進行旋轉
Translate
若要平移物體 只需使用Translate()
即可
void Update () {
// 移動距離:10m/s 這是個標量(只有大小 沒有方向的量)
float s = 10 * Time.deltaTime;
// 默認是在原來的基礎上按照x y z的方向來平移
this.cube_trans.Translate(new Vector3(0, 0, s));
}
默認是在原來的基礎上按照x y z的方向來平移的 若要以世界座標軸來平移 可以換添加一個構造參數
this.cube_trans.Translate(new Vector3(0, 0, s),Space.World);
還可設置以使得以指定transform作爲參照物移動
this.cube_trans.Translate(new Vector3(0, 0, s), this.transform);
平移物體有兩個關鍵:參考系 和 在該參考系下每個方向的分量
參考系有三個 分別是自己(Self) 和 世界(World) 和 自定義目標
🚩縮放
縮放 同樣也是有x方向的縮放 y方向的縮放 和 z方向的縮放
若將父節點進行縮放 那麼裏面的所有子節點都會跟着縮放相應的大小 比例係數會影響子節點
使用localScale來控制縮放 這是相當於父節點而言的
改變縮放
this.cube_trans.localScale = new Vector3(0.5f, 0.5f, 0.5f);
查詢縮放
// 若父節點進行了縮放 那麼比例係數會影響子節點
Debug.Log("localScale " + this.cube_trans.localScale);
還有個lossyScale 這是隻讀的 是相對於全局而言的
// 整個對象在全局的縮放係數
Debug.Log(this.cube_trans.lossyScale);
🚩旋轉的表示法
矩陣旋轉
任何一個旋轉都可以使用矩陣來進行表示
優點:旋轉軸可以是任意向量
缺點:旋轉其實只需要知道一個向量 + 一個角度 總共4個值的信息
但矩陣法卻使用了16個元素 比較耗費內存
因此 矩陣法使用的並不多
歐拉角
歐拉角是用來唯一地確定定點轉動剛體位置的三個一組獨立角參量 由章動角θ 進動角ψ和自轉角φ組成
歐拉角要按照一定的次序形成不同的角度 (就像是轉魔方…)
若順序不同 雖然是同樣的角度 但是最終的結果值也都是不同的
Unity是按照z x y
的順序進行旋轉的
- 優點:
- 很容易理解 形象直觀
- zxy的順序不同會造成不同的結果
- 缺點:會造成萬向節死鎖(Gimbal Lock)的現象
比如下圖 x和z兩個軸在同一個平面內 這樣 無論旋轉哪個角 都是繞着y軸轉
如此 就丟掉了一個維度
萬向節死鎖在某些情況下 會導致動畫不夠流暢
若出現該情況 則必須同時修改兩個旋轉軸纔行了
Quaternion 四元數
在某些情況下不方便使用歐拉角 此時就需使用四元數了
四元數是一種表示旋轉的方式
- 優點:
- 可以避免萬向節死鎖現象
- 只需要一個四維的四元數即可執行繞任意過原點的向量的旋轉 方便快遞
某些實現下比旋轉矩陣的效率更高 - 可以提供平滑插值
- 缺點:比歐拉旋轉稍微複雜 因爲多了一個維度
歐拉角和四元數之間可以相互轉化
在Unity的編輯器中 爲了直觀地顯示旋轉 使用的是歐拉角表示法
而在代碼裏 爲了避免萬向節死鎖問題 使用的是四元數
因此 在編輯器中看到的角度和代碼中的角度是不一樣的
將四元數轉換爲歐拉角
eulerAngles
能返回四元數對應的歐拉角
// Unity在代碼中爲了避免萬向節死鎖問題 使用四元數來存放一個旋轉
// Unity編輯器中爲了直觀地顯示旋轉 顯示的是歐拉角
// eulerAngles可以將四元素轉換爲歐拉角
Vector3 e_degree = cube_trans.rotation.eulerAngles;
Debug.Log(e_degree);
將歐拉角轉換爲四元數
使用Quaternion的Euler()
函數 然後傳入一個三維向量對象 可以將歐拉角轉換爲四元數
// 將歐拉角轉換爲四元數 直接到了指定角度
cube_trans.rotation = Quaternion.Euler(new Vector3(0, 30, 60));
這個是直接到了指定角度 這是和Rotate()的區別
Rotate 歐拉角度
調用transform的Rotate()
函數 然後傳入一個三維向量對象 同樣能夠旋轉指定的歐拉角度
是在當前的基礎上再進行旋轉的
// 直接旋轉歐拉角度 是在當前的基礎上再進行旋轉的
cube_trans.Rotate(new Vector3(0, 30, 60));
同樣的 Rotate不僅只能以自己旋轉 也支持傳入構造參數 改變參照物 能夠以世界空間和指定參照物來旋轉
實現一直旋轉
// 定義角速度
float w = 360;
// 角度=角速度×時間
float degree = w * Time.deltaTime;
// 旋轉
// cube_trans.Rotate(0, degree, 0);
// 將每個小變換組成一個大變換 因此不能用加法[+] 而是應該用乘法[*]
cube_trans.rotation = cube_trans.rotation * Quaternion.Euler(new Vector3(0, degree, 0));
🚩其它常用旋轉方法
在三維空間中 有個軸既垂直於初始位置 又垂直於結束位置 這個軸就是方向
初始位置和結束位置的角就是角度
方向 + 角度就能算出四元數 將四元數給物體 物體就能旋轉
Rotate(歐拉角,空間)
Rotate(方向,角度)
private Transform cube = transform.FindChild("Cube");
// 歐拉角旋轉
cube.Rotate(new Vector3(0, 45, 0));
// 旋轉向量+角度
cube.Rotate(new Vector3(0, 1, 0), 45);
Rotate是逆時針爲負 順時針爲正
使用RotateAround()
繞着某個點旋轉
語法:RotateAround(要圍繞的點,方向,角度)
public class game_scene : MonoBehaviour {
private Transform sphere;
// Use this for initialization
void Start () {
sphere = transform.FindChild("Sphere");
}
// Update is called once per frame
void Update () {
// 角度=時間*速度
float angle = Time.deltaTime * 180;
// 要圍繞的中心點
Vector3 pos = new Vector3(0, 0, 0);
// 旋轉 RotateAround(要圍繞的點,方向,角度)
sphere.RotateAround(pos, new Vector3(0, 1, 0), angle);
}
}
LookAt() ★
使用LookAt()
對準對應的方位(視線對準)
經常用與遊戲的尋路 移動等功能
頭頂方向
LookAt是跟着目標不停旋轉 因此需要旋轉角度和旋轉向量
旋轉向量在LookAt中 被稱作是頭頂方向
頭頂方向 即爲注視着目標點的時候 是繞着什麼軸旋轉的
默認以y軸(0,1,0)爲旋轉軸
有了目標點的位置 可以計算出旋轉角度
有了頭頂方向 能夠指名按照哪個向量進行旋轉
有了角度和旋轉向量 那麼就能組成一個四元數 組成一個旋轉方位
void Update () {
// 角度=時間*速度
float angle = Time.deltaTime * 180;
// 要圍繞的中心點
Vector3 pos = new Vector3(0, 0, 0);
// 旋轉 RotateAround(要圍繞的點,方向,角度)
sphere.RotateAround(pos, new Vector3(0, 1, 0), angle);
// 鎖定目光
cube.LookAt(sphere.position, new Vector3(0, 1, 0));
}
localEularAngles()
返回一個當前旋轉相對於父節點的歐拉角
🚩Quaternion四元素常用旋轉方法
LookRotation()
根據自己的上方向盯着前方
用法和LookAt類似 但LookAt使用會更加多
Quaternion rot= Quaternion.LookRotation(this.sphere.position,new Vector3(0,1,0));
cube.rotation = rot;
Angle(lhs,rhs)
計算兩個旋轉之間的夾角
用於計算兩個角之間的差值
FromToRotation(from,to)
獲得從初始向量旋轉到目標向量的旋轉四元數
// 獲得從初始向量旋轉到目標向量的旋轉四元數
Quaternion rot= Quaternion.FromToRotation(new Vector3(0, 0, 1), new Vector3(1, 0, 1));
cube.rotation = rot;
🚩Vector3
Vector3若表示三維座標 則表示一個立方體的點(帶有三條座標軸)
若表示向量 則表示從原點開始到結束點(x,y,z)的向量
向量的加減法
向量的加減法直接將每個分量加減即可
比如:
(x1,y1,z1)+(x2,y2,z2)=(x1+x2,y1+y2,z1+z2)
(x1,y1,z1)-(x2,y2,z2)=(x1-x2,y1-y2,z1-z2)
向量的縮放
向量乘以比例係數即可縮放
放大:(x1,y1,z1)*n=(x1n,y1n,z1n)(n>0)
縮小:(x1,y1,z1)*n=(x1n,y1n,z1n)(n<0)
同理 向量除以比例係數即可縮小
向量的比較
有(x1,y1,z1)和(x2,y2,z2)
若x1=y1,x2=y2,z1=z2那麼這兩個向量相等
只要有一個不等 那麼這兩個向量就不相等
單位向量
單位向量就是根號(x^2+y^2+z^2)=1
的向量
Vector提供了一些單位向量:zero one forward right up
Debug.Log(Vector3.zero); // (0.0,0.0,0.0)
Debug.Log(Vector3.one); // (1.0,1.0,1.0)
Debug.Log(Vector3.forward); // (0.0,0.0,1.0)
Debug.Log(Vector3.right); // (1.0,0.0,0.0)
Debug.Log(Vector3.up); // (0.0,1.0,0.0)
因此:
float speed = 10;
float s = speed * Time.deltaTime;
cube.transform.position += new Vector3(0,0,s);
可改爲
float speed = 10;
float s = speed * Time.deltaTime;
cube.transform.position += Vector3.forward*s;
Lerp線性插值
Lerp求兩個向量之間的線性插值
from+(to-from)*t
如下圖 若t=0 就在A點
若t=1 就在B點
若t=0.5 就在A點和B點的中間
所謂線性插值 這是一條直線
// from (0,0,0) to (10,0,0)
// Time.time爲從開始到現在所有delteTime的累計之和
cube.transform.position = Vector3.Lerp(Vector3.zero, Vector3.right * 10,Time.time);
Slerp球面插值
繞着球面以弧形移動
由於球面的圓心是(0,0,0) 因此不能從(0,0,0)開始移動
// 球面插值
// from (-10,0,0) to (10,0,0)
cube.transform.position = Vector3.Slerp(Vector3.right * -10, Vector3.right * 10, Time.time);
distance向量的距離
用Distance()
來求向量的距離
向量的距離就是兩個點之差求模
// Distance向量的距離
float len = Vector3.Distance(Vector3.right, Vector3.right * -1);
Debug.Log(len); // 2
Angle向量的角度
// Angle向量的角度
float degree = Vector3.Angle(Vector3.right, Vector3.right * -1);
Debug.Log(degree); // 180
Min Max 比較向量的模的大小
Vector3 max= Vector3.Max(Vector3.right, Vector3.right * 2);
Debug.Log(max);
Vector3 min = Vector3.Min(Vector3.right, Vector3.right * 2);
Debug.Log(min);
transform總共包括了四個邏輯
分別是:
- 1、樹 - find get…
- 2、旋轉 - lookAt…
- 3、平移 - Translate…
- 4.縮放 - rotate…
🚩獲取組件實例
每個gameObject下面都有一個transform
MonoBehavior組件也有一個transform指向gameObject
MonoBehavior還有一個數據成員gameObject指向gameObject
因此 圍繞着gameObject有一個組件transform 除此之外 還有一些必有的組件1,2…
同時 每個組件都有一個transform變量指向節點所對應的transform組件 包括其自己也有一個transform變量指向它自己
每個MonoBehavior組件都會拿到gameObject
同時 transform又是一個組件 因此也能拿到gameObject
總而言之 所有組件都會有transform和gameObject
- 凡是組件 都會有一個
gameObject
成員可以獲得該組件實例所綁定的節點 - 凡是組件 都會有一個
transform
成員可以獲得該組件實例所綁定的transform組件 - 凡是GameObject 都會有一個
transform
成員指向其所掛載的Transform組件的實例
GetComponent() 獲取節點上所掛載的transform組件實例
只需要在泛型<>
裏填入要獲取的組件的類型即可
獲取節點上所掛載的transform組件實例
// 獲取節點上所掛載的transform組件實例
// 會自動查找節點掛載的第一個Transform的組件實例
Transform t=gameObject.GetComponent<Transform>();
Debug.Log(t.name);
獲取節點上所掛載的所有爲該類型的組件
// 獲取節點上所掛載的所有爲該類型的組件
Transform[] tList = gameObject.GetComponents<Transform>();
for (int i=0;i<tList.Length;i++)
{
Debug.Log(tList[i].name);
}
除了通過類型 還可以根據組件實例的名稱獲取 傳入字符串格式的名稱即可
// 獲取名爲cube的組件實例
Debug.Log(cube.gameObject.GetComponent("cube"));
上述代碼的特點 都是從gameObject
開始查找的
也可通過組件 獲取當前組件所掛載的節點的其它組件
其實只是封裝了一層 本質還是通過組件找到節點 然後通過節點找指定組件的 就不需要通過gameObject了
// 根據組件獲取當前組件所掛載的節點的其它組件
// 其實只是封裝了一層 本質還是通過組件找到節點 然後通過節點找指定組件的 就不需要通過gameObject了
this.GetComponent<Transform>();
如此 即可在一個組件裏調用其它另外的組件裏的方法了
將代碼中的參數綁定到編輯器
只需要加上public的數據成員 即可將該數據成員綁定到對應的編輯器上了
比如:
public class game_scene : MonoBehaviour {
public int speed=10;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
然後就會識別到了:
同樣 在編輯器中進行修改 在代碼中也能拿到對應的值 (類似於Vue的雙向數據綁定…)