Unity 3D脚本语言的类型

C#的类型系统

  • C#要求其所有类型全部从System.Object类派生。
  • 在Unity 3D使用过程中,C#语言脚本接口是以MonoBehaviour这个类作为基础的。而MonoBehaviour显然也是派生自System.Object。
  • C#语言是类型安全的,其本质是有关类型操作的一种规范,即不能将一种类型当作另一种类型,除非它们真的存在转换关系。
  • C#语言是静态类型
  • C#语言在大多数的时候是显式类型的,在C#3中引入了关键字“var”来表示隐式类型,声明局部变量时使用var,表示编译器在编译时需要对该变量进行类型推断。
所有类都有这几个最基本的方法包括以下4个公共和2个受保护方法。
  • 1)Equals:若两个对象具有相同的值,则返回true,否则返回false。
  • 2)GetHashCode:返回对象的值的哈希码。
  • 3)ToString:默认返回类型的完整名称,即 this.GetType().FullName。但是此方法经常被重写,最典型的例子就是int型等重写该方法以返回其值的字符串形式。
  • 4)GetType:返回一个从 Type类派生的类型实例,以指出调用 GetType 方法的对象是什么类型。常用于为反射提供与对象类型有关的元数据信息。
  • 5)MemberwiseClone。
  • 6)Finalize:虚方法,在对象被标志为应该被作为垃圾回收之后,但在内存真正被回收之前,会调用该方法。因此,如果需要在回收内存前执行清理工作的类型应该重写该方法。

值类型和引用类型

值类型的几个特征
  1. 值类型不派生出其他任何类型
  2. 值类型不需要从其他类型派生
  3. 值类型是不可变的。就是说值类型是十分简单的类型,没有成员会修改类型的任何示例字段。如果类型没有提供会更改其字段的成员,则称该类型是不可变的,而值类型是不可变的。一个比较常见的误区,便是对string类型的误解,string类型即System.String,虽然string类型也是不可变的,但它是引用类型而非值类型。
  4. 值类型是以值方式传递的。就是说值类型变量在进行传递时,会对值类型实例中的字段进行复制。例如方法中的实参传递,或者是一个返回类型为值类型的方法在返回时,返回的值类型中的字段会复制到调用者分配的内存中。值类型的主要优势在于不是必须作为对象在托管堆上分配,但是如果值类型实例过大,也会由于复制的缘故,而对性能产生影响。
  5. 值类型实例有两种表示方式,分别是未装箱和已装箱。而装箱机制是指将值类型转换成引用类型。正是由于值类型不在托管堆中分配,不被垃圾回收,同时也无须通过指针来引用,因而值类型与引用类型存在很多区别。但是有很多情况需要获取和操作对值类型实例的引用。因而装箱机制为了应对这种情况,便应运而生。
  6. 值类型派生自System.ValueType(即struct隐含的基类型是System.ValueType),而System.ValueType同样从System.Object派生而来,因此System.ValueType提供了和System.Object相同的方法,不过值得注意的是System.ValueType重写了Equals方法和GetHashCode方法。
值类型实例进行装箱时的步骤
  1. 在托管堆中分配内存。 需要注意的是,由于是将值类型进行引用类型化,因而分配的内存空间除了值类型各个字段所需的内存之外,还要加上托管堆所有对象都有的两个额外成员(类型对象指针和同步索引块)所需的内存。
  2. 将值类型的字段复制到新分配的堆内存中
  3. 返回对象地址,即对象的引用。值类型成了引用类型。
    通过分析这3个步骤可以发现,Mono运行时只是将值类型变量v的值复制到了一个在托管堆上新创建的对象中。所以该对象的值显然只是原始值的一个副本,因而改变的原始值是不会改变箱内的值的
值类型拆箱的过程
  1. 获取已经装箱对象中各个字段的地址,这个过程便是所说的拆箱。
  2. 将已经装箱对象中各个字段的值从托管堆上复制到线程栈的新的值类型实例中去
拆箱当作了装箱的逆过程是一个误区

相比于装箱,拆箱的代价要小得多拆箱其实就是获取引用的过程,获取的这个引用指向了一个分配在托管堆上的对象中的值。需要注意的是,拆箱并不涉及复制的过程,所以将值从托管堆上的对象中复制到值类型实例中,是拆箱之后紧跟的一步复制过程,而非拆箱本身。

由于装箱和拆箱、复制会对程序的速度和内存空间产生不利的影响,甚至可能会由于频繁地操作托管堆而增加GC的次数,因此装箱和拆箱、复制的操作越少越好。在日常开发过程中,像ArrayList这样会触发装箱机制的类型并不常用,反而是List<T>更加常用。

简单介绍一下操作符new所作的事情?
  1. 计算所需内存空间,new操作符会计算目标类型和包括System.Object类在内的,其所有基类中定义的所有实例字段所需要的字节数。除此之外,为了方便Mono运行时管理对象,还有一些额外的信息需要托管堆为其分配空间,如类型对象指针和同步索引块。
  2. 完成计算对象所需的空间后,就要为对象在托管堆上分配所需要的内存空间了。分配的所有字节都设为0。
  3. 内存空间分配完,接下来需要初始化(在第1点中所提过的)对象的“类型对象指针”以及“同步块索引”。
  4. 当前3个准备步骤全部完成后,最后就要调用类型的实例构造器了。传递在使用new关键字时所指定的实际参数,例如上例中的“‘按钮’,texture”以及“10, 70, 150, 30”。此时编译器会在构造器中自动调用当前类型的基类构造器。每个类型的构造器都负责初始化该类型定义的实例字段。最终一定会调用 System.Object 的构造器,而该构造器仅仅是返回,而没有什么逻辑操作。
  5. 最后会返回指向新建对象的一个引用。也就是说新建的变量是一个指向某类型对象的引用,而非对象本身。在上例中的变量guic便是这样的角色。
    C#中引用类型总是从托管堆分配,而new操作符返回指向对象数据的内存地址。
引用类型的4点总结:
  1. 存储引用类型对象的内存空间须从托管堆上分配。
  2. 每一个对象都有一些额外的成员为Mono运行时提供操作该对象的信息,因而堆上分配空间时也要考虑到这些成员的空间,同时这些成员必须被初始化。
  3. 对象中的其他字段的字节总是0。
  4. 由于并没有一个和new操作符相对应的delete操作符存在,因而对象的回收工作主要由垃圾回收机制,也就是GC来处理。那么显而易见的是当为新的对象分配空间时,有可能会面临没有空间可用的情况,那么就会触发垃圾回收。
值类型和引用类型的区别
  1. 值类型与引用类型最大的区别,便是基于值类型的变量直接包含值。也就是说一个值类型变量赋值给另一个值类型时,将复制其包含的值。这与引用类型变量的赋值不同,引用类型变量的赋值只复制对象的引用,不复制对象本身。而值类型的优势,业主要体现在了提升日常开发中常用的、简单的类型的性能。
  2. 两种类型的另一个显著的差别就在于值类型不能派生出其他的类型,所有值类型都是隐式密封的,这么做的主要目的是为了防止将值类型用作其他类型的基类型。这导致的最直接的结果就是值类型无须提供额外的信息来表明它的实际类型,因此它没有引用类型中有的额外的信息。简而言之,相对于引用类型,值类型的使用缓解了托管堆的压力,并且减少了消耗巨大的垃圾回收的次数。
变量的值的内存空间究竟应该如何分配呢?

变量的值分配的位置与声明该变量的位置有关
局部变量的值总是存储在线程栈上实例变量的值则和实例本身一起存储在实例存储的地方
引用类型实例和静态变量总是存储在堆上
之所以说值类型的实例一般分配在线程栈而非全部都分配在线程栈,是由于在几种情况下,值类型也有可能分配在托管堆上
这些特殊的情况包括数组中的元素、引用类型中的值类型字段、迭代器块中的局部变量、闭包情况下匿名函数(lamda)中的局部变量。 这是由于在这几种情况下的值类型实例如果分配在线程栈上,有可能会出现线程栈中的方法已经调用结束,但是还会访问这些值的情况。也就是说如果分配在线程栈上,有可能会随着被调用方法的返回而被清除掉。因此它们也被分配在了托管堆上,以满足在方法返回之后还能够被访问的要求。
所以单纯地说“引用类型保存在托管堆上,值类型保存在线程栈上”是不准确的。将这句话一分为二看待,引用类型的确总是分配在托管堆上,但是值类型并非总是分配在线程栈上

哪些是引用类型?

任何被称为“类”的类型都是引用类型。并且通常使用以下3个关键字来申明一个自己定义的引用类型。

  • Class
  • Interface
  • Delegate
    还有C#中内建的引用类型。
  • Dynamic
  • Object
  • string
哪些是值类型?

值类型大体可以分为结构枚举两类。
结构可以分为以下3种。

  • 数字型结构:常见的有System.Int32结构、System.Float结构、System.Decimal结构等。
  • 布尔型结构:常见的无非是System.Boolean结构。
  • 用户自定义的结构。
    常见的枚举有System.IO.FileAttributes枚举、System.Drawing.FontStyle枚举等。这些都是值类型。
    所有的值类型都必须派生自System.ValueType,包括所有枚举的基类System.Enum也是从System.ValueType派生而来的。
各个阶段Unity 3D脚本所执行的方法
  1. 编辑器阶段
    • Reset方法:当脚本第一次添加到游戏对象或当执行Reset命令时会调用Reset方法,用来初始化脚本的各个属性。
  2. 场景第一次加载阶段
    该阶段其实也就是脚本第一次加载的阶段。以下两个方法会在脚本第一次加载时被调用。
    • Awake方法:在Start方法之前调用。换句话说,就是在prefab(预设)刚刚实例化之后便调用该方法。
    • OnEnable方法:这个函数在对象可用之后被调用,需要特别注意的是,仅在对象激活状态下可用,也就是当一个MonoBehaviour实例被创建时可用。例如,当加载一个场景或一个拥有该组件的游戏对象被实例化时。
      在此需要特别说明的一点就是,当前游戏场景中所有游戏对象身上的Awake方法和OnEnable方法会在所有的Start、Update等方法之前调用。
  3. 第一帧更新之前的阶段
    在该阶段,也就是Awake之后,Update之前,Unity 3D脚本会调用Start方法
    • Start方法:如果脚本实例为可用,则Start函数在第一帧更新之前被调用。
      需要注意的是,对当前场景中所有的游戏对象而言,Start方法会在所有脚本中的Update方法之前调用。
      以上3个方法,即 Awake()、OnEnable()以及Start()方法,完成了一个Unity 3D脚本的初始化工作。
  4. 执行阶段
    该阶段就是当前帧执行完毕,需要开始执行下一帧的阶段。在该阶段有可能被触发的事件方法是OnApplicationPause方法。
    • OnApplicationPause方法:当检测到暂停状态时,会在当前帧结束之后调用该方法。
  5. 更新顺序
    Unity 3D脚本更新的逻辑在MonoBehaviour中定义的3个更新方法中实现,分别是常见的Update方法,以及没有Update方法曝光率高,但也会用到的FixedUpdate方法和LateUpdate方法。通过这3个Unity 3D脚本提供的更新方法,可以很方便地实现游戏对象的逻辑。
    • FixedUpdate方法:也许很多读者或者使用Unity 3D的游戏开发者常常会有一个误区,即按帧执行逻辑,即每帧执行一次已经是逻辑执行的最小单位了,其实这是错误的。在Unity 3D中,FixedUpdate方法比按帧执行的Update方法调用的次数可能更多,调用的频率可能更频繁。这是由于当帧率比较低的时候,该方法每帧会被调用多次。如果帧率比较高时,则它可能不会每帧都被调用。而这也是因为FixedUpdate()方法是基于可靠的定时器的,不受帧率的影响。因此,FixedUpdate()方法主要用来处理物理计算的相关逻辑,例如处理刚体。
    • Update方法:在游戏运行期间,每一帧都会调用Update()方法。也是游戏逻辑按帧更新的主要方法。
    • LateUpdate方法:在Update()执行后,LateUpdate()也是每帧都被调用。在Update()中执行的任何计算都会在LateUpdate()开始之前完成。LateUpdate()的一个常见应用就是第三人称控制器的相机跟随。如果你把角色的移动和旋转放在 Update()中,那么就可以把所有相机的移动旋转放在 LateUpdate()中。这是为了在相机追踪角色位置之前,确保角色已经完成移动。或者说选择使用LateUpdate更普遍的情形是:用来处理发生在Update方法之后,但是在相机渲染之前的逻辑。
协程

unity3D种游戏引擎的游戏脚本是单线程的,为了实现多线程的功能,引入了协程的功能。
协程功能的实现则主要利用了C#语言的迭代器。

渲染

在使用Unity 3D进行游戏开发时,往往会遇到当一个物体不可见时,该物体上绑定的脚本依然会执行的情况,这种情况无疑会增加游戏执行的开销。那么如何使不可见的游戏对象上的游戏脚本不再执行,而当它再次可见的时候,再使其身上的游戏脚本继续执行呢?这就用到了OnBecameVisible方法和OnBecameInvisible方法。

using UnityEngine;
using System.Collections;
public class ExampleClass : MonoBehaviour
void OnBecameVisible() {
	enabled = true;
}
void OnBecameInvisible() {
	enabled = false;
}

在这个例子中,当该游戏对象对任何一个相机来说都不可见时,Unity 3D游戏引擎会发出一个消息,来通知绑定在该游戏对象身上的游戏脚本来触发OnBecamelnvisible方法,在这个方法中,对enabled变量赋值false来禁用该脚本,从而达到了游戏对象不可见时,其游戏逻辑不继续执行的目的。同样,当该游戏对象对相机可见时,Unity 3D游戏引警会发出另一个消息,来通知该游戏对象身上的脚本来触发OnBecameVisible方法,在这个方法中,反过来对enabled变量赋值true,从而启用该脚本,使得该游戏对象的逻辑得以继续执行。

关于Unity 3D脚本的结构,可以得出一个大概的执行顺序
  1. 调用所有的Awake方法
  2. 调用所有的Start方法
  3. 游戏逻辑循环 (物理部分,主要根据设定的时间间隔来确定)
    1. 调用所有的FixedUpdate方法
    2. 物理模拟。
    3. OnEnter、Exit、Stay触发函数
    4. OnEnter、Exit、Stay碰撞函数
  4. 刚体插值,主要作用于位置transform.position和角度transform.rotation.
  5. OnMouseDown、OnMouseUp等输入事件
  6. 所有Update函数。
  7. 高级动画、混合并应用到变换
  8. 所有LateUpdate函数
  9. 渲染。
  10. 对象销毁或退出场景。

Unity3D游戏脚本种的值类型

Vector2、Vector3、Vector4

点乘积和交叉乘积

向量的交叉乘积以及点乘积的区别

两者最大的区别就是点乘积计算的结果为数值,而交叉乘积计算的结果仍为向量

点乘积的几何公式

点乘积的计算方式为: a·b=|a|·|b|cos<a,b>,其中|a|和|b|表示向量的模,<a,b>表示两个向量的夹角。另外在点乘积中,<a,b>和<b,a> 夹角是不分顺序的。所以通过点乘积,其实是可以计算两个向量的夹角的。

点乘积的作用

使用点乘积的一个作用就是通过计算两个向量的点乘积,可以粗略地判断当前物体是否朝向另外一个物体,只需要计算当前物体的transform.forward向量与(target.transform.position-transform.position)的点乘积即可,大于0则面对另外一个物体,否则是背对着。

交叉乘积
定义

c=a*b,其中a、b、c均为向量。即两个向量的交叉乘积得到的还是向量。

向量的交叉乘积的3条性质
  1. c⊥a,c⊥b,即向量c垂直与向量a、b所在的平面。
  2. 模长|c|=|a||b|sin<a,b>
  3. 满足右手法则。有a*b≠b*a,而a*b = -b*a,可以使用交叉乘积的正负值来判断向量ab的相对位置,即向量b是处于向量a的顺时针方向还是逆时针方向。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值