U3D面试汇总!!!

Posted Shawn的代码日常

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了U3D面试汇总!!!相关的知识,希望对你有一定的参考价值。

文章目录

篇章一:C#语言核心

0.C#语言特性

  • ① 微软开发的一种OOP编程语言。专门用于.NET应用而开发。
  • ② 面向对象的三大特征(封装,继承,多态)。除了三大特征以外,C#还添加了事件和委托,增强了编程的灵活性。
  • 简单,安全。C#不再使用指针,而且不允许直接读取内存等不安全操作。提供了相比C/C++,Java更多的数据类型。且还使用命名空间的概念来管理C# 文件。
  • ④ C# 6.0+版本已支持跨平台使用。并且能开发多种类型维度的应用程序。

1.值类型与引用类型

1.1. 介绍:

值类型:int,bool,float,double,char,enum,struct,short,long…

引用类型:class,object,interface,string,array,delegate+

1.2. 区别:

存储方式:值类型—>栈,引用类型—>堆,引用类型会在栈中存储指向堆数据的内存地址。

存储速度:值类型快,引用类型慢。

存储内容:值类型存储实际数据,引用类型存储指向 内存堆中的指针与引用。

释放方式: 值类型可以在栈中自动释放,引用类型需要在堆中GC释放。

继承类:值类型 :System.ValueType : System.Object,引用类型:System.Object。

1.3. 底层

① 引用类型实例化时,先在栈中开辟空间,用于存储堆中对象地址,然后在堆内开辟空间,存储引用对象。

② 值类型直接在栈中开辟空间,值类型的引用地址也在栈空间中。

在参数对象进入方法体时,实则是在栈中开辟了新的临时空间(对应参数的副本),当栈内值类型修改时,由于栈中内存地址不同,无法影响到本体。而引用类型的存储数据是一个堆内的地址,所以对于引用类型的修改是直接修改堆内对象。

④ 值类型变量中引用类型在堆中(struct中的string属性),引用类型中值类型数据也在堆中(类中的int属性)。

⑤ 内存分为一下三个模块:

  • 栈:存储值类型变量,以及对象内存地址
  • 堆:存储引用类型数据
  • 静态存储区:存储静态的变量以及方法。

2.string类型

2.1. 介绍:

string的修改,实则都是new 了一个新的string,在堆内开辟新的空间,此时栈内的副本也会指向新的对象。

string s = "富强民主";//当前堆内存:“富强民主”
s = "文明和谐";//当前堆内存:“富强民主”,“文明和谐”
s = "自由平等";//当前堆内存:“富强民主”,“我们和谐”,“自由平等”

2.2. StringBuilder和StringBuffer:

为了解决字符串修改重复创建的问题,当频繁的对一个字符串进行修改时,使用StringBuilder代替string。 StringBuilder非线程安全,性能略好,用于单线程`,StringBuffer线程安全,用于多线程

2.3. StringBuilder一定比string性能好吗?

不一定。极少拼接的情况下 性能:string > StringBuilder,由于初始内存消耗:string < StringBuilder,大量拼接的情况下 性能:string < StringBuilder。

2.4. 字符串池

字符串池 是CLR(公共语言运行库)一种针对反复修改字符串对象的优化措施,能减少一定性能消耗。原理是 内部开辟容器通过 键值对 的形式注册字符串对象,键 存储字符串对象的内容,值 存储字符串在堆上的引用。这样当新建字符串时,回去检查,如果不存在就在这个容器中开辟新的空间存放字符串。(废弃资源的回收利用)

3.GC(垃圾回收)

3.1. 介绍:

3.1.1 Unity内部存在两个内存管理池 ↓

栈内存 主要用来存储较小的和短暂的数据(值类型变量,临时变量…)

堆内存 主要用来存储较大的和存储时间长的数据(引用类型变量,对象实例…)

unity中的变量只会在堆内存或栈内存上进行内存分配。非堆即栈。

3.1.2 变量的激活与否 ↓

当变量处于激活状态时,则其占用内存被标记为使用状态,该部分内存被标记为分配状态。

当变量未激活时,其占用内存不再需要,该部分内存可以被会受到内存池中再次使用。该操作即为GC。栈内存回收极其迅速,堆内存并不是及时回收的,此时其对应内存仍为使用状态,不再使用的内存只会在GC的时候被回收。

3.1.3 GC的定义

GC(垃圾回收)主要指堆上的内存分配与回收,unity会定时对堆内存进行GC操作。

3.2. GC算法:

C#:C#使用分代算法来进行GC操作。有着 内存整理,避免碎片化,压缩等特点。

分为以下三代:

  • 0代:未被标记回收的新分配对象。
  • 1代: 上次垃圾回收中没有被回收的对象。
  • 2代: 一次以上的垃圾回收后依然没有被回收的对象。

简易流程:

  1. GC会检查堆内存上的每个存储变量。
  2. 对每个变量检测其引用是否处于激活状态。
  3. 如果变量引用变为不激活,则会标记为可回收
  4. 被标记的变量会被移除,其占有的内存空间被回收到堆内存上。

详细流程:

  1. 当新建立引用对象时,检查0代存储空间是否充足,如果没有,将0代对象进行遍历检查,是否有 激活状态却没有被调用的对象 标记为 可回收。
  2. 遍历完成后,将符合 “可回收” 的对象进行GC,释放的空间返回给0代存储区,其他对象迁移到1代存储区,此时对象是分散分布的,需要进行 压缩 操作,是1代对象顺序紧密排列。新对象存储于0代存储空间。
  3. 当1代空间溢出时,将1代对象同样按照上述流程 遍历,迁移,压缩 到 2代存储区,同时0代迁移到1代。

3.3. GC存在的问题

  1. 游戏性能:GC是一个及其耗费时间的操作,堆内存上变量较多时会导致遍历检查操作变得十分缓慢,使得游戏卡顿(FPS下降),对玩家体验造成极大的影响。
  2. 游戏内存:(unityGC采用非分代非压缩的标记清除算法)GC会产生“内存碎片化”。(例如:房子很大,但可利用房间很小,无法找到合适的房间)。“内存碎片化” 会导致 GC或者堆内存扩容。二者都会时游戏运行效率降低或者游戏内存增大。

3.4. GC的触发时机

  1. 在堆内存上进行内存分配操作,内存不够时会触发GC。(常用)
  2. GC会以一定的频率自动触发。
  3. GC可以被强制执行。

3.5. 如何避免GC

  1. 减小临时变量的使用,多使用公共对象,多利用缓存机制。(将容器定义到函数外)。

  2. 减少new对象的次数。

  3. string和StringBuilder的转换使用(详情见2)

  4. 定义容器时,尽量将空间大小设置为足够,减少容器的扩容

  5. 帧更新函数进行计时器控制,防止内存爆炸。此外,可以选择在加载进度条中手动GC

  6. 缓存对象池:常用于经常摧毁创建的对象,比如子弹,特效等…。将Destroy换成SetActive(false)。将Instantiate换位SetActive(true)

  7. 减少拆 / 装箱:

    装箱:value —> object 拆箱:object —> value。

    产生GC的原因:装箱时,对于值类型会在堆内存上分配一个Object类型的引用来封装该值类型变量,其对应缓存就会产生垃圾。

    多用泛型!处理多个代码对不同数据类型执行相同指令操作时!

    **在使用协程时,yield return 0 —> yield return null ** 能有效避免装箱。

    项目打包之前,删掉所有的Log。

4. 面向对象三大特征

4.1. 封装

将数据和行为相结合,通过行为约束代码修改数据的程度,增强数据安全性。(属性是C#封装特效的最好实例)。

4.2. 继承

提高代码复用度,增强软件可维护的重要手段,符合开闭原则(见设计模式)。继承就是把子类的共性聚集起来提取出一个父类,C#的继承是单继承,但具有传承性(这一点上与Java一致)。

4.3. 多态

同名的方法在不同环境下,会自适应转换为不同的逻辑表现。(例子:猫在走猫步,鸟儿在飞翔…)

5.访问修饰符

  1. public :对任何类和成员都公开,无限制访问。
  2. private :仅对该类公开。
  3. protected :对该类及其派生类公开访问。
  4. internal:只能在包含该类的程序集中访问该类。
  5. protected internal:具有上述两个子修饰符的特征。

6.密封关键字sealed

① 当该关键字修饰类时,该类无法被继承。

② 当该关键字修饰方法时,该方法无法被重写。常常配合override关键字一起使用。

总结:sealed关键字的存在限制了方法或类的向下拓展。可以理解为绝育。即规定最终版,无法再去重写方法。

public class B : A
    protected override void M ()
    protected sealed override void N () 


public sealed class C : B 
    protected override void M ()
    //无法重写N方法了,因为在父类中已经被密封

//无法再继承C类了

7. 结构体和类

7.1. 对比:

数据类型:结构体是值类型,类是引用类型。结构体存在栈中,类存在堆中。

参数传递

结构体参数传递时是值类型的传递,函数中改变参数的值,结构体对象值是不变的。类参数传递时是引用的传递。

C#中结构体定义时,无法给参数赋值。而类可以使用构造函数给成员变量初始化。

构造函数

结构体无法声明无参构造,且有参构造不会顶替掉无参构造。

类可以声明无参构造,但如果写了有参构造,其会顶替调无参构造。

结构体需要在构造函数中初始化所有成员变量,而类随意。

修饰与特性

结构体无法被 static 修饰,而类可以。

结构体无法被继承,而类可以。

结构体中无法声明析构函数,而类可以。

7.2. 使用环境:

结构体

① 结构体是值类型在栈中,存取数据比堆快,容量小。适合去轻量级对象,比如:点,矩形,颜色…

② 如果对象是数据集合,优先考虑结构体,比如集合,坐标。

③ 变量传值时,希望传递对象是拷贝,而不是对象的引用地址,可用结构体。

①类是引用对象,存在堆中,容量大,适合重量级对象。

② 如果对象需要使用继承多态等面向对象特征,使用类,比如玩家 ,怪物…

8.抽象类和接口

8.1. 对比

① 接口不是类,无法被实例化(无构造函数和析构函数),抽象类只能间接实例化(实例化其子类时自动实例化父抽象类)抽象类有构造函数。

② 接口只能做方法声明,抽象类既可以做方法声明也可以做方法实现。

③ 接口只能包含抽象成员,完全抽象。而抽象类可以有实现成员,属于部分抽象。

④ 接口需要被子类实现,而抽象类需要被子类继承。

⑤ 接口中成员只能是public因此可以省略。抽象类随意。

⑥ 接口可以实现多继承,而抽象类只能实现单继承。

⑦ 抽象方法(抽象类和接口都有)需要被实现,故不能是静态也不能是私有。

8.2. 使用环境

抽象类适合做某个领域的固有属性(把对象的共同点抽象提取出来)。而接口是用来定义某个领域的扩展功能。

抽象类:

  1. 当多个类中有重复部分的时候,我们可以根据其共同特征提取出一个基类,希望这个基类不能被实例化,就可以将该基类设计为抽象类。
  2. 当需要为一些类提供实现代码时,可优先考虑抽象类,因为抽象类中的非抽象方法可以被子类继承下来,使代码功能更简单。

接口:

当注重代码的扩展性和可维护性时,优先使用接口

  1. 接口和其实现类可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类使用更加方便。
  2. 接口只关心对象之间交互的方法,而不关心对象所对应的具体类。接口是程序间的一个协议,比抽象类的使用更加安全清晰。

9. 静态构造函数

9.1. 样例

//静态构造函数
class Student
    public static string type;
    static Student()
        //常常用于静态变量的赋值
        type = "学生";
    
    public void Study()


//调用时机
Student.type;
new Student().Study();

9.2. 特点

① 静态构造函数既没有访问修饰符,也没有参数。

② 在创建第一个实例或任何静态成员被引用时,.NET将自动调用静态构造函数来初始化类。

③ 一个类只能有一个静态构造函数并且最多仅运行一次。

④ 静态构造函数无法被继承。且如果类中没有该模块,但又在程序运行生存期内需要初始化静态成员,编译器会自动生成该语句块。

⑤ 如果该模块语句发生一场,运行时不会调用该构造函数,并且在程序运行盛器内,该类型保持未初始化。

10.虚函数实现原理

每个虚函数都会有一个 虚函数表。该虚函数表实质是一个 指针数组。存放每一个对象虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数同时增加自己的虚函数入口地址,如果派生类重写了虚函数,则继承过来的虚函数入口地址将被派生类的重写虚函数入口地址代替。程序会发生动态绑定,将父类指针绑定到实例化的对象上从而实现多态。

11. 指针和引用的区别

为空:引用无法为空,指针可以指向空对象。

初始化相关:引用必须初始化且初始化后无法改变,指定对那个对象的引用。而指针无需且可以改变指向。

访问相关:引用可以直接访问对象,指针只能间接访问对象。

数据大小引用的大小是指所引用对象的大小。而指针大小是本身的大小,通常为4字节

其他特性

  1. 引用没有const,而指针有。
  2. 引用和指针的自增运算意义不同。
  3. 引用无需分配内存空间,指针需要。

12. ref & out

12.1. 作用:

解决值类型和引用类型在函数内部改值或重新声明能够影响外部传入的变量也被修改。

12.2. 区别:

ref:ref传入的变量必须初始化但是在内部可改可不改。

out:out传入的变量可以不用初始化,但在内部必须修改其值。

案例:

RaycastHit hitInfo;
Physics.Raycast(ray,out hitInfo);

13. 委托 & 事件

13.1 委托的介绍:

委托是约束集合中的一个类,相当于一组方法列表的引用,可以便捷地使用委托对方法集合进行操作。委托是对函数指针的封装。

13.2 委托和事件的区别:

关系

事件可以看作是委托的一个变量。事件是基于委托存在的,事件是委托的安全包裹,让委托的使用更具有安全性。

区别

声明 委托可以用 “ = ” 来赋值,事件不可以。

调用范围 委托可以在声明他的类外部调用,而事件只能在其内部调用。

委托是一个类型,而事件只是一个对象

④ **事件的本质就是 一个private 的委托与Add,Remove两个方法。**只能对自己进行 +=-= 操作。

14. C#数据类型汇总

14.1 布尔类型

bool,true / false,占一个字节。(bool -> System.Boolean),

14.2 有符号整数类型

sbyte , 占一个字节。(sbyte -> System.Byte),-128 - 127

short, ,占2个字节。(short -> System.Int16),-32768 - 32767

int,,占4个字节。(int -> System.Int32), -2147483648 - 2147483647

long,,占8个字节。(long -> System.Int64),大约1E+20。

14.3 无符号整数类型

byte , 占1个字节。(byte -> System.Byte),0 - 255

ushort,占2个字节。(ushort -> System.UInt16),0 - 65535

uint,占4个字节。(uint -> System.UInt32),0 - 4294967295

ulong,占8个字节。(ulong -> System.UInt64),大约1E+20。

14.4 浮点型

float,占4个字节。(float -> System.Single)

double,占8个字节。(double -> System.Double)

14.5 字符型

char ,占2个字节。(char -> System.Char)

15.new的底层实现

案例:

Student s = new Student();
  1. 在应用程序堆中创建一个引用类型对象实例,并为其分配内存地址。
  2. 自动传递该实例的引用给构造函数(构造函数中能使用 this.属性 的原因)。
  3. 调用该实例的构造函数。
  4. 返回该实例的引用内存地址,赋值给s变量,该s引用对象保存到数据是指向在堆上创建该类型的实例地址。

16.协变与逆变

协变(out):

和谐,自然的变化。

里氏替换原则中,父类型容器可以装在子类型对象,子类可以转换为父类。感受是和谐的。

逆变(in):

逆常规,不正常的变化。

里氏替换原则中,子类对象不能装载父类对象。所以父类转换为子类,感受是不和谐的。

协变和逆变是用来修饰泛型的。只有泛型接口与委托能用。

案例:

//out修饰的泛型,只能作为返回值
delegate T Test<out T>();
//in修饰的泛型,只能作为参数
delegate T Test<in T>(T t);

17.反射

作用:

在程序加载运行时,动态获取程序集,并且可以获取到程序集的信息反射在运行时动态获取类,对象,方法,对象数组数据的一种重要手段。

优点:允许在运行时发现并使用编译时仍解决不了的类型和成员。

缺点:

1.根据目标类型字符串搜索扫描程序集的元数据时间较长。

2.反射调用方法或者属性比较耗时。

通过反射获取实例:

反射可以直接访问类的构造,直接通过 getConstructor,访问这个构造函数,然后通过不同的参数列表,就可以具体定位到哪一个重载,通过这个方法,去得到类的实例,把对象就拿到了。

18.删除List中元素时需注意什么

在遍历删除List中的元素时,会导致List.Count发生变化,从而使for循环错误,数组下标越界或删除错误值。

解决方案

可以从后往前删除元素。

19. 字典Dictionary的实现原理

定义:一种 键值对 形式存放数据。key的类型无限制。

实现原理核心:Hash算法和 解决Hash冲突的算法。

Hash算法

对实例对象和字符串来说,它们没有直接的数字作为Hash标准,因此它们需要通过内存地址计算一个Hash值,计算这个值的算法就是哈希函数。

除留余数法:取key被某个不大于散列表表长的数p除后余数作为散列地址。

Hash冲突:不同的key进行Hash计算,得到的结果一定是同一Hash地址。常用的解决办法是拉链法(链地址法)。

拉链法:将产生冲突的元素建立一个单链表,并将头指针地址存储至Hash表对应位置,这样定位到Hash位置后可通过遍历单链表来查找元素。

20. 进程,线程,协程全归纳

20.1 并发和并行

并发:在操作系统中,同一时间段,几个程序在同一个CPU上运行,但在任意时间点,只有一个程序在CPU上运行。

并行:当操作系统有多个CPU时,一个处理A线程,一个处理B线程,线程之间互不干扰,可以同时进行。

区别并发在宏观表现为有多个程序同时运行,微观上这些程序是分时交替执行的。

并行在多CPU系统中,将并发执行的程序分配到不同CPU上处理,每个CPU用来处理一个程序,这样的程序可以实现同时执行。

**20.2 **进程

概念:一个进程好比一个程序,它是 操作系统 资源分配的最小单位。同一时间执行的进程数不会超过核心数。但单核CPU也可以运行多进程。只是会极快地在进程间来回切换实现的多进程。进程是CPU资源分配的基本单位。

20.3 线程

概念:线程相比进程即为应用程序的不同任务。线程依赖于进程。它是 程序执行过程中的最小单元,不过线程的堆共享堆不共享栈,这也会导致锁的问题。线程是独立运行和调度的基本单位

20.4 线程与进程的区别

  • 1.进程是CPU分配资源的基本单位,线程是独立运行和调度的基本单位。
  • 2.进程有自己的资源空间,一个进程包含多个线程,线程与CPU资源分配无关,多个线程共享一个进程的资源。
  • 3.线程调度与切换比进程快很多。

20.5 阻塞和非阻塞

阻塞指调用线程或进程被操作系统挂起。

非阻塞指调用线程或进程不会被操作系统挂起。

20.6 同步和异步

同步:阻塞模式。指一个进程在执行某个请求时,如果该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行。

异步:非阻塞模式。无需一直等待下去,而是继续执行接下来的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理。

20.7 协程

概念:协程即为 一种用户态的轻量级线程。

特点:协程有自己的寄存器上下文和栈。协程可以保留上一次调用时的状态,每次重入时,相当于进入上一次调用的状态,即进入上一次离开时所处的逻辑流位置。

优点:高并发 + 高扩展 + 低成本。

  • 1.无需线程上下文切换的开销。
  • 2.无需原子操作锁定及同步的开销。
  • 3.方便切换控制流,简化编程模型。

缺点

  • 1.无法利用多核资源。
  • 2.进行阻塞时会阻塞整个程序。

篇章二:数据结构基础

1.力扣105 重建二叉树

题目说明:

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

输入输出样例:

Input: preorder = [3,9,20,15,7],inorder=[9,3,15,20,7]
Output: [3,9,20,null,null,15,7]			

代码:

class Solution 
    //构建哈希表方便后续快速根据val得到在中序遍历的位置
 	private Map<Integer,Integer> map = new HashMap<>();  
    public TreeNode buildTree(int[] preorder,int[] inorder)
        //判空
        if (preorder.length == 0) return null;
        int n = preorder.length;
        //初始化哈希表
        for (int i = 0;i < n;++i)
            map.put(inorder[i],i);
        //直接调用核心逻辑
        return CoreBuildTree(preorder,inorder,0,n-1,0,n-1);
    
    
    //核心算法逻辑
     /*
    * Param1:前序遍历数组
    * Param2:中序遍历数组
    * Param3:前序遍历左边界值
    * Param4:前序遍历右边界值
    * Param5:中序遍历左边界值
    * Param1:中序遍历右边界值
    * Return:返回当前递归的树根节点
    */
    private TreeNode CoreBuildTree(int[] preorder,int[] inorder,int preorder_left,int preorder_right,int inorder_left,int inorder_right)
        //递归函数优先考虑返回条件
        if (preorder_left > preorder_right) return null;
        //通过前序遍历拿到当前根节点值
        int rootVal = preorder[preorder_left];
        //根据当前根节点值找到其在中序遍历表中的下标
        int rootIndex_inorder = map.get(rootVal);
        //创建出当前根节点
        TreeNode root = new TreeNode(rootVal);
        //通过中序遍历表查出当前根节点有多少左子节点
        int size_left_subTree = rootIndex_inorder - inorder_left;
        
        //递归部分(左右两边)
        root.left = CoreBuildTree(preorder,inorder,preorder_left + 1,preorder_left + size_left_subTree,inorder_left,rootIndex_inorder - 1);
        root.right = CoreBuildTree(preorder,inorder,preorder_left + size_left_subTree+1,preorder_right,rootIndex_inorder + 1,inorder_right);
        return root;
    

2. 力扣79 矩阵中的路径

题目描述:

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false

案例:

输入:board = 
[["A","B","C","E"],
["S","F","C","S"],
["A","D","E","E"]],
word = "ABCCED"
输出:true

代码:

class Solution 
    public boolean exist(char[][] board, String word) 
    	//数组的长度
        int m = board.length,n = board[0].length;
        //存储某位置是否被访问过
        boolean[][] visited = new boolean[m][n];
        //遍历二维数组
        for (int i = 0;i < m;++i)
            for (int j = 0;j < n;++j)
                boolean ans = aroundCheck(board,visited,i,j,word,0);
                if (ans) return ans;
            
        
        return false;
    
 	//核心递归算法
    /*
        Param1:二维字符网格
        Param2:标记数组
        Param3,4:当前所在的位置
        Param5:当前比对的字符串从k开始的子串
        Param6:字符串中的第几位
    */
    private boolean aroundCheck(char[][] board,boolean[][] visited,int i,int j,String target,int k)
        //作为递归函数优先考虑返回条件
        if (board[i][j] != target.charAt(k)) return false;
        if (k == target.length - 1) return true;
        
        //将该位置设为已访问
        visited[i][j] = true;
        //准备向四个方向遍历
        int[][] directions = 0,1,0,-1,1,0,-1,0;
        boolean res = false;
        
        //分别查找各个方向是否符合条件
        for (int[] dir : directions)
            int curi = i + dir[0],curj = j + dir[1];
            if (curi >= 0 && curi < board.length && curj >= 0 && curj < board[0].length)
                boolean flag = aroundCheck(board,visited,curi,curj,target,k+1)
                if (flag)
                    //找完了
                  	res = true;
                    break;
              	    
        
        //剪枝回溯
        visited[i][j] = false;
        return res;
    


篇章三:游戏算法基础

1.A*寻路算法 (重点)

1.1. A*寻路的基本原理

① 寻路消耗公式:f = g + h

f:寻路消耗

g:距离起点的距离,详细解释看以下具体寻路步骤

h:距离终点的曼哈顿距离,即当前点到终点的x差值 + y差值。之所以采用 曼哈顿距离,是因为该距离相比直接计算直线距离少了开方步骤,性能更高。

② 开启列表:详情如下。

③ 关闭列表:详情如下。

④ 格子对象的父对象。

1.2. A* 寻路具体步骤

  1. 将 起点数据(格子信息,格子的父对象) 加入 关闭列表
  2. 将当前结点作为 新的起点,依次遍历该格子周围的八个格子,如果遍历到的格子为 不可移动(障碍物)或者已经被加入到开启列表或关闭列表中时,则直接跳过该格子的相关操作。反之,将遍历到的格子信息存入 开启列表中,通过 寻路消耗计算公式 计算其 寻路消耗,找到 寻路消耗 最小的格子并将其加入 关闭列表 中。
  3. 将2找到的格子作为新的起点,重复2的操作。当满足 当前格子 等于 终点位置 时,代表寻路成功。
  4. 寻路成功后,顺着关闭列表中存储的父对象回溯到起点,回溯点位即为寻路后找到的最优路径。如果开启列表 为空时,则寻路失败,遇到死路。

1.3. A*的算法实现

1.3.1 寻路结点类

//格子类型枚举
public enum E_Node_Type

    Walkable,Stoppable


//A*寻路结点类
public class AStarNode 

    //当前格子坐标
    public int x;
    public int y;

    //寻路消耗f
    public float f;
    //距离起点的常规距离
    publi

以上是关于U3D面试汇总!!!的主要内容,如果未能解决你的问题,请参考以下文章

U3D面试五

U3D面试题六

unity3d面试题大全

Unity面试题汇总

Unity 面试经验汇总

2022-02-09 U3D全栈班 001-Unity基本认识