记录最近学习Unity3D中的一些小知识点。
Unity3D的四种坐标系 Unity3D的四种坐标系
World Space(世界坐标):我们在场景中添加物体(如:Cube),他们都是以世界坐标显示在场景中的。transform.position
可以获得该位置坐标。与之类似的还有局部坐标系,是相对于父节点而言的,获取方法为transform.localPosition
。
Screen Space(屏幕坐标):以像素来定义的,以屏幕的左下角为(0,0)
点,右上角为(Screen.width,Screen.height)
,Z的位置是以相机的世界单位来衡量的。注:鼠标位置坐标属于屏幕坐标,Input.mousePosition
可以获得该位置坐标,手指触摸屏幕也为屏幕坐标,Input.GetTouch(0).position
可以获得单个手指触摸屏幕坐标。
ViewPort Space(视口坐标):视口坐标是标准的和相对于相机的。相机的左下角为(0,0)
点,右上角为(1,1)
点,Z的位置是以相机的世界单位来衡量的。
绘制GUI界面的坐标系:这个坐标系与屏幕坐标系相似,不同的是该坐标系以屏幕的左上角为(0,0)
点,右下角为(Screen.width,Screen.height)
。
四种坐标系的转换
世界坐标→屏幕坐标:
1 2 camera.WorldToScreenPoint(transform.position); //将世界坐标转换为屏幕坐标。其中camera为场景中的camera对象。
屏幕坐标→视口坐标:1 2 camera.ScreenToViewportPoint(Input.GetTouch(0).position); //将屏幕坐标转换为视口坐标。其中camera为场景中的camera对象。
视口坐标→屏幕坐标:1 camera.ViewportToScreenPoint();
视口坐标→世界坐标:1 camera.ViewportToWorldPoint();
世界坐标中的固有方向
方向
描述
up
世界坐标的Y轴方向
right
世界坐标的X轴方向
forward
世界坐标的Z轴方向
访问其它的GameObject或Component 查找GameObject 通过Gameobject.Find
来查找,参数为GameObject的名称(或带路径的名称):
1 2 3 4 5 GameObject hand; hand = GameObject.Find("Hand"); hand = GameObject.Find("/Hand"); hand = GameObject.Find("/Monster/Arm/Hand"); hand = GameObject.Find("Monster/Arm/Hand");
根据Tag判断类型:
1 2 3 4 if(gameObject.tag == "Player") { //... }
获取所有匹配标签的GameObject:
1 GameObject [] objs = GameObject.FindGameObjectsWithTag("标签名");
查找GameObject下挂载的Component 1 2 3 4 5 6 7 8 HingeJoint hinge = gameObject.GetComponent( typeof(HingeJoint) ) as HingeJoint; // 根据type查找 HingeJoint hinge = gameObject.GetComponent<HingeJoint>(); // 泛型方式根据type查找 HingeJoint hinge = gameObject.GetComponent( "HingeJoint" ) as HingeJoint; // 根据type的名称查找
类似的还有:
1 2 3 4 5 6 7 GetComponent() GetComponents() GetComponent() GetComponentInChildren() GetComponentsInChildren() GetComponentInParent() GetComponentsInParent()
内置事件函数执行顺序 Unity3D的脚本中有一些内置的事件函数,其执行顺序如下:
编辑器(Editor)
ResetReset
用于在脚本被第一次挂载到对象上时初始化脚本属性,或者当使用Reset
命令时。
第一次加载场景(First Scene Load) 场景开始时会调用以下函数,场景中的每个对象只调用一次。
Awake 在实例化prefab之后、所有的Start()
之前调用。如果一个GameObject在启动期间处于非激活状态,则不会调用Awake()
,直到它被激活,或者它挂载的任何脚本中有函数被调用。
OnEnable (只有当Object在激活状态才会被调用)该函数在Object被启用(enabled)时调用。当创建MonoBehaviour
实例,如载入关卡、或挂载有该脚本组件的GameObject被实例化时会调用该函数。
注意:对于被添加到场景中的对象,Awake
和OnEnable
函数会在所有脚本的Start
、 Update
等之前被调用,但对于游戏过程中实例化的对象无法强制如此。
第一帧更新之前(Before the first frame update)
Start 如果脚本实例处于激活状态(enabled),会在第一帧刷新之前调用Start
。
对于场景中添加的游戏对象,所有脚本的Start
都会在任意脚本的Update
等函数之前调用,但对于游戏过程中实例化的对象无法强制如此。
帧之间(In between frames)
OnApplicationPause 当帧内侦测到有暂停时,帧结束时会调用此函数。调用于正常的帧更新之间。在OnApplicationPause
被调用后将会有额外的一帧以显示暂停状态的图像。
更新顺序(Update Order) 当要跟踪游戏逻辑、交互、动画、摄像机位置等时,有一些事件可供使用。通常的模式是将大多数工作放在Update
函数中执行,但也有一些其它的函数供使用。
FixedUpdate 通常FixedUpdate
会比Update
调用得更频繁。当帧率很低时,每帧可能有多次调用;当帧率很高时,可能在帧之间不会有调用。所有的物理计算及更新都是在FixedUpdate
之后立即执行。当在FixedUpdate
内进行运动计算时,相关数值可不必乘以Time.deltaTime
,因为FixedUpdate
是由可靠地计时器调用,而与帧率无关。
UpdateUpdate
在每帧被调用,是帧更新的主要承载函数。
LateUpdateLateUpdate
在每帧调用一次,调用时机为Update
结束之后。Update
中的所有计算都会在LateUpdate
调用之前完成。LateUpdate
一个比较通常的用法是跟随的第三人视角摄像机。如果角色在Update
中移动或转动,则可以将所有的摄像机移动和转动放入LateUpdate
。这样可以确保在摄像机跟随角色之前角色已经完成了所有的移动。
渲染(Rendering)
OnPreCull 在摄像机对场景进行剔除(cull)之前调用。剔除将会决定对于摄像机来说哪些游戏对象可见或不可见。OnPreCull
调用之后即是摄像机的剔除操作。
OnBecameVisible/OnBecameInvisible 当游戏对象对于所有摄像机都变为可见(visible)/不可见(invisible)时调用
OnWillRenderObject 如果游戏对象对于摄像机可见,则每有一个摄像机将会调用一次
OnPreRender 在摄像机对场景进行渲染之前调用
OnRenderObject 在常规场景渲染完成之后调用。此时可以使用GL
类或Graphics.DrawMeshNow
来绘制自定义的几何体
OnPostRender 在摄像机完成对场景的渲染之后调用
OnRenderImage 在场景渲染完成之后调用,用于屏幕图像的后处理(postprocessing)
OnGUI 用于响应GUI事件,在每帧内会调用多次。首先处理Layout
和Repaint
事件,紧接着处理每个输入事件的Layout
和键盘/鼠标事件
OnDrawGizmos 在场景中绘制用于可视化的Gizmo
协程(Coroutines) 正常的协程会在Update
函数返回后更新。协程是一个可以将其执行挂起(yield
)直到其YieldInstruction
完成的函数。以下是协程的不同用法:
yield 协程将在下一帧的所有的Update
完成调用之后继续执行
yield WaitForSeconds 协程将在指定时间的延迟之后的一帧,调用全部Update
函数之后
yield WaitForFixedUpdate 协程在所有的FixedUpdate
调用之后继续
yield WWW 协程在一个WWW
下载完成后继续
yield StartCoroutine 将协程构成链,并且会先等待MyFunc
协程完成
销毁对象(When the Object is Destroyed)
OnDestroy 会在该对象存在的最后一帧所有帧更新函数调用完成之后调用(在调用Object.Destroy
或是关闭场景时会销毁对象)
退出(When Quitting) 以下函数会对所有激活状态(active)的游戏对象调用
以上各函数的执行可参考下图:
主要变量
变量
描述
childCount
返回Transform的子节点的个数
eulerAngles
角度单位的欧拉角形式的旋转参数
forward
世界坐标下蓝色轴(Z)的正向
hasChanged
从上次被置为false
之后transform是否发生过变化
hierarchyCapacity
transform的层级结构的transform容量
hierarchyCount
transform层级结构中transform的数量
localEulerAngles
相对于父节点的角度单位的欧拉角形式的旋转参数
localPosition
相对于父节点的位置参数
localRotation
相对于父节点的旋转参数
localScale
相对于父节点的缩放比例参数
localToWorldMatrix
将局部坐标转化为世界坐标的矩阵
lossyScale
全局尺度的缩放比例
parent
父节点
position
世界坐标下的位置参数
right
世界坐标下红色轴(X)的正向
root
层级结构的最顶端
rotation
世界坐标下四元数形式的旋转参数
up
世界坐标下绿色轴(Y)的正向
worldToLocalMatrix
将世界坐标转化为局部坐标的矩阵
主要方法
方法
描述
DetachChildren
断开与所有子节点的父子关系
Find
根据名字查找子节点并返回
GetChild
根据索引值返回子节点
GetSiblingIndex
获取在同级节点中的索引
InverseTransformDirection
将方向direction从世界空间转化到局部空间
InverseTransformPoint
将位置position从世界空间转化到局部空间
InverseTransformVector
将矢量vector从世界空间转化到局部空间
IsChildOf
是否是parent的子节点
LookAt
旋转transform,以使得forward指向target当前位置
Rotate
按照Z->X->Y的顺序,沿Z轴旋转eulerAngles.z角度,沿X轴旋转eulerAngles.x角度,沿Y轴旋转eulerAngles.y角度
RotateAround
在世界坐标中沿着穿过指定点point的轴axis旋转指定角度angle
SetAsFirstSibling
将transform移至同级结点列表的头部
SetAsLastSibling
将transform移至同级结点列表的末尾
SetParent
设置父节点
SetPositionAndRotation
设置transform组件在全局坐标系下的位置和旋转参数
SetSiblingIndex
设置在同级结点中的索引
TransformDirection
将方向direction从局部空间转化到世界空间
TransformPoint
将位置position从局部空间转化到世界空间
TransformVector
将矢量vector从局部空间转化到世界空间
Translate
使用translation的方向和距离移动transform
读写xml 需要额外引入文件/命名空间:
1 2 using System.Xml; using System.IO;
读取xml 现有文件test.xml文件如下,文件内容为:
1 2 3 4 5 6 7 8 9 10 <root > <enemy id ="1000" name ="enemy1" lv ="1" > <attack > 10</attack > <hitpoint > 100</hitpoint > </enemy > <enemy id ="1001" name ="enemy2" lv ="2" > <attack > 20</attack > <hitpoint > 180</hitpoint > </enemy > </root >
加载并输出xml文件的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 string filepath = Application.dataPath + @"/XML/test.xml"; if (File.Exists (filepath)) { XmlDocument xmlDoc = new XmlDocument (); xmlDoc.Load (filepath); XmlNodeList nodeList = xmlDoc.SelectSingleNode ("root").ChildNodes; foreach (XmlElement xe in nodeList) { Debug.Log ("Name :" + xe.Name); Debug.Log ("Attribute name:" + xe.GetAttribute ("name")); Debug.Log ("Attribute lv:" + xe.GetAttribute ("lv")); foreach (XmlElement x1 in xe.ChildNodes) { Debug.Log ("Inner Name :" + x1.Name); Debug.Log ("Inner Value :" + x1.InnerText); } } Debug.Log ("complete XML = " + xmlDoc.OuterXml); } else { Debug.Log ("file not exists"); }
写入xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 string filepath = Application.dataPath + @"/XML/test1.xml"; if(!File.Exists (filepath)) { XmlDocument xmlDoc = new XmlDocument(); XmlElement root = xmlDoc.CreateElement("root"); XmlElement child = xmlDoc.CreateElement("enemy"); child.SetAttribute("id","1000"); child.SetAttribute("name","enemy1"); child.SetAttribute("lv","1"); XmlElement subChild1 = xmlDoc.CreateElement("attack"); subChild1.InnerText = "10"; XmlElement subChild2 = xmlDoc.CreateElement("hitpoint"); subChild2.InnerText = "100"; child.AppendChild(subChild1); child.AppendChild(subChild2); root.AppendChild(child); xmlDoc.AppendChild(root); xmlDoc.Save(filepath); }
得到的xml文件内容如下:
1 2 3 4 5 6 <root > <enemy id ="1000" name ="enemy1" lv ="1" > <attack > 10</attack > <hitpoint > 100</hitpoint > </enemy > </root >
编辑xml 在刚才的文件test1.xml中,将enemy
的lv
属性改为10
,attack
节点内容改为30
,删除enemy
的hitpoint
节点,在root
下增加一个新的enemy
节点并为其设置一些属性和子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 string filepath = Application.dataPath + @"/test1.xml"; if(File.Exists (filepath)) { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(filepath); XmlNodeList nodeList=xmlDoc.SelectSingleNode("root").ChildNodes; foreach(XmlElement xe in nodeList) { if(xe.GetAttribute("id")=="1000") { xe.SetAttribute("lv","10"); foreach(XmlElement x1 in xe.ChildNodes) { if(x1.Name=="attack") { x1.InnerText="30"; } } break; } } Debug.Log("modify end"); XmlNode root = xmlDoc.SelectSingleNode("root"); XmlElement newChild = xmlDoc.CreateElement("enemy"); newChild.SetAttribute("id","1000"); newChild.SetAttribute("name","enemy2"); newChild.SetAttribute("lv","2"); XmlElement subChild1 = xmlDoc.CreateElement("attack"); subChild1.InnerText = "20"; XmlElement subChild2 = xmlDoc.CreateElement("hitpoint"); subChild2.InnerText = "200"; newChild.AppendChild(subChild1); newChild.AppendChild(subChild2); root.AppendChild(newChild); xmlDoc.Save(filepath); Debug.Log("add node end"); }
得到的xml文件为:
1 2 3 4 5 6 7 8 9 10 <root > <enemy id ="1000" name ="enemy1" lv ="10" > <attack > 30</attack > <hitpoint > 100</hitpoint > </enemy > <enemy id ="1000" name ="enemy2" lv ="2" > <attack > 20</attack > <hitpoint > 200</hitpoint > </enemy > </root >
json序列化与反序列化 json加载与读取(反序列化) 使用的json文件如下:
1 2 3 4 5 6 7 8 9 10 { "enemyDataList" : [ { "id":"1" , "lv":"1" , "score" : "1" , "speed" : "4" , "hitPoint" : "1" , "attackInterval" : "100"}, { "id":"2" , "lv":"2" , "score" : "2" , "speed" : "4" , "hitPoint" : "2" , "attackInterval" : "100"}, { "id":"3" , "lv":"3" , "score" : "4" , "speed" : "4.5" , "hitPoint" : "5" , "attackInterval" : "10"}, { "id":"4" , "lv":"4" , "score" : "7" , "speed" : "5" , "hitPoint" : "10" , "attackInterval" : "3"}, { "id":"5" , "lv":"5" , "score" : "10" , "speed" : "5.5" , "hitPoint" : "18" , "attackInterval" : "1"}, { "id":"6" , "lv":"6" , "score" : "14" , "speed" : "6" , "hitPoint" : "30" , "attackInterval" : "0.5"} ] }
读取并加载json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 using System.Collections; using System.Collections.Generic; using UnityEngine; using System.IO; using System; public class EnemyManager : MonoBehaviour { private EnemyDataList data = new EnemyDataList(); //...略去无关代码 [Serializable] public class EnemyData { public int lv; public int score; public float speed; public int hitPoint; public float attackInterval; } [Serializable] public class EnemyDataList { public EnemyData[] enemyDataList; } private void LoadJson(){ string filepath = Application.dataPath + "/Json/Enemy.json"; if (!File.Exists(filepath)) { Debug.Log (filepath + "do not exist"); return; } StreamReader sr = new StreamReader(filepath); if (sr == null) { Debug.Log (filepath + "read failed"); return; } string json = sr.ReadToEnd(); if (json.Length > 0) { data = JsonUtility.FromJson<EnemyDataList> (json); } else { Debug.Log (filepath + "empty file"); } //Debug.Log (data.enemyDataList.Length.ToString()); } }
json序列化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [Serializable] public class EnemyData { public int lv; public int score; public float speed; public int hitPoint; public float attackInterval; } private void WriteJson() { EnemyData enemyData = new EnemyData(); enemyData.lv = 1; enemyData.score = 10; enemyData.speed = 100; enemyData.hitPoint = 100; enemyData.attackInterval = 10; string json = JsonUtility.ToJson(enemyData); Debug.Log(json); }
在游戏预览窗口调整角度和修改数据 如对于FPS游戏,可以在游戏预览窗口对武器的位置进行调整,调整好以后在游戏预览窗口将需要保存的游戏对象拖拽生成Prefab即可保存即时的状态。
在编辑窗口中,选中GameObject,使用快捷键command+shift+F(win下是 control+shift+F),可以使选中的游戏对象与当前Scene窗口对齐,可用于调整灯光、摄像机等。
扩展函数 例如,可以为Transform
定义扩展函数如下:
1 2 3 4 5 6 public static class MyExt { public static void SetPositionX (this Transform trans , float x) { trans.position = new Vector3 (x , trans.position.y , trans.position.z); } }
脚本内调用:
1 this.transform.SetPositionX(10);
Coroutine使用案例 延迟调用 指定在一段时间后指定某函数,首先有以下封装:
1 2 3 4 5 6 7 8 9 10 11 using UnityEngine; using System.Collections; using System; public class DelayToInvoke : MonoBehaviour { public static IEnumerator DelayToInvokeDo(Action action, float delaySeconds) { yield return new WaitForSeconds(delaySeconds); action(); } }
调用
1 2 3 4 5 6 7 void OnClick() { StartCoroutine(DelayToInvoke.DelayToInvokeDo(() => { Debug.Log("Delay"); }, 0.1f)); }
将一次move分解到每帧 1 2 3 4 5 6 7 8 9 10 11 12 13 public virtual void Move(){ StartCoroutine(IMove()); } protected IEnumerator IMove(){ float movedDis = 0; while(movedDis < cubeDistance){ movedDis += updateMove.magnitude * Time.deltaTime; transform.localPosition = transform.localPosition + updateMove * Time.deltaTime; yield return new WaitForFixedUpdate (); } transform.localPosition = targetPos; }
自动寻路 涉及到三个游戏对象
寻路地形
需要勾选Navigation Static
在Window-Navigation中设置地形的参数
Bake
自动寻路的对象
添加组件NavMeshAgent
在挂载的脚本中添加以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //...略去无关内容 Transform m_target; UnityEngine.AI.NavMeshAgent m_agent; float m_speed = 1.0f; void Start () { m_target = GameObject.FindGameObjectWithTag ("Target"); m_agent = GetComponent<UnityEngine.AI.NavMeshAgent> (); m_agent.SetDestination (m_target.transform.position); } void Update () { m_agent.SetDestination (m_target.transform.position); MoveTo (); } void MoveTo(){ float speed = m_speed * Time.deltaTime; m_agent.Move (m_transform.TransformDirection(new Vector3(0,0,speed))); }
寻路目标
放置于寻路地形上
控制目标移动,自动寻路对象会跟随目标
使用单例 使用单例方法一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton<T> : MonoBehaviour where T : MonoBehaviour { protected static T instance; public static T Instance { get { if(instance == null) { instance = (T) FindObjectOfType(typeof(T)); if (instance == null) { Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none."); } } return instance; } } }
定义的单例类需要继承Singleton
。
使用单例方法二 1 2 3 4 5 6 7 8 9 10 11 12 public static ClassSingleton instance { get; private set; } ... protected void OnEnable() { if (ClassSingleton.instance == null) { ClassSingleton.instance = this; } #if UNITY_EDITOR else { Debug.LogWarning("Multiple ClassSingleton in scene... this is not supported"); } #endif }
2D碰撞 碰撞二者必须都要有BoxCollider2D组件,并且其中之一要有Rigidbody2D组件。
组件之一需要在挂载的脚本中覆写OnTriggerEnter2D
方法。
1 2 3 4 5 6 void OnTriggerEnter2D(Collider2D other){ if (other.gameObject.tag == "enemy") { Destroy (this.gameObject); other.gameObject.GetComponent<Enemy>().hit (this.damage); } }
FPS游戏玩家移动和视角转动 玩家对象需要挂载组件CharacterController
,以及以下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { public Transform m_transform; CharacterController m_cc; float m_speed = 5.0f; Transform camTransform; Vector3 camRot; float camHeight = 5.5f; void Start () { m_transform = this.transform; m_cc = this.GetComponent<CharacterController> (); camTransform = Camera.main.transform; Vector3 pos = m_transform.position ; pos.y += camHeight; camTransform.position = pos; camTransform.rotation = m_transform.rotation; camRot = camTransform.eulerAngles; Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } void Update () { ControlCheck (); } private void ControlCheck(){ //cam rotation float rh = Input.GetAxis ("Mouse X"); float rv = Input.GetAxis ("Mouse Y"); camRot.x -= rv; camRot.y += rh; camTransform.eulerAngles = camRot; // keep player the same rotation Vector3 camR = camTransform.eulerAngles; camR.x = 0; camR.z = 0; m_transform.eulerAngles = camR; // move player with WASD float speed_x = 0, speed_y = 0, speed_z = 0; if (Input.GetKey (KeyCode.W)) { speed_z += m_speed * Time.deltaTime; } else if(Input.GetKey (KeyCode.S)){ speed_z -= m_speed * Time.deltaTime; } if (Input.GetKey (KeyCode.A)) { speed_x -= m_speed * Time.deltaTime; } else if(Input.GetKey (KeyCode.D)){ speed_x += m_speed * Time.deltaTime; } m_cc.Move (m_transform.TransformDirection(new Vector3(speed_x , speed_y , speed_z))); // keep camera the same postion Vector3 pos = m_transform.position; pos.y = camHeight; camTransform.position = pos; } }
获取点击或触摸操作 点击或触摸拖动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 //... bool isMouseDown = false; void Update () { mouseControl (); } void mouseControl (){ if (Input.GetMouseButtonDown (0)) { isMouseDown = true; } if (Input.GetMouseButtonUp (0)) { isMouseDown = false; lastMousePosition = Vector3.zero; } if(isMouseDown){ if (Vector3.zero != lastMousePosition) { Vector3 offset = Camera.main.ScreenToWorldPoint (Input.mousePosition) - lastMousePosition; this.transform.position += offset; } lastMousePosition = Camera.main.ScreenToWorldPoint (Input.mousePosition); } }
获取点击或触摸点在世界中的位置并移动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class PlaceTargetWithMouse : MonoBehaviour { public float surfaceOffset = 1.5f; public GameObject setTargetOn; private void Update() { if (!Input.GetMouseButtonDown(0)) { return; } Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit)) { return; } transform.position = hit.point + hit.normal*surfaceOffset; //if (setTargetOn != null) //{ // setTargetOn.SendMessage("SetTarget", transform); //} } }
REFERENCE https://docs.unity3d.com/500/Documentation/Manual/index.html https://docs.unity3d.com/500/Documentation/ScriptReference/index.html https://docs.unity3d.com/ScriptReference/Transform.html http://www.xuanyusong.com/archives/1901 http://www.xuanyusong.com/archives/3763 http://blog.csdn.net/stalendp/article/details/46707079 http://blog.csdn.net/stalendp/article/details/17114135 http://blog.csdn.net/neil3d/article/details/38534809