这是一个关于Unity内部脚本如何工作的简单概览。
Unity内部的脚本,是通过附加自定义脚本对象到游戏物体组成的。在脚本对象内部不同志的函数被特定的事件调用。最常用的列在下面:
Update:这个函数在渲染一帧之前被调用,这里是大部分游戏行为代码被执行的地方,除了物理代码。
FixedUpdate:这个函数在每个物理时间步被调用一次,这是处理基于物理游戏的地方。
在任何函数之外的代码:
在任何函数之外的代码在物体被加载的时候运行,这个可以用来初始化脚本状态。
注意:文档的这个部份假设你是用Javascript,参考用C#编写获取如何使用C#和Boo编写脚本的信息。
你也能定义事件句柄,它们的名称都以On开始,(例如OnCollisionEnter),为了查看完整的预定义事件的列表,参考MonoBehaviour 文档。
常用操作
大多数游戏物体的操作是通过游戏物体的Transform或Rigidbody来做的,在行为脚本内部它们可以分别通过transform和rigidbody访问,因此如果你想绕着Y轴每帧旋转5度,你可以如下写:
function Update(){
transform.Rotate(0,5,0);
}
如果你想向前移动一个物体,你应该如下写:
function Update(){
transform.Translate(0,0,2);
}
跟踪时间
Time类包含了一个非常重要的类变量,称为deltaTime,这个变量包含从上一次调用Update或FixedUpdate(根据你是在Update函数还是在FixedUpdate函数中)到现在的时间量。
所以对于上面的例子,修改它使这个物体以一个恒定的速度旋转而不依赖于帧率:
function Update(){
transform.Rotate(0,5*Time.deltaTime,0);
}
移动物体:
function Update(){
transform. Translate (0, ,0,2*Time.deltaTime);
}
如果你加或是减一个每帧改变的值,你应该将它与Time.deltaTime相乘。当你乘以Time.deltaTime时,你实际的表达:我想以10米/秒移动这个物体不是10米/帧。这不仅仅是因为你的游戏将独立于帧而运行,同时也是因为运动的单位容易理解。( 米/秒)
另一个例子,如果你想随着时间增加光照的范围。下面的表达式,以2单位/秒改变半径。
function Update (){
light.range += 2.0 * Time.deltaTime;
}
当通过力处理刚体的时候,你通常不必用Time.deltaTime,因为引擎已经为你考虑到了这一点。
访问其他组件
组件被附加到游戏物体,附加Renderer到游戏物体使它在场景中渲染,附加一个Camera使它变为相机物体,所有的脚本都是组件,因为它们能被附加到游戏物体。
最常用的组件可以作为简单成员变量访问:
Component 可如下访问
Transform transform
Rigidbody rigidbody
Renderer renderer
Camera camera (only on camera objects)
Light light (only on light objects)
Animation animation
Collider collider
…等等。
对于完整的预定义成员变量的列表。查看Component,Behaviour和MonnoBehaviour类文档。如果游戏物体没有你想取的相同类型的组件,上面的变量将被设置为null。
任何附加到一个游戏物体的组件或脚本都可以通过GetComponent访问。
transform.Translate(0,3,0); //等同于
GetComponent(Transform).Translate(0, 1, 0);
注意transfom和Transform之间大小写的区别,前者是变量(小写),后者是类或脚本名称(大写)。大小写不同使你能够从类和脚本名中区分变量。
应用我们所学,你可以使用GetComponent找到任何附加在同一游戏物体上的脚本和组件,请注意要使用下面的例子能够工作,你需要有一个名为OtherScript的脚本,其中包含一个DoSomething函数。OtherScript脚本必须与下面的脚本附加到相同的物体上。
function Update(){
otherScript = GetComponent(OtherScript); //这个在同一游戏物体桑找到名为OtherScript的脚本
otherScript.DoSomething(); //并调用它上加的DoSomething
}
访问其它游戏物体
大多数高级的代码不仅需要操作一个物体,Unity脚本接口有各种方法来找到并访问其他游戏物体和组件。在下面,我们假定有个一名为OtherScript,js的脚本附加到场景的游戏物体上。
var foo = 5;
function DoSomething ( param : String) {
print(param + " with foo: " + foo);
}
1.通过检视面板赋值引用
你可以通过检视面板赋值变量到任何物体
var target : Transform;
function Update ()
{
target.Translate(0, 1, 0);//变换拖动到target的物体
}
你也可以在检视面板中公开到其他物体的引用,下面你可以拖动一个包含的游戏物体到检视面板中的target槽。设置在检视面板中赋值的target变量上的foo,调用DoSomething。
var target : OtherScript;
function Update ()
{
target.foo = 2;//设置target物体的foo变量
target.DoSomething("Hello");// 调用target上的DoSomething
}
2.通过物体层次定位
对于一个已经存在的物体,可以通过游戏物体的Transform组件来找到子和父物体;
transform.Find("Hand").Translate(0, 1, 0);、、找到脚本所附加的游戏物体的子“Hand”
一旦在层次视图中找到这个变换,你可以使用GetComponent来获取其他脚本,
transform.Find("Hand").Translate(0, 1, 0); //找到名为“Hand”的子 然后应用一个力到附加在hand上的刚体
transform.Find("Hand").GetComponent(OtherScript).DoSomething("Hello"); //在附加到它上面的OtherScript中,设置foo为2;
transform.Find("Hand").rigidbody.AddForce(0, 10, 0);// //变换的所有子向上移动10个单位
你可以循环所有的子,
for (var child : Transform in transform)
{
child.Translate(0, 1, 0);
}
参考Transform类文档获取更多信息。
3.根据名称或标签定位.
你可以使用GameObject.FindWithTag和GameObject.FindGameObjectsWithTag搜索具有特定标签的游戏物体,使用GameObject.Find根据名称查找物体。
function Start ()
{
var go = GameObject.Find("SomeGuy");// 按照名称
go.transform.Translate(0, 1, 0);
var player = GameObject.FindWithTag("Player");// 按照标签
player.transform.Translate(0, 1, 0);
}
你可以在结果上使用GetComponent,在找到的游戏物体上得到任何脚本或组件。
function Start ()
{
var go = GameObject.Find("SomeGuy");// 按名称
go.GetComponent(OtherScript).DoSomething();
var player = GameObject.FindWithTag("Player");// 按标签
player.GetComponent(OtherScript).DoSomething();
}
一些特殊的物体有快捷方式,如主相机使用Camera.main。
4.作为参数传递
一些事件消息在事件包含详细信息。例如,触发器事件传递碰撞物体的Collider组件到处理函数。
OnTriggerStay给我们一个到碰撞器的引用。从这个碰撞器我们可以获取附加到其上的刚体。
function OnTriggerStay( other : Collider ) {
if (other.rigidbody) {// 如果另一个碰撞器也有一个刚体
other.rigidbody.AddForce(0, 2, 0);// 应用一个力到它上面
}
}
或者我们可以通过碰撞器获取附加在同一个物体上的任何组件。
function OnTriggerStay( other : Collider ) {
if (other.GetComponent(OtherScript)) {// 如果另一个碰撞器附加了OtherScript
other.GetComponent(OtherScript).DoSomething();// 调用它上面的DoSomething
// 大多数时候碰撞器不会附加脚本
// 所以我们需要首先检查以避免null引用异常
}
}
注意通过上述例子中的other变量,你可以访问碰撞物体中的任何组件。
5.一种类型的所有脚本
使用Object.FindObjectsOfType找到所有具有相同类或脚本名称的物体,或者使用Object.FindObjectOfType.找到这个类型的第一个物体。
function Start ()
{
var other : OtherScript = FindObjectOfType(OtherScript); // 找到场景中附加了OtherScript的任意一个游戏物体
other.DoSomething();
}
向量
Unity使用Vector3类同一表示全体3D向量,3D向量的不同组件可以通过想x,y和z成员变量访问。
var aPosition : Vector3;
aPosition.x = 1;
aPosition.y = 1;
aPosition.z = 1;
你也能够使用Vector3构造函数来同时初始化所有组件。
var aPosition = Vector3(1, 1, 1);
Vector3也定义了一些常用的变量值。
var direction = Vector3.up; // 与 Vector3(0, 1, 0);相同
单个向量上的操作可以使用下面的方式访问:
someVector.Normalize();
使用多个向量的操作可以使用Vector3类的数;
theDistance = Vector3.Distance(oneVector, otherVector);
(注意你必须在函数名之前写Vector3来告诉JavaScript在哪里找到这个函数,这适用于所有类函数)
你也可以使用普通数学操作来操纵向量。
combined = vector1 + vector2;
查看Vector3类文档获取完整操纵和可用属性的列表。
成员变量 & 全局变量
定义在任何函数之外的变量是一个成员变量。在Unity中这个变量可以通过检视面板来访问,任何保存在成员变量中的值也可以自动随工程保存。
var memberVariable = 0.0;
上面的变量将在检视面板中显示为名为"Member Variable"的数值属性。
如果你设置变量的类型为一个组件类型(例如Transform, Rigidbody, Collider,任何脚本名称,等等)然后你可以在检视面板中通过拖动一个游戏物体来设置它们。
var enemy : Transform;
function Update()
{
if ( Vector3.Distance( enemy.position, transform.position ) < 10 );
print("I sense the enemy is near!");
}
}
你也可以创建私有成员变量。私有成员变量可以用来存储那些在该脚本之外不可见的状态。私有成员变量不会被保存到磁盘并且在检视面板中不能编辑。当它被设置为调试模式时,它们在检视面板中可见。这允许你就像一个实时更新的调试器一样使用私有变量。
private var lastCollider : Collider;
function OnCollisionEnter( collisionInfo : Collision ) {
lastCollider = collisionInfo.other;
}
全局变量
你也可以使用static关键字创建全局变量,这创造了一个全局变量,名为someGlobal
static var someGlobal = 5;// 你可以在脚本内部像普通变量一样访问它
print(someGlobal);// 'TheScriptName.js'中的一个静态变量
someGlobal = 1;
为了从另一个脚本访问它,你需要使用这个脚本的名称加上一个点和全局变量名。
print(TheScriptName.someGlobal);
TheScriptName.someGlobal = 10;
实例化
实例化,复制一个物体。包含所有附加的脚本和整个层次。它以你期望的方式保持引用。到外部物体引用的克隆层次将保持完好,在克隆层次上到物体的引用映射到克隆物体。
实例化是难以置信的快和非常有用的。因为最大化地使用它是必要的。例如, 这里是一个小的脚本,当附加到一个带有碰撞器的刚体上时将销毁它自己并实例化一个爆炸物体。
var explosion : Transform;
// 当碰撞发生时销毁我们自己
// 并生成给一个爆炸预设
function OnCollisionEnter (){
Destroy (gameObject);
var theClonedExplosion : Transform;
theClonedExplosion = Instantiate(explosion, transform.position, transform.rotation);
}
实例化通常与预设一起使用
Coroutines & Yield
在编写游戏代码的时候,常常需要处理一系列事件。这可能导致像下面的代码。
private var state = 0;
function Update()
{
if (state == 0) {
state = 1;// 做步骤0
return;
}
if (state == 1) {
state = 2;// 做步骤1
return;
}
// …
}
更方便的是使用yield语句。yield语句是一个特殊类型的返回,这个确保在下次调用时该函数继续从该yield语句之后执行。
while(***e) {
yield; //等待一帧
// 做步骤2
yield; //等待一帧
// 做步骤1
// ...
}
你也可以传递特定值给yield语句来延迟Update函数的执行,直到一个特定的事件发生。
// 做一些事情
yield WaitForSeconds(5.0); //等待5秒
//做更多事情…
可以叠加和连接coroutines。
这个例子执行Do,在调用之后立即继续。
Do ();
print ("This is printed immediately");
function Do ()
{
print("Do now");
yield WaitForSeconds (2);
print("Do 2 seconds later");
}
这个例子将执行Do并等待直到它完成,才继续执行自己。
yield StartCoroutine("Do");//链接coroutine
print("Also after 2 seconds");
print ("This is after the Do coroutine has finished***cution");
function Do ()
{
print("Do now");
yield WaitForSeconds (2);
print("Do 2 seconds later");
}
任何事件处理句柄都可以是一个coroutine ,注意你不能在Update或FixedUpdate内使用yield,但是你可以使用StartCoroutine来开始一个函数。
参考YieldIns***ction, WaitForSeconds, WaitForFixedUpdate, Coroutine and MonoBehaviour.StartCoroutine获取更多使用yield的信息。
用C#编写脚本
除了语法,使用C#或者Boo编写脚本还有一些不同。最需要注意的是:
1.从MonoBehaviour继承
所有的行为脚本必须从MonoBehaviour继承(直接或间接)。在Javascript中这自动完成,但是必须在C#或Boo脚本中显示申明。如果你在Unity内部使用Asset -> Create -> C Sharp/Boo Script菜单创建脚本,创建模板已经包含了必需的定义。
public class NewBehaviourScript : MonoBehaviour {...} // C#
class NewBehaviourScript (MonoBehaviour): ... # Boo
2.使用Awake或Start函数来初始化
Javascript中放置在函数之外的代码,在C#或Boo中要放置在Awake或Start中。
Awake和Start的不同是Awake在场景被加载时候运行,而Start在第一次调用Update或FixedUpdate函数之前被调用,所有Awake函数在任何Start函数调用之前被调用。
3.类名必须与文件名相同
Javascript中,类名被隐式地设置为脚本的文件名(不包含文件扩展名)。在c#和Boo中必须手工做。
4.在C#中Coroutines有不同语法。
Coroutines必有一个IEnumerator返回类型,并且yield使用yield return… 而不是yield…
using System.Collections;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour {
// C# coroutine
IEnumerator SomeCoroutine ()
{
yield return 0;// 等一帧
yield return new WaitForSeconds (2);//等两秒
}
}
5.不要使用命名空间
目前Unity还不支持将代码放置在一个命名空间中,这个需要将会出在未来的版本中。
6.只有序列化的成员变量会显示在检视面板中
私有和保护成员变量只在专家模式中显示,属性不被序列化或显示在检视面板中。
7.避免使用构造函数
不要在构造函数中初始化任何变量,使用Awake或Start实现这个目的。即使是在编辑模式中Unity也自动调用构造函数,这通常发生在一个脚本被编译之后,因为需要调用构造函数来取向一个脚本的默认值。构造函数不仅会在无法预料的时刻被调用,它也会为预设或未激活的游戏物体调用。
单件模式使用构造函数可能会导致严重的后果,带来类似随机null引用异常。
因此如果你想实现,如,一个单件模式,不要使用构造函数,而是使用Awake。其实上,没有理由一定要在继续自MononBehaviour类的构造函数中写任何代码。
最重要的类
Javascript中可访问的全局函数或C#中的基类
移动/旋转物体
动画系统
刚体
FPS或第二人称角色控制器
性能优化
1.使用静态类型
在使用Javascript时最重要的优化是使用静态类型而不是动态类型,Unity使用一种叫做类型推理的技术来自自动转换Javascript为静态类型编码而不需要你做任何工作。
var foo=5;
在上面的例子里foo会自动被推断为一个整型值。因此,Unity可能使用大量的编译时间来优化。而不使用耗时的动态名称变量查找等。这就是为什么Unity比其他在JavaScript的实现平均快20倍的原因之一。
唯一的问题是,有时并非一切都可以做类型推断。Unity将会为这些变量重新使用动态类型。通过回到动态类型,编写JavaScript代码很简单。但是这也使得代码运行速度较慢。
让我们看一些例子:
function Start ()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
这里foo将是动态类型,因此调用DoSomething函数将使用较长时间,因为foo的类型是未知的,它必须找出它是否支持DoSomething函数,如果支持,调用它。
function Start ()
{
var foo : MyScript = GetComponent(MyScript);
foo.DoSomething();
}
这里我们强制foo为指定类型,你将获得更好的性能。
2.使用#pragma strict
当然现在问题是,你通常没有意识到你在使用动态类型。#pragma strict解决了这个!简单的在脚本顶部添加#pragma strict。然后,unity将在脚本中禁用动态类型,强制使用静态类型,如果一个类型未知。Unity将报告编译错误。那么在这种情况下foo将在编译时产生一个错误:
#pragma strict
function Start ()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
3.缓存组件查找
另一个优化是组件缓存。不幸的是该优化需要一点编码,并且不一定是值得的,但是如果你的脚本是真的用了很长时间了,你需要把最后一点表现出来,这是一个很好的优化。
当你访问一个组件通过GetComponent或访问变量,Unity会通过游戏对象找到正确的组件。这一次可以很容易地通过缓存保存在一个私有变量里引用该组件。
简单地把这个:
function Update ()
{
transform.Translate(0, 0, 5);
}
变成:
private var myTransform : Transform;
function Awake ()
{
myTransform = transform;
}
function Update ()
{
myTransform.Translate(0, 0, 5);
}
后者的代码将运行快得多,因为Unity没有找到变换在每一帧游戏组件中的对象。这同样适用于脚本组件,在你使用GetComponent代替变换或者其它的东西。
4.使用内置数组
内置数组的速度非常快,所以请使用它们。
而整列或者数组类更容易使用,因为你可以很容易地添加元素,他们几乎没有相同的速度。内置数组有一个固定的尺寸,但大多数时候你事先知道了最大的大小在可以只填写了以后。关于内置数组最好的事情是,他们直接嵌入在一个结构紧凑的缓冲区的数据类型没有任何额外的类型信息或其他开销。因此,遍历是非常容易的,作为一切缓存在内存中的线性关系。
private var positions : Vector3[];
function Awake ()
{
positions = new Vector3[100];
for (var i=0;i<100;i++)
positions[i] = Vector3.zero;
}
5.如果你不需要就不要调用函数
最简单的和所有优化最好的是少工作量的执行。例如,当一个敌人很远最完美的时间就是敌人入睡时可以接受。直到玩家靠近时什么都没有做。这是种缓慢的处理方式的情况:
function Update ()
{
if (Vector3.Distance(transform.position, target.position) > 100)// 早期进行如果玩家实在是太遥远。
return;
perform real work work...
}
这不是一个好主意,因为Unity必须调用更新功能,而你正在执行工作的每一个帧。一个比较好的解决办法是禁用行为直到玩家靠近。有3种方法来做到这一点:
1.使用OnBecameVisible和OnBecameInvisible。这些回调都是绑到渲染系统的。只要任何相机可以看到物体,OnBecameVisible将被调用,当没有相机看到任何一个,OnBecameInvisible将被调用。这种方法在很多情况下非常有用,但通常在AI中并不是特别有用,因为只要你把相机离开他们敌人将不可用。
function OnBecameVisible () {
enabled = ***e;
}
function OnBecameInvisible ()
{
enabled = false;
}
2.使用触发器。一个简单的球形触发器会工作的非常好。一旦你离开这个影响球你将得到OnTriggerEnter/Exit调用。
function OnTriggerEnter (c : Collider)
{
if (c.CompareTag("Player"))
enabled = ***e;
}
function OnTriggerExit (c : Collider)
{
if (c.CompareTag("Player"))
enabled = false;
}
3.使用协同程序。Update调用的问题是它们每帧中都发生。很可能会只需要每5秒检检查一次到玩家的距离。这应该会节省大量的处理周期。
脚本编译(高级)
Unity编译所有的脚本为.NET dll文件,.dll将在运行时编译执行。
这允许脚本以惊人的速度执行。这比传统的javascript快约20倍。比原始的C++代码慢大约50%。在保存的时候,Unity将花费一点时间来编译所有脚本,如果Unity还在编译。你可以在Unity主窗口的右下角看到一个小的旋转进度图标。
脚本编译在4个步骤中执行:
1.所有在"Standard Assets", "Pro Standard Assets" 或 "Plugins"的脚本被首先编译。
在这些文件夹之内的脚本不能直接访问这些文件夹之外脚本。不能直接引用或它的 变量,但是可以使用GameObject.SentMessage与它们通信。
2.所有在"Standard Assets/Editor", "Pro Standard Assets/Editor" 或 "Plugins/Editor"的脚本被首先编译。
如果你想使用UnityEditor命名空间你必须放置你的脚本在这些文件夹中,例如添加菜单项或自定义的向导,你都需要放置脚本到这些文件夹。这些脚本可以访问前一组中的脚本。
3.然后所有在"Editor"中的脚本被编译。
如果你想使用UnityEditor命名空间你必须放置你的脚本在这些文件夹中。例如添加菜单单项或自定义的向导,你都需要放置脚本到这些文件夹。这些脚本可以访问所有前面组中的脚本,然而它们不能访问后面组中的脚本。这可能会是一个问题,当编写编辑器代码编辑那些在后面组中的脚本时。有两个解决方法:1、移动其他脚本到"Plugins"文件夹 2、利用JavaScript的动态类型,在javascript中你不需要知道类的类型。在使用GetComponent时你可以使用字符串而不是类型。你也可以使用SendMessage,它使用一个字符串。
4.所有其他的脚本被最后编译
所有那些没有在上面文件夹中的脚本被最后编译。所有在这里编译的脚本可以访问第一个组中的所有脚本("Standard Assets","ProStandard Assets" or "Plugins")。这允许你让不同的脚本语言互操作。例如,如果你想创建一个JavaScript。它使用一个C#脚本;放置C#脚本到"Standard Assets"文件夹并且JavaScript放置在"Standard Assets"文件夹之外。现在JavaScript可以直接引用c#脚本。放置在第一个组中的脚本,将需要较长的编译时间,因为当他们被编译后,第三组需要被重新编译。因此如果你想减少编译时间,移动那些不常改变的到第一组。经常改变的到第四组。