Unity游戏开发学习——基础知识

1.类的三大特性

在面向对象编程中,类的三大特性是封装、继承和多态。

  1. 封装(Encapsulation):封装指的是将数据和操作数据的方法包装在一个单元中,通过对外提供公共接口来访问数据,同时隐藏内部实现的细节,使得代码更加易读、易用和易于维护。这样可以保证数据的安全性和一致性,并且可以更好地组织和管理代码。

  2. 继承(Inheritance):继承是指一个类可以继承另一个类的属性和方法。被继承的类称为父类或基类,继承的类称为子类或派生类。子类可以继承父类的特性,并且可以在此基础上进行扩展或修改。

    • 代码的重用和扩展:子类可以继承父类的属性和方法,避免了重复编写代码,提高了代码的复用性。子类还可以在继承基础上进行扩展和修改,实现代码的灵活性和可扩展性。
    • 统一的代码结构和接口:继承可以建立类之间的层级关系,使得代码更具有组织性和可读性。子类可以通过重写父类的方法,实现不同对象的不同行为,同时保持了统一的方法接口。
  3. 多态(Polymorphism):多态指的是同一种操作可以在不同的对象上具有不同的表现形式。具体来说,多态允许使用父类类型的变量来引用子类类型的对象,通过方法的重写和重载来实现不同对象的不同行为。

    • 简化代码的逻辑和维护:多态允许使用父类类型的变量来引用子类类型的对象,通过统一的方法调用方式,简化了代码的逻辑,使得代码更加易读和易于维护。
    • 提高代码的灵活性和可扩展性:通过多态,可以在运行时根据对象的实际类型来决定调用哪个类的方法,实现了动态绑定。这样可以在不修改原有代码的情况下,通过添加新的子类来扩展功能。

这三大特性共同构成了面向对象编程的基础,它们使得代码更加结构化、可维护和可扩展,同时提高了代码的重用性和灵活性。

高内聚,低耦合

高内聚(High Cohesion)和低耦合(Low Coupling)是软件设计中的两个重要原则,用于指导模块、组件或类的设计方式。下面对这两个概念进行解释:

  1. 高内聚(High Cohesion):高内聚是指一个模块、组件或类内部的各个元素相互关联紧密,共同完成特定的功能或任务。高内聚的设计使得模块内的功能职责清晰,并且各个元素之间的依赖关系较强。

    高内聚的特点:

    • 模块或类内的功能相关性强,每个元素都是为了完成同一类任务而设计。
    • 模块或类内部的协作较多,各个元素之间的控制流程简单且清晰。
    • 修改某个元素时,影响范围较小,不会对其他元素产生太大影响

    高内聚的好处:

    • 提高代码的可读性和可维护性。
    • 方便重用和测试。
    • 减少模块间的依赖,提高系统的灵活性和扩展性。
  2. 低耦合(Low Coupling):低耦合是指模块、组件或类之间的相互依赖关系较弱,彼此之间的关联关系尽可能减少。低耦合的设计使得模块、组件或类可以独立开发、测试和修改,提高系统的灵活性和可维护性。

    低耦合的特点:

    • 模块或类之间的依赖关系少,相互之间尽量减少直接引用。
    • 使用抽象接口或中间层来解除直接依赖,提供松耦合的方式进行交互。
    • 模块或类之间的通信通过松散的接口或消息传递完成。

    低耦合的好处:

    • 提高系统的灵活性和可扩展性,模块或类的替换和重用更容易。
    • 减少修改某个模块或类时对其他模块或类的影响。
    • 降低系统的复杂度,便于理解和维护。
      通过高内聚和低耦合的设计,可以提高代码的可读性、可维护性和可扩展性,降低模块或类之间的相互影响,从而构建出更优雅和可靠的软件系统。

2.值类型和应用类型区别

C#中常见的一些数据类型:

  1. 值类型:

    • 数值类型:整型(byte、sbyte、short、ushort、int、uint、long、ulong)、浮点型(float、double、decimal)
    • 字符类型:字符类型(char)
    • 布尔类型:布尔类型(bool)
    • 结构体类型:结构体(struct)
    • 枚举类型:枚举(enum)
  2. 引用类型:

    • 字符串类型:字符串(string)
    • 数组类型:数组(array)
    • 类类型:类(class)
    • 接口类型:接口(interface)
    • 委托类型:委托(delegate)
    • 对象类型:对象(object)

除了以上基本数据类型,还可以通过组合、泛型等方式创建和使用其他复杂的数据类型。

值类型引用类型
存储方式值类型的变量存储的是实际的数据值。当值类型的变量被赋值给另一个变量或作为参数传递给方法时,会复制该值的副本。对副本的操作不会影响原始值引用类型的变量存储的是对象的引用或地址,而不是实际的数据值。当引用类型的变量被赋值给另一个变量或作为参数传递给方法时,复制的是引用或地址,两个变量引用同一个对象。对于引用类型的副本和原始对象的操作都会影响到同一个对象。
内存分配值类型的变量通常直接存储在栈(Stack)上,它们的内存分配和销毁都是自动的,且具有较高的效率。引用类型的对象通常存储在堆(Heap)上,它们的内存分配和销毁需要由垃圾回收器(Garbage Collector)来管理,引用类型的变量存储在栈上,实际存储的是对象的引用。
传递方式值类型在赋值给其他变量或作为参数传递给方法时,会进行复制,因此会占用更多的内存空间。对于大型值类型的传递,可能会带来性能上的开销。引用类型在赋值给其他变量或作为参数传递给方法时,只是复制了引用,它们共享相同的对象,因此不会占用额外的内存空间。

3.GC是什么,为什么会产生GC,怎么避免GC

GC(Garbage Collection)是一种自动内存管理机制,它负责在程序运行时自动检测和释放不再使用的内存。GC通过标记和回收垃圾对象来实现内存的回收和重用,以避免内存泄露和提高内存使用效率。

产生GC的原因主要有以下几点:

  1. 动态分配内存:在程序运行过程中,可能会动态地创建对象和分配内存空间。如果没有及时释放不再使用的内存,将会导致内存泄露。

  2. 对象的生命周期:程序中的对象会在不同的时刻被创建和销毁。一些对象可能只在局部范围内使用,一旦超出范围就不再需要。如果没有及时销毁这些不再使用的对象,将会引发内存泄露。

  3. 内存管理方式:对于手动管理内存的编程语言(如C、C++),如果程序员没有正确地进行内存分配和释放,容易导致内存泄露和内存访问错误。

为了避免GC或减少GC的发生,可以采取以下策略:

  1. 尽量减少对象的创建:可以通过重用对象、使用对象池等方式来避免频繁地创建和销毁对象,减少内存管理的开销。

  2. 及时释放不再使用的对象:当某个对象不再被使用时,可以手动将其置为null或显式调用析构函数来释放其占用的内存空间。

  3. 尽量使用局部变量:将对象的作用域限制在局部范围内,使得对象超出作用域后可以被自动销毁。

  4. 避免循环引用:循环引用是指多个对象相互引用,形成一个环状结构,导致它们之间无法被垃圾回收器正确地识别和释放。可以通过断开循环引用、使用弱引用(Weak Reference)等方式来解决循环引用问题。

  5. 避免大对象和大数组:大对象和大数组占用大量内存空间,容易导致内存碎片等问题。可以将大对象拆分为多个小对象,或者使用流式处理等方式来避免一次性加载大量数据。

需要注意的是,GC是一种自动化的内存管理机制,通常由编程语言或运行时环境提供支持。在大多数情况下,我们无需过多关注GC的具体实现细节,只需要编写良好的代码,及时释放不再使用的对象,并避免常见的内存泄露情况即可。

4.MonoBehaviour的生命周期有那些,它们之间的执行顺序是什么

  1. Awake
    Awake方法在对象被创建后立即被调用,用于初始化对象的状态和变量。不同对象的Awake方法会按照它们在场景中的出现顺序依次执行。

  2. OnEnable
    OnEnable方法在对象被激活时调用,可用于启用相关的组件、注册事件或进行其他初始化操作。

  3. Start
    Start方法在对象的第一个更新帧之前调用,常用于执行一些只需在游戏开始时执行一次的逻辑。

  4. FixedUpdate
    FixedUpdate方法在固定时间间隔内被调用,用于处理物理相关的计算和操作。它与Update方法的不同之处在于,它以固定的时间间隔进行调用而不受帧率的影响

  5. Update
    Update方法在每一帧被调用,用于处理游戏逻辑的更新。例如,处理玩家输入、移动物体、更新游戏状态等。

  6. LateUpdate
    LateUpdate方法在Update方法之后被调用,在每一帧更新之后执行。一般用于处理在Update中可能修改了位置或旋转的逻辑。例如,相机跟随玩家移动。

  7. OnDisable
    OnDisable方法在对象被禁用时调用,通常用于执行资源的释放、取消事件的注册等清理操作。

  8. OnDestroy
    OnDestroy方法在对象被销毁之前调用,用于进行最后的清理工作。例如,释放占用的资源、取消注册的事件等。

5.Array,List,Dictionary之间的区别是什么

是什么大小存储
Array(数组)数组是一种固定长度且连续存储的数据结构,可以在内存中按索引快速访问元素。数组的长度在创建时确定,无法动态增加或减少。数组可以具有多个维度数组可以存储任意类型的元素,包括基础类型、对象等。
List(列表)列表是一种动态长度的数据结构,可以根据需要动态增加或减少元素。只具有一个维度,但可以使用数组列表或列表的列表。列表可以存储任意类型的元素,通过索引访问和修改。
Dictionary(字典)字典是一种键值对(Key-Value)的数据结构,每个元素都由一个唯一的键和对应的值组成。字典的元素无序存储,通过键快速访问对应的值。 只具有一个维度。字典可以存储不同类型的键和值,键和值的类型可以不同。

数组可以具有多个维度,而 ArrayList或泛型List始终只具有一个维度,但是我们可以轻松创建数组列表或列表的列表。
在决定使用泛型List还是使用ArrayList 类(两者具有类似的功能)时,记住泛型List类在大多数情况下执行得更好并且类型安全。如果对泛型List类的类型使用object类型时,则两个类的行为是完全相同的。

总结:

  • Array是固定长度的连续存储数据结构,适合静态集合
  • List是动态长度的数据结构,可以动态增加或减少元素
  • Dictionary是键值对的数据结构,用于快速查找和访问

6.ref和out有什么区别,原理是什么

使用ref和out关键字声明的参数是双向传递的,既可以作为输入参数也可以作为输出参数。

ref关键字:

  • 在进入方法之前必须被初始化,即在方法调用之前必须为其赋值。

out关键字:

  • 在进入方法之前不需要被初始化,即可以在方法内部为其赋值。

原理:
ref和out关键字的原理是通过将参数的引用传递给方法,在方法内部对参数进行修改后,方法外部的变量也会受到影响。

总结起来,主要区别在于在函数调用之前是否要对参数进行初始化。使用时应根据具体情况选择适当的关键字。

需要注意的是,在使用ref和out关键字时,调用方法和方法签名的定义必须严格一致,包括参数的数量和类型。否则会导致编译错误。

7.C#的装箱和拆箱是什么,在什么情况下需要使用

  1. 装箱(Boxing):将值类型转换为对象类型。当将值类型赋值给一个对象类型变量或将值类型作为参数传递给接受对象类型的方法时,会触发装箱操作。装箱会创建一个新的对象,并将值类型的值复制到新对象中。

    int i = 10;
    object obj = i; // 装箱操作
    
  2. 拆箱(Unboxing):将对象类型转换为值类型。当从对象类型中提取值类型的值或将对象类型赋值给一个值类型的变量时,会触发拆箱操作。拆箱会从存储在对象中的值中创建一个副本,并将其存储在值类型变量中。

    object obj = 10;
    int i = (int)obj; // 拆箱操作
    

装箱和拆箱操作会带来一定的性能开销,因为涉及到对象的创建和拷贝。因此,在以下情况需要使用装箱和拆箱:

  • 当需要将值类型存储到对象集合(如ArrayList、List等)中时,需要进行装箱操作。
  • 当需要将值类型作为参数传递给接受对象类型的方法时,需要进行装箱操作。
  • 当从对象集合中获取值类型的值或将对象类型转换为值类型时,需要进行拆箱操作。

在一些性能要求较高的场景,尽量避免过多的装箱和拆箱操作,可以使用泛型集合(如List)来避免装箱,或者使用合适的值类型来避免拆箱。此外,还可以使用接口或基类来规避装箱和拆箱,提高代码的性能和效率。

8.C#如何检测敌人是否在视线范围内

在C#中,若要检测敌人是否在视线范围内,可以使用以下步骤:

  1. 获取玩家或角色的位置和朝向信息。

    Vector3 playerPosition = player.transform.position; // 获取玩家位置
    Quaternion playerRotation = player.transform.rotation; // 获取玩家朝向
    
  2. 获取敌人的位置信息。

    Vector3 enemyPosition = enemy.transform.position; // 获取敌人位置
    
  3. 计算玩家与敌人之间的距离。

    float distance = Vector3.Distance(playerPosition, enemyPosition); // 计算玩家与敌人之间的距离
    

点乘:

    Vector3 relativePosition = enemyPosition  - playerPosition; // 先计算出敌人想对自身的位置信息
    Vector3 cubeForward = player.transform.forward;	// 再使用自身正方向与相对方向两个向量做点乘的相关运算
    // 计算两个向量的点乘
	// 如果大于0说明敌人在自身前面
	// 如果小于0说明敌人在自身后面
	// 如果等于0说明敌人在自身左右
	float result = Vector3.Dot(cubeForward, relativePosition);
	
	// 得到两个向量后,可以直接计算其夹角(两个向量的夹角)
	float angle = Vector3.Angle(cubeForward, relativePosition);
		
	// 当两个向量的长度都为1时,点乘的结果就是夹角的余弦值
	float cos = Vector3.Dot(cubeForward.normalized, relativePosition.normalized);
		
	// 通过反余弦函数得到两个向量的弧度
	float radians = Mathf.Acos(cos);
		
	// 弧度值通过数据库转换成角度值
	angle = radians * Mathf.Rad2Deg;
  1. 根据距离判断敌人是否在视线范围内,可以使用视线判断函数或射线投射。

    bool isInLineOfSight = CanSeeEnemy(playerPosition, playerRotation, enemyPosition); // 使用视线判断函数
    // 或
    bool isInLineOfSight = CheckLineOfSight(playerPosition, enemyPosition); // 使用射线投射
    
    • 方法一:视线判断函数(CanSeeEnemy):这是一个自定义函数,使用玩家位置、朝向和敌人位置等信息,根据特定的视线范围和碰撞检测规则,判断敌人是否在视线范围内。

    • 方法二:射线投射(CheckLineOfSight):使用Physics.Raycast或Physics.RaycastAll函数,从玩家位置发射射线到敌人位置,检测射线是否遇到障碍物。如果没有遇到障碍物(如墙壁),则说明敌人在视线范围内。

9.position和localposition的区别,二者如何相互转换

区别:

  1. position:表示对象在全局坐标系中的位置,即相对于场景原点(0, 0, 0)的位置。
  2. localPosition:表示对象在局部坐标系中的位置,即相对于其父对象的位置。

相互转换:

  1. 将localPosition转换为position:可以通过递归地获取父对象的position并相加,以计算出全局坐标系中的位置。

    Vector3 worldPosition = transform.position + transform.parent.position;
    
  2. 将position转换为localPosition:可以通过递归地获取父对象的position并相减,以计算出相对于父对象的位置。

    Vector3 localPosition = transform.position - transform.parent.position;  //全部父物体的position
    

要注意的是,当对象的层次结构中包含旋转、缩放等转换时,使用递归计算localPosition可能会导致不准确的结果。在这种情况下,可以使用Transform的InverseTransformPoint方法将globalPosition转换为localPosition。

Vector3 localPosition = transform.InverseTransformPoint(worldPosition);

总结来说,通过递归获取父对象的position并进行加减运算可以实现position和localPosition之间的转换。另外,也可以使用Transform的方法来直接进行转换。

10.destroyimmediate和destroy的区别

  • DestroyImmediate立即对对像进行销毁;

  • Destroy销毁场景中的物体,但内存中还存在,当令它需要销毁时,只是给一个标识。而内存中它依然是存在的。

  • 只有当内存不够,或一段时间没有再次被引用时(或者更多合理的条件满足),机制才会将它销毁并释放内存。

  • 这样做的目的就是为了避免频繁对内存的读写操作。回收器会定时清理一次内存中引用计数为0的对象,很可能你的要销毁的对象在其他地方还有引用而你自己不清楚,直接销毁可能导致其他地方空引用错误