Unity-学习笔记

Unity-学习笔记·

Sence - “场景”视图·

  • 平移:在工具栏中选择“抓手”工具,然后在“场景”视图中单击并拖动以平面横向移动视图。
  • **缩放:**按住 Alt (Windows) 或 Option (macOS),在场景视图中右键单击并拖动以进行缩放。
  • **轴点:**按住 Alt(Windows)或 Option (macOS),左键点按并拖动以围绕当前轴点观察。(注: 此选项在 2D 模式下不可用。)
  • **焦点(帧选择):**选择游戏对象后, 在“场景”视图中按 F 键,将视图聚焦在该游戏对象上。 注: 如果光标不在“场景”视图中,则“帧选择”将不起作用。

飞越模式·

通过以第一人称飞来飞去,在场景视图中导航:

  • 在“场景”视图中,按住鼠标右键。
  • 使用 WASD 向左/向右/向前/向后移动视图。
  • 使用 QE 上下移动视图。
  • 选择并按住 Shift 以加快移动速度。

注: 飞越模式在 2D 模式下不可用。相反,在移动光标的同时按住鼠标右键可在“场景”视图中平移。

查看工具·

**Q:**手动工具,用于平移视图

**W:**移动工具,以选择和更改位置

**E:**旋转工具,用于选择和旋转

**R:**缩放工具,用于选择和更改大小

**T:**矩形变换工具,以 2D 形式缩放

**Y:**变换工具,使用一个 Gizmo 进行移动、缩放和旋转

对于每个变换工具,都会显示一个 Gizmo,允许您沿每个特定轴操作游戏对象。当您操作这些控件时,转换组件中的值会相应地更改。

坐标系·

世界坐标系·

  • 在Unity中创建的物体都是以全局坐标系中的坐标原点(0,0,0),来确定各自的位置的
  • Unity中,如果一个游戏物体没有父物体,则 Inspector中transform 显示的为全局坐标
  • Unity中,可以用 transform.position 取得一个物体的世界坐标

局部坐标系·

  • 每个物体都有自身独立的物体坐标系;当物体移动或改变方向时,和该物体相关联的局部坐标系本身将随之移动或改变方向
  • Unity中模型Mesh保存的顶点坐标均为局部坐标系下的坐标,因此物体的改变并不会影响顶点的坐标
  • 如果一个游戏物体是另一个的子物体,则该物体的坐标 在 Inspector 中 transform 显示的为其父物体的局部坐标。 其实子物体的坐标是相对于其父物体的原点坐标来计算的。
  • 可以通过 transform.localPosition 取得一个物体在其父物体的局部坐标系中的坐标
  • 如果该游戏物体没有父物体,那么 transform.localPosition 获得的依然是该物体在全局坐标系中的坐标。
  • 如果该物体有父物体,则获得是其在父物体的局部坐标系中的坐标。此时检视视图中显示的为 localPosition 的值。

屏幕坐标系·

屏幕坐标系是以屏幕**左下角为(0,0)**点,右上角为(Screen.Width,Screen.Height)

  • 屏幕坐标系是建立在屏幕上的二维坐标系;是以像素为单位,屏幕的左下角为(0, 0),右上角为 (Screen.width, Screen.height);z轴坐标为相机的世界坐标中z轴的负值
  • 鼠标位置坐标属于屏幕坐标 , 可以使用 Input.mousePosition 获得鼠标当前位置的坐标。通过该函数返回的是Vector3类型的变量,但z分量始终为0。
  • 手指触摸屏幕的坐标属于屏幕坐标 ; 可以使用Input.GetTouch(0).position 可以获得单个手指触摸屏幕时手指的坐标。

视口坐标系·

视口坐标是以摄像机为准,以屏幕的左下角为(0,0)点右上角为(1,1)点

  • 视口坐标系是与屏幕坐标系息息相关的,它是将 Game视图的屏幕坐标系单位化,即左下角为(0, 0),右上角为(1, 1),z轴坐标是相机的世界坐标中z轴坐标的负值
  • 假如说屏幕坐标系左下角是(0,0), 右上角是 (800,600)。 现在在一个点在屏幕坐标系上的坐标为 (200,450)。 现在我们把该点的坐标单位化为: x = =( 200/800 = 0.25), y == ( 450 / 600 = 0.75 ), 最终该点单位化的结果坐标为 (0.25,0.75,0)

鼠标位置·

Input.mousePosition

坐标系之间的转换(API)·

世界转本地
transform.InverseTransformDirection();
transform.InverseTransformPoint();
transform.TransformVector();

本地转世界
transform.TransformDirection();
transform.TransformPoint();
transform.TransformVector();

世界转屏幕
Camera.main.WorldToScreenPoint();

屏幕转世界
Camera.main.ScreenToWorldPoint();

世界转视口
Camera.main.WorldToViewportPoint();

视口转世界
Camera.main.ViewportToWorldPoint();

视口转屏幕
Camera.main.ViewportToScreenPoint();

屏幕转视口
Camera.main.ScreenToViewportPoint();

Camera - 摄像机·

使用场景的视角·

将摄像机与当前场景视图对齐,方法是在“层次结构”窗口中选择摄像机,然后按 Ctrl+Shift+F。

物体显示顺序·


此设置,会将物体从 Y 轴上排序,位于屏幕上方的物体会被屏幕下方的物体所遮挡。

PivotRotation -定向方式·

描述·

对象坐标的定向方式。

Local 从活动对象定向。
Global 与全局轴对齐。

Local(局部):物体的自身坐标,可以理解为上下左右
Global(全局):物体的世界坐标,可以理解为东南西北

当cube旋转时,Global的坐标朝向是不变的,Local的坐标朝向与物体的旋转朝向一致

MonoBehaviour·

MonoBehaviour 是一个基类,所有 Unity 脚本都派生自该类。
使用 C# 时,必须显式派生自 MonoBehaviour。

该类不支持 null 条件运算符 (?.) 和 null 合并运算符 (??)。

Awake() - 脚本载入时调用·

Awake 在加载脚本实例时调用。

在加载场景时初始化包含脚本的活动 GameObject 时,或者在将先前非活动的 GameObject 设置为活动时,或者在初始化使用 Object.Instantiate 创建的 GameObject 之后,都将调用 Awake。 在应用程序启动前使用 Awake 来初始化变量或状态。

在脚本实例的生存期内,Unity 仅调用 Awake 一次。脚本的生存期持续到包含它的场景被卸载为止。如果重新加载场景,Unity 会再次加载脚本实例,因此会再次调用 Awake。如果以叠加方式加载场景多次,Unity 会加载多个脚本实例,因此 Awake 会被调用多次(每个实例一次)。


对于放置在场景中的活动 GameObject,Unity 在初始化场景中的所有活动 GameObject 后调用 Awake,因此可以安全地使用 GameObject.FindWithTag 等方法查询其他 GameObject。

Unity 调用每个 GameObject 的 Awake 的顺序是不确定的。因此,你不应依赖一个 GameObject 的 Awake 会在另一个 GameObject 的 Awake 之前或之后调用(例如,你不应假定由一个 GameObject 的 Awake 设置的引用可在另一个 GameObject 的 Awake 中使用)。相反,你应该使用 Awake 在脚本之间设置引用,并使用 /Start/(在所有 Awake 调用完成后调用)来回传递任何信息。

始终先调用 Awake,然后才调用任何 Start 函数。这让你可以对脚本的初始化进行排序。即使脚本是活动 GameObject 的禁用组件,也将调用 Awake

Awake 不能充当协程。

注意:请使用 Awake 来代替构造函数进行初始化,因为组件的序列化状态在构造时是未定义的。 与构造函数一样,仅调用 Awake 一次。

Start() - 脚本实例时调用·

在首次调用任何 Update 方法之前启用脚本时,在帧上调用 Start。

类似于 Awake 函数,Start 在脚本生命周期内仅调用一次
但是,不管是否启用脚本,初始化脚本对象时都会调用 Awake。如果在初始化时未启用脚本,则可以在与 Awake 不同的帧上调用 Start。

在调用任何对象的 Start 函数之前,将在场景中的所有对象上调用 Awake 函数。如果对象 A 的初始化代码需要依赖于已经初始化的对象 B,则这一点会非常有用;此时,B 的初始化应在 Awake 中完成,A 则应在 Start 中完成。

在游戏过程中实例化对象时,Awake 函数在 Scene 对象的 Start 函数完成后调用。

Start()和Awake()的区别·

Awake在MonoBehavior创建后就立刻调用,Start将在MonoBehavior创建后在该帧Update之前,在该Monobehavior.enabled == true的情况下执行。

1
2
3
4
5
void Awake (){}     
//初始化函数,在游戏开始时系统自动调用。一般用来创建变量之类的东西。

void Start(){}
//初始化函数,在所有Awake函数运行完之后(一般是这样,但不一定),在所有Update函数前系统自动条用。一般用来给变量赋值。

参考资料:[Unity3D]脚本中Start()和Awake()的区别

创建对象时的情况:
创建对象时 Unity 不会运行 Start(),而是在下一帧才开始运行。
创建对象时(调用 Instantiate 时)就会立即调用 Awake() 。

这会导致,在 Start() 里面赋值的 Rigidbody ,在创建对象时并没有调用并赋值。
所以,要放在 Awake() 里面赋值。

FixedUpdate() - 物理系统更新·

用于物理计算且独立于帧率的 MonoBehaviour.FixedUpdate 消息。

MonoBehaviour.FixedUpdate 具有物理系统的频率;每个固定帧率帧调用该函数。
FixedUpdate 之后,进行 Physics 系统计算。调用之间的默认时间为 0.02 秒(50 次调用/秒)。使用 Time.fixedDeltaTime 来访问该值。变更该值,方法是在脚本内将其设置为所需的值,或者导航到 Edit > Settings > Time > Fixed Timestep,然后在此处对其进行设置。

FixedUpdate 频率高于或低于 Update。如果应用程序以 25 帧/秒 (fps) 的速度运行,Unity 大约每帧调用该应用程序两次,或者,100 fps 使每个 FixedUpdate 大约渲染两帧。通过 Time 设置,控制所需帧率以及 Fixed Timestep 速率。使用 Application.targetFrameRate 可以设置帧率。

使用 Rigidbody 时使用 FixedUpdate。将力设置为 Rigidbody,并在每个固定帧应用该力。FixedUpdate 按照测量的时间步长进行,一般不会与 MonoBehaviour.Update 冲突。

Update()·

如果启用了 MonoBehaviour,则每帧调用 Update。
在实现任何类型的游戏脚本时,Update 都是最常用函数。 但并非所有 MonoBehaviour 脚本都需 Update()。

要获取自上次调用 Update 以来所经过的时间,请使用 Time.deltaTime。 仅在启用了 Behaviour 时,才会调用该函数。 您可以重写该函数来提供自定义组件的功能。

LateUpdate() - 延时更新·

如果启用了 Behaviour,则每帧调用 LateUpdate。
LateUpdate 在调用所有 Update 函数后调用。 这对于安排脚本的执行顺序很有用。

例如,跟随摄像机应始终在 LateUpdate 中实现, 因为它跟踪的对象可能已在 Update 中发生移动。

要获取自上次调用 LateUpdate 以来所经过的时间,请使用 Time.deltaTime。 仅在启用了 Behaviour 时,才会调用该函数。 您可以重载该函数来提供您的组件的功能。

StartCoroutine - 启动协程·

public Coroutine StartCoroutine (IEnumerator routine);

可以使用 yield 语句,随时暂停协程的执行。使用 yield 语句时,协程会暂停执行,并在下一帧自动恢复。请参阅协程文档以了解更多详细信息。

协程非常适合用于在若干帧中对行为建模。StartCoroutine 方法在第一个 yield 返回时返回,不过可以生成结果,这会等到协程完成执行。即使多个协程在同一帧中完成,也不能保证它们按照与启动相同的顺序结束。

生成的任何类型(包括 null)都会导致执行在后面的帧返回,除非协程已停止或完成。

注意:可以使用 MonoBehaviour.StopCoroutine 和 MonoBehaviour.StopAllCoroutines 停止协程
销毁 MonoBehaviour 时,或是如果 MonoBehaviour 所附加到的 GameObject 已禁用,也会停止协程。
禁用 MonoBehaviour 时,不会停止协程。

GameObject - 游戏对象·

Find - 按名称获取对象·

public static GameObject Find (string name);

按 name 查找 GameObject 对象,然后返回它。
此函数仅返回找到的其中一个活动 GameObject(不确定)。如果未找到具有 name 的 GameObject,则返回 null。如果 name 包含“/”字符,则会向路径名称那样遍历此层级视图。

Find()较为消耗性能,建议不要每帧(Update()里)都使用此函数,而是在启动时将结果缓存到成员变量中,或者使用 GameObject.FindWithTag

注意:如果您要查找子 GameObject,使用 Transform.Find 通常会更加轻松。
注意:如果正在使用多个场景运行此游戏,则 Find 将在所有这些场景中进行搜索。

常见使用:

1
2
3
4
5
6
7
8
9
10
11
private GameObject hand;

void Start()
{
hand = GameObject.Find("/Monster/Arm/Hand");
}

void Update()
{
hand.transform.Rotate(0, 100 * Time.deltaTime, 0);
}

FindWithTag - 按标签获取对象·

public static GameObject FindWithTag (string tag);

返回一个标签为 tag 的活动** GameObject 对象**。如果未找到 GameObject,则返回 null。
在使用标签之前,必须先在标签管理器中声明标签。如果标签不存在,或将空字符串或 null 作为标签传递,将抛出 UnityException。

注意:此方法返回它找到的具有指定标签的第一个 GameObject。如果一个场景包含多个具有指定标签的活动 GameObjects,则无法保证此方法返回特定 GameObject。

FindGameObjectsWithTag - 按标签获取“对象数组”·

public static GameObject[] FindGameObjectsWithTag (string tag);

返回标签为 tag 的活动 GameObjects 的** GameObject[] 数组**。如果未找到任何 GameObject,则返回空数组。
标签在使用前必须在标签管理器中加以声明。如果此标签不存在,或者传递了空字符串或 null 作为标签,则将抛出 UnityException。

关于得到的数组内容顺序的问题 - by.宇羽·

当通过FindGameObjectsWithTag来找相关物体,得到的集合顺序是随机的,并不是按照Hierarchy面板中的顺序。(editor里是按Hierarchy排序的,但build后就完全乱来了)

当我们需要对其进行排序时,可以借助GetSiblingIndex()来实现。
GameObject[] Imgs =GameObject.FindGameObjectsWithTag("Image").OrderBy(g => g.transform.GetSiblingIndex()).ToArray();

CompareTag - 检测对象标签·

public bool CompareTag (string tag);

下面的示例显示 CompareTag 检查一个标签为 Player 的碰撞体。

1
2
3
4
5
6
7
void OnTriggerEnter(Collider other)
{
if (other.gameObject.CompareTag("Player"))
{
Destroy(other.gameObject);
}
}

GetComponent - 获取对象组件·

public T GetComponent ();

如果游戏对象附加了类型为 的组件,则将其返回,否则返回 null。

1
2
3
HingeJoint hinge = gameObject.GetComponent<HingeJoint>();
if (hinge != null)
hinge.useSpring = false;

public Component GetComponent (Type type);

如果游戏对象附加了类型为 type 的组件,则将其返回,否则返回 null。
使用 gameObject.GetComponent 将返回找到的第一个组件,并且未定义顺序。如果预期存在多个相同类型的组件,请改用 gameObject.GetComponents,并针对某些唯一的属性循环使用返回的组件测试。

1
2
3
HingeJoint hinge = gameObject.GetComponent(typeof(HingeJoint)) as HingeJoint;
if (hinge != null)
hinge.useSpring = false;

public Component GetComponent (string type);

如果游戏对象附加了名为 type 的组件,则将其返回,否则返回 null。
出于性能原因,最好使用具有类型而不是字符串的 GetComponent。 但有时可能无法访问该类型,例如在尝试从 Javascript 访问 C# 脚本时。 在这种情况下,可以仅根据名称而不是类型访问该组件。

1
2
3
HingeJoint hinge = gameObject.GetComponent("HingeJoint") as HingeJoint;
if (hinge != null)
hinge.useSpring = false;

type 要检索的组件的类型。

Transform - 变换·

Position - 世界坐标·

public Vector3 position ;

当前物体在世界空间中的变换位置,即世界坐标,(按分析)不会受父物体的影响。
GameObjectTransformposition 属性,可以在 Unity Editor 中以及通过脚本来访问该属性。变更该值可移动 GameObject。获得该值可在 3D 世界空间中定位此 GameObject

localPosition - 局部位置坐标·

public Vector3 localPosition ;

相对于父变换的变换位置,即相对位置坐标,(按分析)会受到父物体的影响。
如果变换没有父级,则其与 Transform.position 相同。

在Unity的Hierarchy面板中,所显示的position为localPosition,对于无父物体的物体而言,其代表世界坐标(其父亲为世界坐标轴),对于有父物体的物体而言,则表示两者的相对位置。

旋转·

Unity中的 rotationlocalRotationeulerAngleslocalEulerAngles都是用来表示旋转的一个API。
不同的是rotation、localRotation是Quaternion类型,返回一个四元数(x,y,z,w)范围:(0,1)
而eulerAngles、localEulerAngles返回的是一个欧拉角(x,y,z)范围:(0,360)

localRotation - 局部旋转·

public Quaternion localRotation ;

相对于父级旋转的变换旋转。
Unity 在内部将旋转存储为四元数。
要旋转对象,请使用 Transform.Rotate
要将旋转修改为欧拉角,请使用 Transform.localEulerAngles

参考资料:Unity3D中的欧拉角使用(二) ——localRotation
transform.localRotation = Quaternion.Eular(x,y,z)控制旋转的时候,按照Z-X-Y的旋转顺规,且绕轴旋转的坐标轴是父节点本地坐标系的坐标轴

eulerAngles - 欧拉旋转·

public Vector3 eulerAngles ;

以欧拉角表示的旋转(以度为单位)。
Transform.eulerAngles 表示世界空间中的旋转。在检视面板中查看 GameObject 的旋转时,可能会看到与此属性中存储的值不同的角度值。这是因为检视面板显示本地旋转,有关更多信息,请参阅 Transform.localEulerAngles

欧拉角可以通过围绕各个轴执行三个单独的旋转来表示三维旋转。在 Unity 中,围绕 Z 轴、X 轴和 Y 轴(按该顺序)执行这些旋转。可以通过设置此属性来设置四元数的旋转,并且可以通过读取此属性来读取欧拉角的值。

使用 .eulerAngles 属性设置旋转时,务必要了解,虽然提供 X、Y 和 Z 旋转值描述旋转,但是这些值不存储在旋转中。而是将 X、Y 和 Z 值转换为四元数的内部格式。
读取 .eulerAngles 属性时,Unity 将四元数的内部旋转表示形式转换为欧拉角。因为可通过多种方式使用欧拉角表示任何给定旋转,所以读出的值可能与分配的值截然不同。如果尝试逐渐增加值以生成动画,则这种情况可能会导致混淆。
若要避免这些类型的问题,使用旋转的建议方式是避免在读取 .eulerAngles 时依赖一致的结果,特别是在尝试逐渐增加旋转以生成动画时。有关实现此目标的更佳方式,请参阅四元数 * 运算符

不要单独设置某个 eulerAngles 轴(例如,eulerAngles.x = 10;),这会导致偏差和不希望的旋转。 在将它们设置为新值时,请一次性设置所有 eulerAngles 轴。 Unity 在角度与存储在 Transform.rotation 中的旋转之间进行转换。

参考资料:Unity中的 eulerAngles、localEulerAngles细节剖析

eulerAngles和rotation的区别·

eulerAngles的角度是不能随时变化的,是一个定值,
而rotation的角度是可以增加的,
eulerAngles用vector3来赋值,而rotation用Quaternion来赋值。

localScale - 局部变换缩放·

public Vector3 localScale ;

相对于 GameObjects 父对象的变换缩放。

RectTransform - 矩形变换·

class in UnityEngine / 继承自:Transform

矩形的位置、大小、锚点和轴心信息。
RectTransforms 用于 GUI,不过也可以用于其他情况。 它用于存储和操作矩形的位置、大小和锚定,并支持各种形式的缩放(基于父 RectTransform)。
Rect Transform(矩形变换)仍然是一种 Transform(变换),因此可以在脚本中用作变换,但矩形变换具有额外的 UI 数据

注意:Inspector 基于在使用哪个锚点预设对公开哪些属性进行更改。有关更多信息,请参阅矩形变换基本布局

RectTransform之UI宽高和SizeDelta·

Vector3 - 三维向量·

normalized - 归一化向量·

public Vector3 normalized ;
对一个三维向量进行归一化,向量方向保持不变,但其大小设为 1.0,然后获取该值。

请注意,当前向量保持不变,返回一个新的归一化向量

如果要对一个三维向量归一化并改变其值,请使用 Normalize 函数。

如果向量太小而无法标准化,则返回零向量。

Normalize - 归一化函数·

public static Vector3 Normalize (Vector3 value);
对一个三维向量进行归一化,向量方向保持不变,但其大小设为 1.0,然后以结果改变其值。

请注意,此函数将更改当前向量

如果 要保持当前向量不变,请使用 normalized 变量。

如果该向量太小而无法标准化,则将其设置为零。

magnitude - 向量长度·

public float magnitude ;
返回该向量的长度。(float)

向量长度为 (xx+yy+z*z) 的平方根。

如果只需要比较一些向量的大小, 则可以使用 sqrMagnitude 比较它们的平方数(计算平方数更快)。

例如:
返回向量的长度,也就是点目标点(x,y,z)到原点(0,0,0)的距离。
float distance = gameObject.transform.position.magnitude;

sqrMagnitude - 向量长度的平方·

public float sqrMagnitude ;
返回该向量的平方长度。(只读)

向量 v 的大小以 Mathf.Sqrt(Vector3.Dot(v, v)) 方式进行计算。 但是,Sqrt 计算相当复杂, 执行时间比普通算术运算要长。 计算平方数会比使用 magnitude 属性要快得多 - 计算基本相同,只是消除了执行缓慢的 Sqrt 调用。 如果您只将大小用于比较距离的目的,则也可以将平方数与距离的平方进行比较, 因为这种比较也会给出相同的结果。

Collider2D - 2D碰撞体·

2D 游戏使用的碰撞体类型的父类。
如:BoxCollider2DCircleCollider2DPolygonCollider2DEdgeCollider2D

IsTouchingLayers - 与Layers触碰·

public bool IsTouchingLayers (int layerMask = Physics2D.AllLayers);

检查该碰撞体是否正在接触指定 layerMask 上的任何碰撞体。
请务必注意,检查碰撞体是否正在接触是对照上次物理系统更新进行的,即,当时接触碰撞体的状态。如果刚添加了新的 Collider2D,或者移动了 Collider2D,但尚未进行更新,则这些碰撞体不会显示正在接触。接触状态与物理碰撞或触发器回调所指示的状态相同。

如:
bool isGound = myFeet.IsTouchingLayers(LayerMask.GetMask("Ground"));

ClosestPoint - 碰撞处的最近点·

public Vector3 ClosestPoint(Vector3 position);

  • position:您需要找到最接近点所对应的位置。

返回碰撞体上最接近给定位置的一个点。

此方法计算碰撞体上最接近于 3D 世界位置的点。在以下示例中,closestPoint 是碰撞体上的点,location 是 3D 空间中的点。如果 location 位于碰撞体中,closestPoint 将在内部。
注意:与 ClosestPointOnBounds 的区别是返回的点实际上位于碰撞体上,而不是在碰撞体的边界上。(bounds 是围绕碰撞体的框形。)

Bounds.ClosestPoint·

public Vector3 ClosestPoint (Vector3 point);

返回该包围盒上或其内部的最近的点。
如果该点在包围盒内,则将返回未修改的点位置。

Time - 时间·

timeScale - 时间流逝速率·

public static float timeScale ;

时间流逝的标度。可用于慢动作效果。

当 timeScale 为 1.0 时,时间流逝的速度与实时一样快。
当 timeScale 为 0.5 时,时间流逝的速度比实时慢 2x。
当 timeScale 设置为 0 时,如果您的所有函数都是独立于帧率的, 则游戏基本上处于暂停状态。

timeScale 影响 Time 类的所有时间和增量时间测量变量(但 realtimeSinceStartupfixedDeltaTime 除外)。
如果您减小了 timeScale,建议也将 Time.fixedDeltaTime 减小相同的量。
当 timeScale 设置为 0 时,不会调用 FixedUpdate 函数。

协程与线程·

Unity3D是以生命周期主线程循环进行游戏开发。
线程:
Unity3D中的子线程无法运行Unity SDK(开发者工具包,软件包、软件框架)跟API(应用程序编程接口,函数库)。
限制原因:大多数游戏引擎都是主循环结构,游戏中逻辑更新和画面更新的时间点要求有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中的对象不同步的现象。虽然多线程也能保证这个效果,但是引用多线程,会加大同步处理的难度与游戏的不稳定性。
但是多线程也是有好处的,如果不是画面更新,也不是常规的逻辑更新(指包括AI、物理碰撞、角色控制这些),而是一些其他后台任务,比如大量耗时的数据计算、网络请求、复杂密集的I/O操作,则可以将这个独立出来做成一个工作线程,这需要写Unity游戏的Native扩展。

协程:
对于Unity3D,它是生命周期主线程循环的设计,它更倾向于使用Time slicing(时间分片,时间流逝速度)的Coroutine(协程)去完成异步任务,融合到生命周期中。
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

Coroutine - 协程·

协程允许您将任务分散到多个帧中。在 Unity 中,协程是一种方法可以暂停执行并将控制权返回给 Unity,但随后在下一帧中从中断的位置继续

在大多数情况下,当您调用方法时,该方法将运行到完成,然后将控制权以及任何可选的返回值返回给调用方法。这意味着在方法中发生的任何操作都必须在单个帧更新中发生。

如果要使用方法调用来包含一段时间内的过程动画或一系列事件,则可以使用协程。

但是,请务必记住,协程不是线程。在协程中运行的同步操作仍在主线程上执行。如果要减少在主线程上花费的 CPU 时间,则避免在协程中阻塞操作与在任何其他脚本代码中阻塞操作同样重要。如果要在 Unity 中使用多线程代码,请考虑使用 C# 作业系统

如果需要处理较长的异步操作(如等待 HTTP 传输、资产加载或文件 I/O 完成),最好使用协程。
( HTTP 传输、资产加载或文件 I/O本身最好还是用线程)

在Unity3D中,协程是可自行停止运行 (yield),直到给定的 Yield Instruction 结束再继续运行的函数。协程 (Coroutines) 的不同用法:

  1. yield return null - 这一帧到此暂停,下一帧再从暂停处继续,常用于循环中。
  2. yield return new WaitForEndOfFrame - 等到这一帧的cameras和GUI渲染结束后再从此处继续,即等到这帧的末尾再往下运行。这行之后的代码还是在当前帧运行,是在下一帧开始前执行,跟return null很相似。
  3. yield return new WaitForFixedUpdate - 在下一次执行FixedUpdate的时候继续执行这段代码,即等一次物理引擎的更新
  4. yield return new WaitForSeconds(3.0f) - 等待3秒,然后继续从此处开始,常用于做定时器
  5. yield return WWW - 等待直至异步下载完成。
  6. yield return StartCoroutine(methodName) - 等待另一个协程执行完。这是把协程串联起来的关键,常用于让多个协程按顺序逐个运行。
  7. yield break - 直接跳出协程,对某些判定失败必须跳出的时候,比如加载AssetBundle的时候,WWW失败了,后边加载bundle没有必要了,这时候可以yield break跳出。

值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将暂停运行。

协程可以相互嵌套(也可以自身嵌套?)

例如:下面是一个普通攻击模块,按下攻击键后,需要等待动画把攻击动画显示出来后,启用攻击碰撞框,然后再隔一段时间关闭攻击碰撞框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Attack()
{
if (Input.GetButtonDown("Attack")) // 监测‘攻击’键的输入
{
anim.SetTrigger("Attack"); // 响应动画属性中的 Attack 变量,表示播放攻击动画
StartCoroutine(startAttack()); // 调用‘启用攻击碰撞框’的协程
}
}

// 新建一个‘启用攻击碰撞框’的协程
IEnumerator startAttack()
{
yield return new WaitForSeconds(startTime); // 等待一段指定的时间(startTime)延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间会受到Time.timeScale的影响);
collider2D.enabled = true; // 启用‘攻击碰撞框’
StartCoroutine(disableHitBox()); // 调用‘关闭攻击碰撞框’的协程
}

// 新建一个‘关闭攻击碰撞框’的协程
IEnumerator disableHitBox()
{
yield return new WaitForSeconds(time); // 等待一段指定的时间(time)延迟之后继续执行
collider2D.enabled = false; // 关闭‘攻击碰撞框’
}
1
2
3
4
5
6
7
8
9
10
11
12
IEnumerator GenCoins()
{
/// 先执行下方代码
WaitForSeconds wait = new WaitForSeconds(intervalTime);
for (int i = 0; i < coinQuantity; i++)
{
GameObject gb = Instantiate(coin, transform.position, Quaternion.identity);
Vector2 randomDirection = new Vector2(Random.Range(-0.3f, 0.3f), 1.0f);
gb.GetComponent<Rigidbody2D>().velocity = randomDirection * coinUpSpeed;
yield return wait; // 暂停代码运行,等待指定时间后继续运行
}
}

停止协程·

若要停止协程,请使用 StopCoroutineStopAllCoroutines
如果协程附加到的游戏对象的 SetActive 被设置为 false ,则协程也会停止。
调用 Destroy(example)(其中 example 是 MonoBehaviour 实例)会立即触发 OnDisable,并会处理协程,从而有效地停止它。最后,在帧结束时调用 OnDestroy

注意:如果你通过设置 enabled 为 false 来禁用 MonoBehaviour , Unity不会停止协程。

线程·

Input - 输入控制器·

使用该类来读取传统游戏输入中设置的轴,以及访问移动设备上的多点触控/加速度计数据。
KeyCode,其中列出了所有的按键、鼠标和游戏杆选项

GetAxis - 获取轴·

public static float GetAxis (string axisName);

返回由 axisName 标识的虚拟轴的值。
对于键盘和游戏杆输入设备,该值将处于(-1 ~ 1)的范围内。

该值的含义取决于输入控制的类型,例如,对于游戏杆的水平轴,值为 1 表示游戏杆向右推到底,值为 -1 表示游戏杆向左推到底;值为 0 表示游戏杆处于中性位置。

如果将轴映射到鼠标,该值会有所不同,并且不会在 -1…1 的范围内。此时,该值为当前鼠标增量乘以轴灵敏度。通常,正值表示鼠标向右/向下移动,负值表示鼠标向左/向上移动。

该值与帧率无关;使用该值时,您无需担心帧率变化问题。
如果您将输入用于任何类型的移动行为,请使用 Input.GetAxis。它为您提供了可以映射到键盘、操纵杆或鼠标的平滑且可配置的输入。仅将 Input.GetButton 用于类似操作的事件,请勿将其用于移动。

GetButtonDown - 按钮按下·

public static bool GetButtonDown (string buttonName);

在用户按下由 buttonName 标识的虚拟按钮的帧期间返回 true。
Update 函数调用此函数,因为状态每帧都会重置。在用户释放键并再次按下它之前,它不会返回 true。(只会检测一次按下,而不会检测按住

仅在实现武器单发开火事件等操作时,才使用该函数。
对于任意类型的移动行为,请使用 Input.GetAxis

编辑、设置或删除按钮及其名称(例如“Fire1”):

  1. Edit > Project Settings > Input Manager 打开输入管理器.
  2. 单击它旁边的箭头,展开 Axis。这将显示当前按钮的列表。您可以使用其中一个作为“buttonName”参数。
  3. 展开列表中的某个项目后,可以访问和更改相关选项,例如按钮名称以及触发它的键、游戏杆或鼠标移动操作。
  4. 有关按钮的更多信息,请参阅输入管理器页面。

Prefab - 预制体·

Unity 的预制件系统允许创建、配置和存储游戏对象及其所有组件、属性值和子游戏对象作为可重用资源。预制件资源充当模板,在此模板的基础之上可以在场景中创建新的预制件实例。

如果要在场景中的多个位置或项目中的多个场景之间重用以特定方式配置的游戏对象,比如非玩家角色 (NPC)、道具或景物,则应将此游戏对象转换为预制件。这种方式比简单复制和粘贴游戏对象更好,因为预制件系统可以自动保持所有副本同步。

预制件嵌套
预制件的变体

如果游戏对象在一开始不存在于场景中,而希望在运行时实例化游戏对象(例如,使能量块、特效、飞弹或 NPC 在游戏过程中的正确时间点出现),那么也应该使用预制件。

使用预制件的一些常见示例包括:

  • 环境资源 - 例如,在一个关卡附近多次使用的某种树(如上面的截屏所示)。
  • 非玩家角色 (NPC) - 例如,某种类型的机器人可能会在游戏的多个关卡之间多次出现。它们的移动速度或声音可能不同(使用覆盖)。
  • 飞弹 - 例如,海盗的大炮可能会在每次射击时实例化炮弹预制件。
  • 玩家主角 - 玩家预制件可能被放置在游戏每个关卡(不同场景)的起点。

Prefab Variant - 预制件变体·

希望预制件有一组预定义的变化时,预制件变体非常有用。

例如,您可能希望在游戏中使用几种不同类型的 GermSlimeTarget,这些全都基于同一个基本 GermSlimeTarget 预制件。但是,您可能想要一些 GermSlimeTarget 携带物品,一些以不同的速度移动,或者一些发出额外的声音效果。

为此,您可以设置初始 GermSlimeTarget 预制件来执行您希望所有 GermSlimeTarget 共同执行的所有基本操作,然后可以创建多个预制件变体来实现以下目的:

  • 通过在脚本上使用属性覆盖来更改速度,使 GermSlimeTarget 更快移动。
  • 通过将额外的游戏对象附加到手臂,使 GermSlimeTarget 携带物品。
  • 通过添加一个播放格喳声的 AudioSource 组件,使 GermSlimeTarget 发出子弹般的格喳声。

预制件变体继承另一个称为基础预制件的预制件的属性。对预制件变体进行的覆盖优先于基础预制件的值。预制件变体可以使用任何其他预制件作为其基础预制件(包括模型预制件或其他预制件变体)。

  • 继承
  • 覆盖
  • 依赖
  • 解除依赖

Object·

Instantiate - 克隆对象·

public static Object Instantiate (Object original);
public static Object Instantiate (Object original, Transform parent);
public static Object Instantiate (Object original, Transform parent, bool instantiateInWorldSpace);
public static Object Instantiate (Object original, Vector3 position, Quaternion rotation);
public static Object Instantiate (Object original, Vector3 position, Quaternion rotation, Transform parent);

返回·

Object 实例化的克隆对象。

参数·

original 要复制的现有对象。
position 新对象的位置。
rotation 新对象的方向。
parent 将指定给新对象的父对象。
instantiateInWorldSpace 分配父对象时,传递 true 可直接在世界空间中定位新对象。传递 false 可相对于其新父项来设置对象的位置。

克隆 original 对象并返回克隆对象。

此函数会通过与编辑器中的复制命令类似的方式创建对象的副本。
如果要克隆 GameObject,则可以指定其位置和旋转(否则,这些默认为原始 GameObject 的位置和旋转)。
如果要克隆 Component,则也会克隆它附加到的 GameObject(同样可指定可选的位置和旋转)。

克隆 GameObjectComponent 时,也将克隆所有子对象和组件,它们的属性设置与原始对象相同。

默认情况下,新对象的父对象 为 null;它与原始对象不"同级”"。但是,仍可以使用重载方法设置父对象。
如果指定了父对象但未指定位置和旋转,则使用原始对象的位置和旋转作为克隆对象的本地位置和旋转或是其世界位置和旋转(如果 instantiateInWorldSpace 参数为 true)。

克隆时 GameObject 的活动状态会维持,因此,如果原始对象处于非活动状态,则克隆对象也会在非活动状态下创建。此外,对于层级视图中的对象和所有子对象,其每个 Monobehaviour 和 Component 都会仅当调用此方法时它们在层级视图中处于活动状态时,才会调用其 Awake 和 OnEnable 方法。

这些方法不会创建与新实例化对象的预制件连接。可以使用 PrefabUtility.InstantiatePrefab 创建具有预制件连接的对象。在运行时实例化预制件

您还可以使用泛型来实例化对象。有关更多详细信息,请参阅泛型函数页面。
通过使用泛型,我们不需要将结果转换为特定类型。
Missile missileCopy = Instantiate<Missile>(missile);

SceneManager·

LoadScene - 加载场景·

public static void LoadScene (int sceneBuildIndex, SceneManagement.LoadSceneMode mode= LoadSceneMode.Single);
public static void LoadScene (string sceneName, SceneManagement.LoadSceneMode mode= LoadSceneMode.Single);

sceneName 要加载的场景的名称或路径。
sceneBuildIndex Build Settings 中要加载场景的索引。
mode 允许您指定是否以累加方式加载场景。有关选项的更多信息,请参阅 LoadSceneMode

按照 Build Settings 中的名称或索引加载场景。
注意:在大多数情况下,为了避免在加载时出现暂停或性能中断现象, 您应该使用此命令的异步版,即: LoadSceneAsync

使用 SceneManager.LoadScene 时,不会立即加载场景,而是在下一帧加载。这种半异步的行为可能会导致帧卡顿,并可能令人困惑,因为加载无法立即完成。

由于加载被设置为在下一个渲染帧中完成,调用 SceneManager.LoadScene 会强制完成之前的所有 AsyncOperations,即使 AsyncOperation.allowSceneActivation 设置为 false 也一样。要避免这种情况,请改用 LoadSceneAsync

提供的 sceneName 可以只是场景名称(不包含 .unity 扩展名),也可以是 BuildSettings 窗口中显示的路径(仍然不包含 .unity 扩展名)。如果仅提供场景名称,此方法将加载场景列表中匹配的第一个场景。如果有多个名称相同但路径不同的场景,应该使用完整路径。

SceneA 能够以累加方式多次加载 SceneA。每个加载的场景都使用常规名称。如果 SceneA 加载 SceneB 十次,每个 SceneB 将具有相同的名称。无法找到特定的已添加场景。

请注意,除非您从 AssetBundle 加载场景,否则 sceneName 不区分大小写。
有关在编辑器中打开场景的信息,请参阅 EditorSceneManager.OpenScene

1
2
3
4
5
6
7
8
9
public void LoadA(string scenename)
{
SceneManager.LoadScene(scenename);
}

public void LoadB(int sceneANumber)
{
SceneManager.LoadScene(sceneANumber);
}

LoadSceneMode - 场景加载的模式·

通过 LoadSceneMode 选择在使用 SceneManager.LoadScene 时加载哪种类型的场景。

单模式将加载一个标准的 Unity 场景,该场景将独立显示在 Hierarchy 窗口中。
附加模式将加载一个显示在 Hierarchy 窗口的场景,而窗口中同时还包含其他活动场景。

Single 关闭所有当前加载的场景,并加载一个场景。
Additive 将场景添加到当前加载的场景。

Attributes - 变量·

SerializeField - 序列化·

强制 Unity 对私有字段进行序列化。

效果:在 Inspector(检查器)中显示被序列化的变量

当 Unity 对脚本进行序列化时,仅对公共字段进行序列化。 如果还需要 Unity 对私有字段进行序列化, 可以将 SerializeField 属性添加到这些字段。

Unity 将对所有脚本组件进行序列化,重新加载新程序集, 并从序列化的版本重新创建脚本组件。此 序列化是通过 Unity 内部序列化系统完成的;而不是通过 .NET 的序列化功能来完成。

1
2
3
4
5
[SerializeField]
private bool isGround = true;

[SerializeField]
private float Health = 10.0f;

参考资料:Unity3D中[SerializeField]特性的使用


序列化系统可执行以下操作:

  • 可序列化(可序列化类型的)公共非静态字段
  • 可序列化标记有 SerializeField 属性的非公共非静态字段。
  • 不能序列化静态字段。
  • 不能序列化属性。

可序列化的类型

Unity 可序列化以下类型的字段:

  • 继承 UnityEngine.Object 的所有类,例如 GameObject、Component、MonoBehaviour、Texture2D、AnimationClip。
  • 所有基本数据类型,例如 int、string、float、bool。
  • 某些内置类型,例如 Vector2、Vector3、Vector4、Quaternion、Matrix4x4、Color、Rect、LayerMask。
  • 可序列化类型数组
  • 可序列化类型列表
  • 枚举
  • 结构

有关序列化的更多信息,请参阅脚本序列化

注意:如果在一个列表(或数组)中将一个元素放置两次,当此列表被序列化时,将获得该元素的两个副本,而不是获得两次新列表中的一个副本。

注意:如果要序列化自定义 Struct 字段,则必须为该 Struct 给定 [System.Serializable] 属性。

提示:Unity 不会序列化 Dictionary,但您可以为键存储一个 List<> 和为值存储一个 List<>,然后在 Awake() 上将它们组合在非序列化字典中。这不能解决您需要修改字典 并将其“保存”回时出现的问题,但在许多其他情况下,这是一个方便的技巧。

NonSerialized - 取消序列化·

NonSerialized 属性将变量标记为无法序列化。

通过该方法,您可以使一个变量保持公开,且 Unity 不会尝试序列化它或在 Inspector 中显示它。
另请参阅:HideInInspector

1
2
[System.NonSerialized]
public int p = 5;

Tags and Layers - 标签、图层·

Tags and Layers 设置(主菜单:Edit > Project Settings__,然后选择 Tags and Layers__ 类别)可用于设置标签 (Tags)排序图层 (Sorting Layers)图层 (Layers)

Layer - 层·

在 Unity 中,__层__定义哪些游戏对象可以与不同的功能以及彼此交互。它们主要有两种用途:由__摄像机__用来仅渲染场景的某一部分;由__光源__用来仅照亮场景的某些部分。但是,层也可以供射线投射用于选择性地忽略碰撞体或创建碰撞

注意:第 31 层为 Editor 的预览窗口内部机制使用。为了防止冲突,请勿使用此层。

基于层的碰撞检测·

通过基于层的碰撞检测,可以让某个游戏对象与另一个设置为特定层或多个层的游戏对象发生碰撞。

上图显示了 Scene 视图中的六个游戏对象(3 个平面,3 个立方体),窗口右侧显示了 Layer Collision Matrix(层碰撞矩阵)。Layer Collision Matrix 定义了哪些游戏对象与哪些层碰撞。
在此示例中,Layer Collision Matrix 设置为只有属于相同层的游戏对象才能碰撞。

设置基于层的碰撞检测:

  1. 在 Unity 菜单栏中,选择 Edit > Project Settings,
  2. 然后选择 Physics 类别以打开 Physics 窗口。
  3. 选择碰撞矩阵中的哪些层将与其他层交互(勾选相应层即可)。

参考资料:Unity 基础 之 Layer(层layer)

SortingLayer·

SortingLayer 可用于轻松设置多个物体的渲染顺序。

始终有一个名称为“Default”的默认 SortingLayer,最初所有物体都添加到其中。添加了更多 SortingLayer,可轻松控制物体组的渲染顺序。图层可排在默认图层的前面或后面。

参考资料:Unity SortingLayer和Layer区别、相机、渲染顺序和射线检测

SortingGroup - 排序组·

排序组 (Sorting Group) 可以将具有精灵渲染器 (Sprite Renderer) 的游戏对象分组在一起,并控制这些渲染器渲染精灵的顺序。Unity 将同一排序组中的精灵渲染器一起渲染,就好像它们是单个游戏对象一样。

设置排序组·

要将游戏对象添加到排序组中,请将 Sorting Group 组件添加到此游戏对象。为此,请选择游戏对象,然后选择 Component > Rendering > Sorting Group,或者选择游戏对象的 Inspector 窗口中的 Add Component 按钮。
将 Sorting Group 组件添加到游戏对象时,Unity 会将同一排序组应用于该组件附加到的游戏对象的所有子游戏对象。

Sorting Group 属性·

Unity 使用 Sorting Group 组件的** Sorting Layer **和 Order in Layer 值来确定排序组在渲染队列内相对于场景中其他排序组和游戏对象的优先级。

Bolt - 可视化编程·

Quaternion - 四元数·

Quaternion.identity 表示“无旋转”