Unity 3D 脚本参考
2012-04-25 15:25
423 查看
第一章 概述
Unity中的脚本(Script)由附加到游戏对象(GameObject)的自定义脚本对象(Custom
Script Object)组成,它们又被称为行为。脚本对象中各种函数被称为必然事件(Certain
Event)。常用的必然事件有如下三个:
1、Update:该函数在渲染帧之前被调用,大部分的游戏行为代码都在这里执行,除了
物理代码。
2、FixedUpdate:该函数在每进行一次物理时间步调时被调用,它用来执行基于物理的
游戏行为。
3、Code outside any function:这类函数在对象加载时被调用,它们用来执行脚本状态
的初始化工作。
你可以自定义事件处理器(Event Handler),它们都以“On”前缀进行命名,例如:
OnCollisionEnter。
第二章 常用操作
大部分游戏对象的操作都是通过它们的Transform和Rigidbody实例来实现的,我们可
以在脚本中直接通过成员变量transform和rigidbody来访问这两个实例。例如:要实现游戏
对象每帧以Y轴旋转5度的效果,可以进行如下编码:
function Update()
{
transform.Rotate(0, 5, 0);
}
让游戏对象向你移动,可进行如下编码:
function Update()
{
transform.Translate(0, 0, 2);
}
第三章 时间
Time类包含了一个重要的类变量deltaTime,它表示距上一次调用Update或FixedUpdate
所用的时间。
因此通过它可以让游戏对象按照一个常速进行旋转,而不是依赖于它的帧频:
function Update()
{
tranform.Rotate(0, 5 * Time.deltaTime, 0);
}
同样地移动效果:
function Update()
{
transform.Translate(0, 0, 2 * Time.deltaTime);
}
如果想要一个值根据每帧的变化而变化(增加或减少),你应该使用Time.deltaTime 来
乘以这个值。这样才能使得变化的效果依赖于单位时间,而不是帧频。这不仅使得游戏的运
行独立于帧频,也使得运动的效果符合现实。
同理,要让灯光的照射范围在每秒使半径增加2个单位,可进行如下编码:
function Update()
{
light.range += 2.0 * Time.deltaTime;
}
但是在通过force来处理rigidbody时,一般情况下不要乘以Time.deltaTime,因为Unity
引擎已经为你进行了处理。
第四章 访问组件
组件被用来附加到游戏对象。当附加一个渲染器组件(Renderer)时,将使得游戏对象
被渲染到屏幕上;当附加一个摄像机组件(Camera)时,将使得游戏对象变为摄像机对象。
所有的脚本都是组件,因此它们可以被附加到游戏对象中。
常用的组件都可以通过一个简单的成员变量来进行访问:
组件 用于访问相应组件的成员变量
Transform transform
Rigidbody rigidbody
Renderer renderer
Camera camera(只能用于摄像机对象)
Light light(只能用于灯光对象)
Animation animation
Collider collider
如果游戏对象没有你想要获取的组件类型,上面的变量将被设置为null。
另外,也可以通过GetComponent函数来获取被附加到游戏对象中的任意内置组件或脚
本。例如:
transfrom.Translate(0, 1, 0);
//等同于
GetComponent(Transfrom).Translate(0, 1, 0);
两者之间不同在于前者(transform等)是一个变量,而后者(Transform等)是一个类
或脚本名。
现在我们进行一个练习,以便更加深入地掌握GetComponent函数。这个游戏对象附加
了两个脚本,我们在其中一个脚本中通过GetComponent 函数来获取另一个脚本(叫做
OtherScript),并访问它的DoSomething函数:
function Update()
{
//在当前游戏对象中查找名为OtherScript的脚本,并调用它的DoSomething函数。
otherScript = GetComponent(OtherScript);
otherScript.DoSomething();
}
第五章 访问游戏对象
大多数高级的游戏代码都不只是操作单个游戏对象。Unity脚本接口拥有多种方式来查
找和访问其他游戏对象或其中的组件。假设有一个名为OtherScript 的脚本被附加到游戏对
象中,该脚本包含了如下的代码片段:
var foo = 5;
function DoSomething(param : String)
{
print(param + " with foo: " + foo);
}
1、通过检查器进行访问
你可以通过检查器来为任意对象类型的变量分配值:
//需要将Translate拖拽到target变量上
var target : Transform;
function Update()
{
target.Translate(0, 1, 0);
}
你也可以把指向其他对象的引用暴露在检查器中。例如:你可以拖拽一个附加了
OtherScript脚本的游戏对象到检查器中的target变量中:
//可以在检查器中通过target变量来设置foo变量
var target : OtherScript;
function Update()
{
//设置foo变量
target.foo = 2;
//调用DoSomething函数
target.DoSomething("Hello");
}
2、通过对象层次进行访问
你可以通过一个游戏对象中的Transform组件来查找该对象的子对象和父对象:
//查找游戏对象中名为“Hand”的子对象
//我们将该脚本附加到当前游戏对象中
transform.Find("Hand").Translate(0, 1, 0);
一旦找到了层次中的transform 对象,你就可以通过使用GetComponent函数来获取其
他脚本:
//查找名为“Hand”的子对象,获取其中附加的OtherScript脚本,并设置foo变量为2。
transform.Find("Hand").GetComponent(OtherScript).foo=2;
//查找名为“Hand”的子对象,获取其中附加的OtherScript脚本,并调用DoSomething函数
transform.Find("Hand").GetComponent(OtherScript).DoSomething("Hello");
//查找名为“Hand”的子对象,获取其中附加的rigidbody对象,并申请一个force。
transform.Find("Hand").rigidbody.AddForce(0, 10, 0);
使用相同的原理,可以通过如下的代码来循环访问所有的子对象:
//将所有子对象都向上移动10个单位
for(var child : Transform in transform)
{
child.Translate(0, 1, 0);
}
3、通过名称或Tag进行访问
可以通过特 定的Tag 来 调用GameObject.FindWithTag 或
GameObject.FindGameObjectsWithTag函数,从而在所有的游戏对象中查找想要的游戏对象。
也可以通过游戏对象的名称来调用GameObject.Find函数来进行查找。
function Start()
{
//通过名称查找
var go = GameObject.Find("SomeGuy");
go.transform.Translate(0, 1, 0);
//通过tag查找
var player = GameObject.FindWithTag("Player");
player.transform.Translate(0, 1, 0);
}
在查找到的游戏对象上调用GetComponent函数,可以获取该游戏对象中所附加的内置
组件或脚本:
function Start()
{
//通过名称查找
var go = GameObject.Find("SomeGuy");
go.GetComponent(OtherScript).DoSomething();
//通过tag查找
var player = GameObject.FindWithTag("Player");
player.GetComponent(OtherScript).DoSomething();
}
一些特殊的对象都拥有一个访问自己的快捷方式,例如:可以通过Camera.main变量来
代表主摄像机。
4、传递参数
一些事件消息都包含了详细的信息,例如:触发器事件会把碰撞对象的碰撞器组件
(Collider)传递给事件处理器函数。
OnTriggerStay函数提供了指向一个碰撞器对象的引用,通过它我们可以获取其中附加的
rigidbody:
function OnTriggerStay(other : Collider)
{
//如果other碰撞器对象拥有rigidbody,就申请一个force。
if(other.rigidbody)
{
other.rigidbody.AddForce(0, 2, 0);
}
}
使用相同的原理,也可以获取碰撞器游戏对象中附加的内置组件或脚本:
function OnTriggerStay(other : Collider)
{
//如果other碰撞器对象拥有OtherScript脚本,就调用它的DoSomething函数。
//大多数情况下,碰撞器都不会附加脚本。因此,我们需要先进行检查,以避免null引用异常。
if(other.GetComponent(OtherScript))
{
other.GetComponent(OtherScript).DoSomething();
}
}
5、查找同类型脚本的所有对象
通过类名或脚本名调用Object.FindObjectOfType或Object.FindObjectsOfType函数,可以
在场景中的所有游戏对象中获取一个或多个同类型的类对象或脚本对象:
function Start()
{
//场景中查找附加了OtherScript脚本的任意一个游戏对象
var other : OtherScript = FindObjectOfType(OtherScript);
other.DoSomething();
}
第六章 向量
Unity使用Vector3类来表示所有的3D向量,可以通过x、y和z成员变量来访问一个
3D向量对象的各个组件:
var aPosition : Vector3;
aPosition.x = 1;
aPosition.y = 1;
aPosition.z = 1;
也可以使用Vector3的构造函数来一次性初始化所有的组件:
var aPosition = Vector3(1, 1, 1);
Vector3也定义了一些常量,用来表示常用的3D向量对象:
//等同于Vector3(0, 1, 0);
var direction = Vector3.up;
可以通过如下代码片段,来访问单个3D向量对象:
someVector.Normalize();
可以通过如下代码片段,来访问多个3D向量对象:
theDistance = Vector3.Distance(oneVector, otherVector);
注:Distance函数为类函数,所以在调用时必须在前面加上Vector3.。
也可以在多个3D向量对象之间使用常用的数学操作符:
combined = vector1 + vector2;
第七章 成员变量和全局变量
在函数外面定义的变量叫做成员变量,它们能够通过Unity的检查器进行访问,存储在
成员变量中的值将自动地保存在项目中。
var memeberVariable = 0.0;
上面的变量将作为一个叫做“Memeber Variable”的数字属性出现在检查器中。
如果设置变量的类型为组件类型(Transform、Rigidbody、Collider和脚本名等),可以通
过把游戏对象拖拽到检查器中这种类型的变量上来设置它们。
var enemy : Transform;
function Update()
{
if( Vecter3.Distance(enemy.postion, transform.position) < 10 )
{
print("I sense the enemy is near!");
}
}
你也可以创建私有成员变量,它们主要用来存储状态信息,而且在脚本外具有不可见性。
私有成员变量不保存在磁盘上,也不能在检查器中进行编辑。只有在检查器被设置为调试模
式时,才允许你通过修改私有成员变量来实时地更新调试器。
private var lastCollider : Collider;
function OnCollisionEnter(collisionInfo : Collision)
{
lastCollider = collisionInfo.other;
}
使用static关键字进行声明的变量叫做全局变量,例如:
//一个名为“TheScriptName”的脚本中拥有一个someGlobal静态变量
static var someGlobal = 5;
//你可以在脚本中像使用普通变量一样来使用它
print(someGlobal);
someGlobal = 1;
为了能在其他脚本中访问全局变量,需要加上“TheScriptName.”前缀:
print(TheScriptName.someGlobal);
TheScriptName.someGlobal = 10;
第八章 实例化
实例化表示复制一个对象,包括所有附加的脚本和整个层次结构。它并不理会指向克隆
层次结构外的对象的引用,而指向克隆层次结构内的对象的引用将会被映射到克隆的对象
上。
下面的脚本被附加到一个具有碰撞器的rigidbody上,当发生碰撞时,将摧毁本身并替
代为大量的爆炸物对象:
var explosion : Transform;
//当碰撞发生时,将摧毁自身并产生大量的预置爆炸物对象。
function OnCollisionEnter()
{
Destroy(gameObject);
var theClonedExplosion : Transform;
theClonedExplosion = Instantiate(explosion, transform.position, transform.rotation);
}
实例化通常用于与预置对象(Prefabs)进行交互。
第九章 协同程序和让步
在编写游戏代码时,通常需要在脚本中结束一系列的事件。例如:
private var state = 0;
function Update()
{
if(state==0)
{
//do step 0
state = 1;
return;
}
if(state==1)
{
//do step 1
state = 2;
return;
}
...
}
这时,我们使用让步(yield)语句是非常方便的,yield 语句是一种特殊的return语句。
它确保函数在下次被调用时,能够从yield语句开始继续执行。
while(true)
{
//do step 0
//wait for one frame
yield;
//do step 1
//wait for one frame
yield;
...
}
你也可以传递一个特定的值给yield语句来推迟Update函数的执行,直到某个事件发生:
//do something
//wait for 5 seconds
yield WaitForSeconds(5.0);
//do something more...
你可以堆积并连接一些协同程序(Coroutine)。例如,下面的示例在执行Do函数时,
立即执行随后的代码:
Do();
print("This is printed immediately");
function Do()
{
print("Do now");
yield WaitForSeconds(2);
print("Do 2 seconds later");
}
下面的示例在执行完Do函数后,才执行随后的代码:
// chain the coroutine
yield StartCoroutine("Do");
print("Also after 2 seconds");
print("This is after the Do coroutine has finished execution");
function Do()
{
print("Do now");
yield WaitForSeconds(2);
print("Do 2 seconds later");
}
注:不能在Update 或FixedUpdate 函数中使用yield 语句,但可以在它们中使用
StartCoroutine函数来调用一个函数。
第十章 使用C#编写脚本
在Unity中可以使用JavaScript、C#和Boo来编写脚本。在使用C#编写脚本时,需注意
以下几个特点:
1、所有脚本都继承至MonoBehaviour
所有的行为脚本都必须直接地或间接地继承至MonoBehaviour。在使用JavaScript 时,
这种继承关系将自动生成,但在使用C#编写脚本时,必须显式地进行定义。通过模板创建
的C#脚本时(Asset->Create->CSharp Script),自动生成的代码已经包含了这样的定义:
//C#
public class NewBehaviourScript : MonoBehaviour
{
...
}
2、使用Awake或Start方法来进行初始化
在使用JavaScript时,所有在函数外的代码,在使用C#时,都必须把它们放置在Awake
或Start方法中。
Awake和Start方法的不同点在于:前者在场景进行加载时被调用;后者在调用Update
或FixedUpdate方法之前被调用。因此,Awake方法在Start方法之前被调用。
3、类名必须与文件名相同
在JavaScript中,类名被隐式地设置为文件名。但在C#中,必须手动地进行设置。
4、在C#中,协同程序的使用语法与JavaScript不同。
协同程序必须返回一个IEnumerator类型,并且使用yield return ...来代替JavaScript中
的yield ...。
using System.Collections;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
//C# coroutine
IEnumerator SomeCoroutine()
{
//Wait for one frame
yield return 0;
}
//Wait for two seconds
yield return new WaitForSeconds(2);
}
5、不能使用命名空间(Namespace)
当前的Unity不支持把脚本放到一个命名空间中。这可能在将来的版本中所有改变。
6、只有成员变量才能被序列化并显示在检查器中
私有和保护成员变量仅仅在专家模式(Expert Mode)下才显示在检查器中。属性不能被
序列化并显示在检查器中。
7、避免使用构造器
永远不要使用构造器来初始化值,而应该在Awake或Start中进行,到时Unity会自动
触发相应的构造器,即使在编辑模式(Edit Mode)修改成员变量的值,也会自动触发。这
通常在脚本编译完成后直接发生,以便为它们设置默认值。这样设计的主要原因有两个:构
造器的调用不可预期;构造器的调用可能为了预置的或未活动的游戏对象。
例如:单例模式(Singleton Pattern)对象使用构造器进行初始化时,可能会导致值为空
的严重后果。因此,应该在Awake方法中来初始化单例模式对象。请记住:任何继承至
MonoBehaviour的类中,都不能包含使用构造器的代码。
第十一章 重要类
1、在JavaScript或基于C#的类中,可访问的全局函数:
http://unity3d.com/support/documentation/ScriptReference/MonoBehaviour.html
2、对游戏对象进行移动或旋转:
http://unity3d.com/support/documentation/ScriptReference/Transform.html
3、动画系统:
http://unity3d.com/support/documentation/ScriptReference/Animation.html
4、Rigid bodies:
http://unity3d.com/support/documentation/ScriptReference/Rigidbody.html
5、FPS or Third person character controller:
http://unity3d.com/support/documentation/ScriptReference/CharacterController.html
第十二章 性能最优化
1、使用静态类型化
在JavaScript中,使用静态类型化(Static Typing)来代替动态类型化(Dynamic Typing)
对性能的优化非常重要。Unity 使用一种叫做类型推导(Type Inference)的技术,自动把
JavaScript转化为静态类型代码(Statically Typed Code),而不需要你做其他任何工作。
var foo = 5;
像foo 变量将会被自动推导为整数值。Unity 可以完成许多编译时优化(Compile Time
Optimization)的功能,并且不进行动态变量的耗时查找。这就是Unity的JavaScript的执行
速度比其他JavaScript快了20倍左右的原因之一。
但是并不是所有变量都能进行类型推导,这时,Unity将回滚到动态类型化来处理它们。
动态类型化时,在JavaScript中编写代码变得更加简单,但它的执行速度将会变慢。例如:
function Start()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
这儿的foo将进行动态类型化,因此,调用DoSomething 函数会耗时些。因为,编译器
不知道foo的类型,它会去分析foo变量是否有DoSomething函数,如果有,才进行调用。
function Start()
{
var foo : MyScript = GetComponent(MyScript);
foo.DoSomething();
}
这里我们强制指定了foo的类型,这样会获得更高的性能。
2、使用#pragma strict
在脚本顶部增加#pragma strict语句,会让Unity编译器在执行该脚本时关闭动态类型化
支持,强制使用静态类型化。因此,如果变量的类型不确定,将导致编译错误。例如,下面
的代码在编译时会产生错误:
#pragma strict
function Start()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
3、缓存组件查找
最优化的另一个方法是缓存组件,但它需要编写额外的代码。如果脚本执行的次数很多,
进行组件的缓存将获得更高的性能,这时编写额外的代码显然是值得的。
在使用GetComponent函数或存取器变量(Accessor Variable)来访问一个组件时,Unity
必须从游戏对象中查找相应的组件。这时,我们可以使用私有变量来缓存一个指向该组件的
引用,以便直接进行使用。
因此,我们可以把:
function Update()
{
transform.Translate(0, 0, 5);
}
写成:
private var myTransform : Transfrom;
function Awake()
{
myTransfrom = transform;
}
function Update()
{
myTransform.Translate(0, 0, 5);
}
后面的代码将运行地更快,因为Unity不必每帧都在游戏对象中查找transform 组件。
这对于脚本组件也同样适用。
4、使用内建数组
内建数组的执行非常快,因此我们应该使用它。尽管ArrayList和Array类的使用方式都
比较简单,但它们的处理速度却有很大差别。内建数组都有固定的大小,通常事先我们都知
道这个最大值。内建数组最大的优势在于它能够在一个紧凑的缓冲区中直接嵌入结构体,而
不需要存储额外的类型信息。因此在缓存中迭代它时,将像在内存的一条线上进行处理,非
常方便和快捷:
private var positions : Vector3[];
function Awake()
{
positions = new Vector3[100];
for(var i=0; i<100; i++)
{
positions[i] = Vector3.zero;
}
}
5、避免调用不必要的函数
最简单和有效的最优化在于减少不必要的工作。例如,当敌人离玩家很远时,我们可以
让敌人静止不动,直到玩家走近它。一种较慢的处理如下:
function Update()
{
// Early out if the player is too far wary
if(Vector3.Distance(transform.position, target.position) > 100)
return;
perform real work work ...
}
这种处理的不足之处在于每帧都要执行Update 函数。一种更好的解决方案是在玩家接
近敌人时才启用脚本,有三种方式来进行实现:
(1)、使用OnBecameVisible 和OnBecameInvisible函数:它们的调用取决于渲染系统。当有摄
像机能看到对象时,将调用OnBecameVisible函数;当没有摄像机能看到对象时,将调用
OnBecameInvisible 函数。一般情况下这是有效的,但对于Al通常是无效的,因为一旦你把
摄像机转开敌人时,他们可能会变得不可用。
function OnBecameVisible()
{
enabled = true;
}
function OnBecameInvisible()
{
enabled = false;
}
(2)、使用触发器:使用一个简单的球体触发器时,你可以根据球体的范围来调用
OnTriggerEnter或OnTriggerExit函数。
function OnTriggerEnter(c : Collider)
{
if(c.CompareTag("Player"))
enabled = ture;
}
function OnTriggerExit(c : Collider)
{
if(c.CompareTag("Player"))
enabled = false;
}
(3)、使用协同程序:Update 函数会在每帧进行调用,我们完全可以使用协同程序来在每隔
5秒钟检查一次距离,这将节省很多的资源。
第十三章 脚本编译(高级)
Unity把所有的脚本编译为.NET dll 文件,这些dll 文件将在运行时实时地进行汇编。这
使得Unity的脚本运行速度非常快,比传统的JavaScript快20倍左右,只比本地C++代码慢
50%左右。在保存脚本时,Unity便会花极少的时间对它们进行编译,在编译的过程中,你
可以看到在主窗口的右下角会显示一个小型旋转进展图标。
脚本编译分为四步进行:
1、在“Standard Assets”、“Pro Standard Assets”和“Plugins”中的脚本,首先
进行编译。
一个文件夹中的脚本不能在另一个文件夹的脚本中直接使用,但可以通过使用
GameObject.SendMessage来进行交互。
2、在“Standard Assets/Editor”、“Pro Standard Assets/Editor”和“Plugins/Editor”
中的脚本,其次进行编译。
如果你想要使用UnityEditor 命名空间,你必须把脚本放到这些文件夹中。例如,增加
菜单项或自定义导航,你必须把相应的脚本放到这些文件夹中。这些脚本能够访问上级组中
的脚本。
3、在“Editor”中的脚本,然后进行编译。
跟上步的情况基本相同,不同点在于这些脚本不能访问下级组中的脚本。这在编写编辑
器代码时会出现一些问题,因为你不能编辑下级组中的脚本。可以通过两种方式来解决:(1)、
移动想要访问的脚本到“Plugins”文件夹中。(2)、利用JavaScript 的动态类型化功能。在
JavaScript中,你不需要知道所使用的类的类型。例如,GetComponent函数和SendMessage
函数都仅仅使用一个字符串来代替类型。
4、其他脚本最后进行编译。
不在上面提到的文件夹中的脚本将最后进行编译。这些脚本有权访问“Standard Assets”、
“Pro Standard Assets”和“Plugins”文件夹中的脚本,这使得你可以在不同语言脚本之间进
行交互。例如,如果你想要创建一个JavaScript,并在其中使用一个C#脚本。你可以把C#
脚本放到“Standard Assets”文件夹中,而把JavaScript放到该文件夹的外面。这样javaScript
就可以直接访问C#脚本了。
第一组(“Standard Assets”、“Pro Standard Assets”和“Plugins”文件夹)中的脚本在编
译时会比较耗时,因为它们在编译的同时,第三组(“Editor”文件夹)也在进行预编译。因
此,如果你想减少编译的时间,可以把第一组中的脚本移到第四组中去,但我们并不推荐这
样做。
针对Unity版本进行条件编译
Unity 2.6增加了一个C#预编译器,它能够识别所使用的Unity版本并对特殊功能进行有
条件的访问。例如:
// Specific version define including the minor revision
#if UNITY_2_6_0
// Use Unity 2.6.0 specific feature
#endif
// Specific version define not including the minor revision
#if UNITY_2_6
// Use Unity 2.6.x specific feature
#endif
这段代码用来在指定的Unity版本中启用一些可用的游戏特性。注意,版本条件编译只
能用于Unity 2.6及其后的版本中。将来,Unity会提供一个更适合的定义来在脚本中标识所
使用的Unity版本。
Unity中的脚本(Script)由附加到游戏对象(GameObject)的自定义脚本对象(Custom
Script Object)组成,它们又被称为行为。脚本对象中各种函数被称为必然事件(Certain
Event)。常用的必然事件有如下三个:
1、Update:该函数在渲染帧之前被调用,大部分的游戏行为代码都在这里执行,除了
物理代码。
2、FixedUpdate:该函数在每进行一次物理时间步调时被调用,它用来执行基于物理的
游戏行为。
3、Code outside any function:这类函数在对象加载时被调用,它们用来执行脚本状态
的初始化工作。
你可以自定义事件处理器(Event Handler),它们都以“On”前缀进行命名,例如:
OnCollisionEnter。
第二章 常用操作
大部分游戏对象的操作都是通过它们的Transform和Rigidbody实例来实现的,我们可
以在脚本中直接通过成员变量transform和rigidbody来访问这两个实例。例如:要实现游戏
对象每帧以Y轴旋转5度的效果,可以进行如下编码:
function Update()
{
transform.Rotate(0, 5, 0);
}
让游戏对象向你移动,可进行如下编码:
function Update()
{
transform.Translate(0, 0, 2);
}
第三章 时间
Time类包含了一个重要的类变量deltaTime,它表示距上一次调用Update或FixedUpdate
所用的时间。
因此通过它可以让游戏对象按照一个常速进行旋转,而不是依赖于它的帧频:
function Update()
{
tranform.Rotate(0, 5 * Time.deltaTime, 0);
}
同样地移动效果:
function Update()
{
transform.Translate(0, 0, 2 * Time.deltaTime);
}
如果想要一个值根据每帧的变化而变化(增加或减少),你应该使用Time.deltaTime 来
乘以这个值。这样才能使得变化的效果依赖于单位时间,而不是帧频。这不仅使得游戏的运
行独立于帧频,也使得运动的效果符合现实。
同理,要让灯光的照射范围在每秒使半径增加2个单位,可进行如下编码:
function Update()
{
light.range += 2.0 * Time.deltaTime;
}
但是在通过force来处理rigidbody时,一般情况下不要乘以Time.deltaTime,因为Unity
引擎已经为你进行了处理。
第四章 访问组件
组件被用来附加到游戏对象。当附加一个渲染器组件(Renderer)时,将使得游戏对象
被渲染到屏幕上;当附加一个摄像机组件(Camera)时,将使得游戏对象变为摄像机对象。
所有的脚本都是组件,因此它们可以被附加到游戏对象中。
常用的组件都可以通过一个简单的成员变量来进行访问:
组件 用于访问相应组件的成员变量
Transform transform
Rigidbody rigidbody
Renderer renderer
Camera camera(只能用于摄像机对象)
Light light(只能用于灯光对象)
Animation animation
Collider collider
如果游戏对象没有你想要获取的组件类型,上面的变量将被设置为null。
另外,也可以通过GetComponent函数来获取被附加到游戏对象中的任意内置组件或脚
本。例如:
transfrom.Translate(0, 1, 0);
//等同于
GetComponent(Transfrom).Translate(0, 1, 0);
两者之间不同在于前者(transform等)是一个变量,而后者(Transform等)是一个类
或脚本名。
现在我们进行一个练习,以便更加深入地掌握GetComponent函数。这个游戏对象附加
了两个脚本,我们在其中一个脚本中通过GetComponent 函数来获取另一个脚本(叫做
OtherScript),并访问它的DoSomething函数:
function Update()
{
//在当前游戏对象中查找名为OtherScript的脚本,并调用它的DoSomething函数。
otherScript = GetComponent(OtherScript);
otherScript.DoSomething();
}
第五章 访问游戏对象
大多数高级的游戏代码都不只是操作单个游戏对象。Unity脚本接口拥有多种方式来查
找和访问其他游戏对象或其中的组件。假设有一个名为OtherScript 的脚本被附加到游戏对
象中,该脚本包含了如下的代码片段:
var foo = 5;
function DoSomething(param : String)
{
print(param + " with foo: " + foo);
}
1、通过检查器进行访问
你可以通过检查器来为任意对象类型的变量分配值:
//需要将Translate拖拽到target变量上
var target : Transform;
function Update()
{
target.Translate(0, 1, 0);
}
你也可以把指向其他对象的引用暴露在检查器中。例如:你可以拖拽一个附加了
OtherScript脚本的游戏对象到检查器中的target变量中:
//可以在检查器中通过target变量来设置foo变量
var target : OtherScript;
function Update()
{
//设置foo变量
target.foo = 2;
//调用DoSomething函数
target.DoSomething("Hello");
}
2、通过对象层次进行访问
你可以通过一个游戏对象中的Transform组件来查找该对象的子对象和父对象:
//查找游戏对象中名为“Hand”的子对象
//我们将该脚本附加到当前游戏对象中
transform.Find("Hand").Translate(0, 1, 0);
一旦找到了层次中的transform 对象,你就可以通过使用GetComponent函数来获取其
他脚本:
//查找名为“Hand”的子对象,获取其中附加的OtherScript脚本,并设置foo变量为2。
transform.Find("Hand").GetComponent(OtherScript).foo=2;
//查找名为“Hand”的子对象,获取其中附加的OtherScript脚本,并调用DoSomething函数
transform.Find("Hand").GetComponent(OtherScript).DoSomething("Hello");
//查找名为“Hand”的子对象,获取其中附加的rigidbody对象,并申请一个force。
transform.Find("Hand").rigidbody.AddForce(0, 10, 0);
使用相同的原理,可以通过如下的代码来循环访问所有的子对象:
//将所有子对象都向上移动10个单位
for(var child : Transform in transform)
{
child.Translate(0, 1, 0);
}
3、通过名称或Tag进行访问
可以通过特 定的Tag 来 调用GameObject.FindWithTag 或
GameObject.FindGameObjectsWithTag函数,从而在所有的游戏对象中查找想要的游戏对象。
也可以通过游戏对象的名称来调用GameObject.Find函数来进行查找。
function Start()
{
//通过名称查找
var go = GameObject.Find("SomeGuy");
go.transform.Translate(0, 1, 0);
//通过tag查找
var player = GameObject.FindWithTag("Player");
player.transform.Translate(0, 1, 0);
}
在查找到的游戏对象上调用GetComponent函数,可以获取该游戏对象中所附加的内置
组件或脚本:
function Start()
{
//通过名称查找
var go = GameObject.Find("SomeGuy");
go.GetComponent(OtherScript).DoSomething();
//通过tag查找
var player = GameObject.FindWithTag("Player");
player.GetComponent(OtherScript).DoSomething();
}
一些特殊的对象都拥有一个访问自己的快捷方式,例如:可以通过Camera.main变量来
代表主摄像机。
4、传递参数
一些事件消息都包含了详细的信息,例如:触发器事件会把碰撞对象的碰撞器组件
(Collider)传递给事件处理器函数。
OnTriggerStay函数提供了指向一个碰撞器对象的引用,通过它我们可以获取其中附加的
rigidbody:
function OnTriggerStay(other : Collider)
{
//如果other碰撞器对象拥有rigidbody,就申请一个force。
if(other.rigidbody)
{
other.rigidbody.AddForce(0, 2, 0);
}
}
使用相同的原理,也可以获取碰撞器游戏对象中附加的内置组件或脚本:
function OnTriggerStay(other : Collider)
{
//如果other碰撞器对象拥有OtherScript脚本,就调用它的DoSomething函数。
//大多数情况下,碰撞器都不会附加脚本。因此,我们需要先进行检查,以避免null引用异常。
if(other.GetComponent(OtherScript))
{
other.GetComponent(OtherScript).DoSomething();
}
}
5、查找同类型脚本的所有对象
通过类名或脚本名调用Object.FindObjectOfType或Object.FindObjectsOfType函数,可以
在场景中的所有游戏对象中获取一个或多个同类型的类对象或脚本对象:
function Start()
{
//场景中查找附加了OtherScript脚本的任意一个游戏对象
var other : OtherScript = FindObjectOfType(OtherScript);
other.DoSomething();
}
第六章 向量
Unity使用Vector3类来表示所有的3D向量,可以通过x、y和z成员变量来访问一个
3D向量对象的各个组件:
var aPosition : Vector3;
aPosition.x = 1;
aPosition.y = 1;
aPosition.z = 1;
也可以使用Vector3的构造函数来一次性初始化所有的组件:
var aPosition = Vector3(1, 1, 1);
Vector3也定义了一些常量,用来表示常用的3D向量对象:
//等同于Vector3(0, 1, 0);
var direction = Vector3.up;
可以通过如下代码片段,来访问单个3D向量对象:
someVector.Normalize();
可以通过如下代码片段,来访问多个3D向量对象:
theDistance = Vector3.Distance(oneVector, otherVector);
注:Distance函数为类函数,所以在调用时必须在前面加上Vector3.。
也可以在多个3D向量对象之间使用常用的数学操作符:
combined = vector1 + vector2;
第七章 成员变量和全局变量
在函数外面定义的变量叫做成员变量,它们能够通过Unity的检查器进行访问,存储在
成员变量中的值将自动地保存在项目中。
var memeberVariable = 0.0;
上面的变量将作为一个叫做“Memeber Variable”的数字属性出现在检查器中。
如果设置变量的类型为组件类型(Transform、Rigidbody、Collider和脚本名等),可以通
过把游戏对象拖拽到检查器中这种类型的变量上来设置它们。
var enemy : Transform;
function Update()
{
if( Vecter3.Distance(enemy.postion, transform.position) < 10 )
{
print("I sense the enemy is near!");
}
}
你也可以创建私有成员变量,它们主要用来存储状态信息,而且在脚本外具有不可见性。
私有成员变量不保存在磁盘上,也不能在检查器中进行编辑。只有在检查器被设置为调试模
式时,才允许你通过修改私有成员变量来实时地更新调试器。
private var lastCollider : Collider;
function OnCollisionEnter(collisionInfo : Collision)
{
lastCollider = collisionInfo.other;
}
使用static关键字进行声明的变量叫做全局变量,例如:
//一个名为“TheScriptName”的脚本中拥有一个someGlobal静态变量
static var someGlobal = 5;
//你可以在脚本中像使用普通变量一样来使用它
print(someGlobal);
someGlobal = 1;
为了能在其他脚本中访问全局变量,需要加上“TheScriptName.”前缀:
print(TheScriptName.someGlobal);
TheScriptName.someGlobal = 10;
第八章 实例化
实例化表示复制一个对象,包括所有附加的脚本和整个层次结构。它并不理会指向克隆
层次结构外的对象的引用,而指向克隆层次结构内的对象的引用将会被映射到克隆的对象
上。
下面的脚本被附加到一个具有碰撞器的rigidbody上,当发生碰撞时,将摧毁本身并替
代为大量的爆炸物对象:
var explosion : Transform;
//当碰撞发生时,将摧毁自身并产生大量的预置爆炸物对象。
function OnCollisionEnter()
{
Destroy(gameObject);
var theClonedExplosion : Transform;
theClonedExplosion = Instantiate(explosion, transform.position, transform.rotation);
}
实例化通常用于与预置对象(Prefabs)进行交互。
第九章 协同程序和让步
在编写游戏代码时,通常需要在脚本中结束一系列的事件。例如:
private var state = 0;
function Update()
{
if(state==0)
{
//do step 0
state = 1;
return;
}
if(state==1)
{
//do step 1
state = 2;
return;
}
...
}
这时,我们使用让步(yield)语句是非常方便的,yield 语句是一种特殊的return语句。
它确保函数在下次被调用时,能够从yield语句开始继续执行。
while(true)
{
//do step 0
//wait for one frame
yield;
//do step 1
//wait for one frame
yield;
...
}
你也可以传递一个特定的值给yield语句来推迟Update函数的执行,直到某个事件发生:
//do something
//wait for 5 seconds
yield WaitForSeconds(5.0);
//do something more...
你可以堆积并连接一些协同程序(Coroutine)。例如,下面的示例在执行Do函数时,
立即执行随后的代码:
Do();
print("This is printed immediately");
function Do()
{
print("Do now");
yield WaitForSeconds(2);
print("Do 2 seconds later");
}
下面的示例在执行完Do函数后,才执行随后的代码:
// chain the coroutine
yield StartCoroutine("Do");
print("Also after 2 seconds");
print("This is after the Do coroutine has finished execution");
function Do()
{
print("Do now");
yield WaitForSeconds(2);
print("Do 2 seconds later");
}
注:不能在Update 或FixedUpdate 函数中使用yield 语句,但可以在它们中使用
StartCoroutine函数来调用一个函数。
第十章 使用C#编写脚本
在Unity中可以使用JavaScript、C#和Boo来编写脚本。在使用C#编写脚本时,需注意
以下几个特点:
1、所有脚本都继承至MonoBehaviour
所有的行为脚本都必须直接地或间接地继承至MonoBehaviour。在使用JavaScript 时,
这种继承关系将自动生成,但在使用C#编写脚本时,必须显式地进行定义。通过模板创建
的C#脚本时(Asset->Create->CSharp Script),自动生成的代码已经包含了这样的定义:
//C#
public class NewBehaviourScript : MonoBehaviour
{
...
}
2、使用Awake或Start方法来进行初始化
在使用JavaScript时,所有在函数外的代码,在使用C#时,都必须把它们放置在Awake
或Start方法中。
Awake和Start方法的不同点在于:前者在场景进行加载时被调用;后者在调用Update
或FixedUpdate方法之前被调用。因此,Awake方法在Start方法之前被调用。
3、类名必须与文件名相同
在JavaScript中,类名被隐式地设置为文件名。但在C#中,必须手动地进行设置。
4、在C#中,协同程序的使用语法与JavaScript不同。
协同程序必须返回一个IEnumerator类型,并且使用yield return ...来代替JavaScript中
的yield ...。
using System.Collections;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
//C# coroutine
IEnumerator SomeCoroutine()
{
//Wait for one frame
yield return 0;
}
//Wait for two seconds
yield return new WaitForSeconds(2);
}
5、不能使用命名空间(Namespace)
当前的Unity不支持把脚本放到一个命名空间中。这可能在将来的版本中所有改变。
6、只有成员变量才能被序列化并显示在检查器中
私有和保护成员变量仅仅在专家模式(Expert Mode)下才显示在检查器中。属性不能被
序列化并显示在检查器中。
7、避免使用构造器
永远不要使用构造器来初始化值,而应该在Awake或Start中进行,到时Unity会自动
触发相应的构造器,即使在编辑模式(Edit Mode)修改成员变量的值,也会自动触发。这
通常在脚本编译完成后直接发生,以便为它们设置默认值。这样设计的主要原因有两个:构
造器的调用不可预期;构造器的调用可能为了预置的或未活动的游戏对象。
例如:单例模式(Singleton Pattern)对象使用构造器进行初始化时,可能会导致值为空
的严重后果。因此,应该在Awake方法中来初始化单例模式对象。请记住:任何继承至
MonoBehaviour的类中,都不能包含使用构造器的代码。
第十一章 重要类
1、在JavaScript或基于C#的类中,可访问的全局函数:
http://unity3d.com/support/documentation/ScriptReference/MonoBehaviour.html
2、对游戏对象进行移动或旋转:
http://unity3d.com/support/documentation/ScriptReference/Transform.html
3、动画系统:
http://unity3d.com/support/documentation/ScriptReference/Animation.html
4、Rigid bodies:
http://unity3d.com/support/documentation/ScriptReference/Rigidbody.html
5、FPS or Third person character controller:
http://unity3d.com/support/documentation/ScriptReference/CharacterController.html
第十二章 性能最优化
1、使用静态类型化
在JavaScript中,使用静态类型化(Static Typing)来代替动态类型化(Dynamic Typing)
对性能的优化非常重要。Unity 使用一种叫做类型推导(Type Inference)的技术,自动把
JavaScript转化为静态类型代码(Statically Typed Code),而不需要你做其他任何工作。
var foo = 5;
像foo 变量将会被自动推导为整数值。Unity 可以完成许多编译时优化(Compile Time
Optimization)的功能,并且不进行动态变量的耗时查找。这就是Unity的JavaScript的执行
速度比其他JavaScript快了20倍左右的原因之一。
但是并不是所有变量都能进行类型推导,这时,Unity将回滚到动态类型化来处理它们。
动态类型化时,在JavaScript中编写代码变得更加简单,但它的执行速度将会变慢。例如:
function Start()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
这儿的foo将进行动态类型化,因此,调用DoSomething 函数会耗时些。因为,编译器
不知道foo的类型,它会去分析foo变量是否有DoSomething函数,如果有,才进行调用。
function Start()
{
var foo : MyScript = GetComponent(MyScript);
foo.DoSomething();
}
这里我们强制指定了foo的类型,这样会获得更高的性能。
2、使用#pragma strict
在脚本顶部增加#pragma strict语句,会让Unity编译器在执行该脚本时关闭动态类型化
支持,强制使用静态类型化。因此,如果变量的类型不确定,将导致编译错误。例如,下面
的代码在编译时会产生错误:
#pragma strict
function Start()
{
var foo = GetComponent(MyScript);
foo.DoSomething();
}
3、缓存组件查找
最优化的另一个方法是缓存组件,但它需要编写额外的代码。如果脚本执行的次数很多,
进行组件的缓存将获得更高的性能,这时编写额外的代码显然是值得的。
在使用GetComponent函数或存取器变量(Accessor Variable)来访问一个组件时,Unity
必须从游戏对象中查找相应的组件。这时,我们可以使用私有变量来缓存一个指向该组件的
引用,以便直接进行使用。
因此,我们可以把:
function Update()
{
transform.Translate(0, 0, 5);
}
写成:
private var myTransform : Transfrom;
function Awake()
{
myTransfrom = transform;
}
function Update()
{
myTransform.Translate(0, 0, 5);
}
后面的代码将运行地更快,因为Unity不必每帧都在游戏对象中查找transform 组件。
这对于脚本组件也同样适用。
4、使用内建数组
内建数组的执行非常快,因此我们应该使用它。尽管ArrayList和Array类的使用方式都
比较简单,但它们的处理速度却有很大差别。内建数组都有固定的大小,通常事先我们都知
道这个最大值。内建数组最大的优势在于它能够在一个紧凑的缓冲区中直接嵌入结构体,而
不需要存储额外的类型信息。因此在缓存中迭代它时,将像在内存的一条线上进行处理,非
常方便和快捷:
private var positions : Vector3[];
function Awake()
{
positions = new Vector3[100];
for(var i=0; i<100; i++)
{
positions[i] = Vector3.zero;
}
}
5、避免调用不必要的函数
最简单和有效的最优化在于减少不必要的工作。例如,当敌人离玩家很远时,我们可以
让敌人静止不动,直到玩家走近它。一种较慢的处理如下:
function Update()
{
// Early out if the player is too far wary
if(Vector3.Distance(transform.position, target.position) > 100)
return;
perform real work work ...
}
这种处理的不足之处在于每帧都要执行Update 函数。一种更好的解决方案是在玩家接
近敌人时才启用脚本,有三种方式来进行实现:
(1)、使用OnBecameVisible 和OnBecameInvisible函数:它们的调用取决于渲染系统。当有摄
像机能看到对象时,将调用OnBecameVisible函数;当没有摄像机能看到对象时,将调用
OnBecameInvisible 函数。一般情况下这是有效的,但对于Al通常是无效的,因为一旦你把
摄像机转开敌人时,他们可能会变得不可用。
function OnBecameVisible()
{
enabled = true;
}
function OnBecameInvisible()
{
enabled = false;
}
(2)、使用触发器:使用一个简单的球体触发器时,你可以根据球体的范围来调用
OnTriggerEnter或OnTriggerExit函数。
function OnTriggerEnter(c : Collider)
{
if(c.CompareTag("Player"))
enabled = ture;
}
function OnTriggerExit(c : Collider)
{
if(c.CompareTag("Player"))
enabled = false;
}
(3)、使用协同程序:Update 函数会在每帧进行调用,我们完全可以使用协同程序来在每隔
5秒钟检查一次距离,这将节省很多的资源。
第十三章 脚本编译(高级)
Unity把所有的脚本编译为.NET dll 文件,这些dll 文件将在运行时实时地进行汇编。这
使得Unity的脚本运行速度非常快,比传统的JavaScript快20倍左右,只比本地C++代码慢
50%左右。在保存脚本时,Unity便会花极少的时间对它们进行编译,在编译的过程中,你
可以看到在主窗口的右下角会显示一个小型旋转进展图标。
脚本编译分为四步进行:
1、在“Standard Assets”、“Pro Standard Assets”和“Plugins”中的脚本,首先
进行编译。
一个文件夹中的脚本不能在另一个文件夹的脚本中直接使用,但可以通过使用
GameObject.SendMessage来进行交互。
2、在“Standard Assets/Editor”、“Pro Standard Assets/Editor”和“Plugins/Editor”
中的脚本,其次进行编译。
如果你想要使用UnityEditor 命名空间,你必须把脚本放到这些文件夹中。例如,增加
菜单项或自定义导航,你必须把相应的脚本放到这些文件夹中。这些脚本能够访问上级组中
的脚本。
3、在“Editor”中的脚本,然后进行编译。
跟上步的情况基本相同,不同点在于这些脚本不能访问下级组中的脚本。这在编写编辑
器代码时会出现一些问题,因为你不能编辑下级组中的脚本。可以通过两种方式来解决:(1)、
移动想要访问的脚本到“Plugins”文件夹中。(2)、利用JavaScript 的动态类型化功能。在
JavaScript中,你不需要知道所使用的类的类型。例如,GetComponent函数和SendMessage
函数都仅仅使用一个字符串来代替类型。
4、其他脚本最后进行编译。
不在上面提到的文件夹中的脚本将最后进行编译。这些脚本有权访问“Standard Assets”、
“Pro Standard Assets”和“Plugins”文件夹中的脚本,这使得你可以在不同语言脚本之间进
行交互。例如,如果你想要创建一个JavaScript,并在其中使用一个C#脚本。你可以把C#
脚本放到“Standard Assets”文件夹中,而把JavaScript放到该文件夹的外面。这样javaScript
就可以直接访问C#脚本了。
第一组(“Standard Assets”、“Pro Standard Assets”和“Plugins”文件夹)中的脚本在编
译时会比较耗时,因为它们在编译的同时,第三组(“Editor”文件夹)也在进行预编译。因
此,如果你想减少编译的时间,可以把第一组中的脚本移到第四组中去,但我们并不推荐这
样做。
针对Unity版本进行条件编译
Unity 2.6增加了一个C#预编译器,它能够识别所使用的Unity版本并对特殊功能进行有
条件的访问。例如:
// Specific version define including the minor revision
#if UNITY_2_6_0
// Use Unity 2.6.0 specific feature
#endif
// Specific version define not including the minor revision
#if UNITY_2_6
// Use Unity 2.6.x specific feature
#endif
这段代码用来在指定的Unity版本中启用一些可用的游戏特性。注意,版本条件编译只
能用于Unity 2.6及其后的版本中。将来,Unity会提供一个更适合的定义来在脚本中标识所
使用的Unity版本。
相关文章推荐
- 初识Unity 3D——基本脚本代码
- Unity 3D 对手机屏幕触摸的控制脚本
- 【Unity 3D】学习笔记二十二:unity游戏脚本(二)
- unity 3d 鼠标旋转物体脚本
- Unity 3d脚本加密方案
- 【Unity 3D】学习笔记二十三:unity游戏脚本(三)
- 【Unity 3D】学习笔记二十四:unity游戏脚本(四)
- unity 3D学习备忘二(unity 3D的脚本调试)
- 初试Unity 3D——理解U3D的组件和脚本
- unity 3D学习日记:创建一个小场景并编写简单C#移动脚本
- Unity 3d C#脚本(1)
- Unity 3D 技能释放的脚本 技能冷却时间
- Unity 3d C#和Javascript脚本互相调用 解决方案(非原创、整理资料,并经过实践得来)
- Unity 3D 动态创建对象,并渲染贴图,动态添加删除脚本
- Unity 3D学习笔记(三)——关于脚本
- Unity 3d中导入c#脚本时出现 can't add script 如何解决
- Unity 3D 官方文档 UGUI总览 IMGUI OnGUI Editor脚本初窥1
- [脚本]Unity 3d 播放sd卡音乐或者打开文件
- Unity 3D Coroutine&yield(C#脚本)
- unity 3D登录界面C#脚本