C#相关

1.值类型和引用类型

值类型:bool,float,int,double,struct,enum

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

区别

  • 值类型存储再栈上,引用类型存储在堆上。相应的栈上的值类型存储快,堆上的引用类型存储满

  • 值类型存储的为真实的值也就是实际数据,引用类型指向的是内存堆中的指针和引用

  • 值类型在使用后自动释放,引用类型使用GC来释放

  • 值类型的继承是System.ValueType ,System.ValueType继承于System.Object,引用类型继承System.Object

  • 值类型在栈中存储的是直接的值,引用类型数据本身实在堆中,栈中存放的是一个引用的地址

底层

  • 引用类型在实例化时,先在栈内开辟空间,用于存储堆中对象的地址,然后在堆内开辟空间,存储引用对象。
  • 而值类型直接在栈中开辟空间存储对象。值类型也有引用地址,但都在栈内的同一空间。
  • 在参数对象进入方法体内,实则是在栈中开辟了新的临时空间。(也就是参数对象的副本)栈内值类型的修改,由于栈中地址不同,所以值类型不会影响到主体。而引用类型的存储数据是一个堆内的地址,所以对于引用类型的修改是直接修改堆内的对象。
  • 值类型对象中的引用类型在堆中(struct中定义的string等)
  • 引用类型对象中的值类型也在堆中(class中的int等)

2.String引用类型的特殊性

string的修改,实则是new 一个新的string,在堆内新开辟空间。而此时栈内的副本也会指向堆内新对象。因此string改变。是新建的对象,和本体没有联系。

2.2 解决
当频繁堆一个字符串进行修改时,利用StringBuilder代替String

2.3 StringBuilder的底层实现?
StringBuilder 是支持扩容的(char类型)数组,在每次空间不足时,会开辟原先数组大小的容量,类似于链表,新建的数组指向上一个已经用完的数组,本身不会产生gc。

2.4 扩展:
StringBuffer是线程安全,一般用于多线程(C#端不存在)StringBuilder是非线程安全,所以性能略好,一般用于单线程

2.5 用StringBuilder拼接字符串就一定比string要好吗?
答:极少拼接(或者短字符串)的情况下 String甚至优于StringBuilder,因为String是公用API,通用性好,用途广泛,读取性能高,占用内存较小,Stringbuilder初始化花费时间更大。

3.ref和out

ref 和 out 都用于参数列表中,用来按引用传递参数。

**ref:**使用 ref 修饰的参数,在传入函数之前必须先初始化;在函数内部可以修改,也可以不修改该参数,传出后变量保留修改后的值。

**out:**使用 out 修饰的参数,在传入函数之前可以不初始化;但在函数内部必须对该参数进行赋值,否则会报错,传出后用于把值带回调用处。

4.C#GC原理

C#采用自动内存管理机制,分为栈内存(非托管内存)和托管堆内存两部分。

**栈内存(非托管内存)**存储值类型、方法参数和局部变量引用,其生命周期与作用域严格绑定——当方法调用结束时,对应的栈帧自动弹出,内存瞬时释放,这个过程是确定且即

时的。

托管堆内存存储所有引用类型对象,由垃圾回收器(GC)全权管理,采用分代回收策略将堆划分为三代:

新创建的小对象(≤85KB)首先进入第0代;当第0代堆空间耗尽时,GC触发一次第0代回收,从根对象出发标记所有可达对象,然后回收未标记的不可达对象,接

着将存活对象压缩并晋升至第1代;若第1代空间也满,则触发第1代回收,同时处理第0代和第1代;第2代存放长期存活对象,其回收(Full GC)通常只在系统内

存严重不足或显式调用时发生,开销较大。超过85KB的大对象直接进入大对象堆(属于第2代但单独管理),避免复制开销。整个GC过程完全自动,开发者无需

手动释放,但需注意非托管资源(文件流、数据库连接等)必须通过IDisposable接口和using语句显式清理。

5.重载根重写的区别

两者都是多态的具体实现

定义方式不同:重载方法名相同参数列表可以不同,重写方法名和参数列表都相同。

调用时机不同:重载是相同对象以不同参数调用,重写用不同对象以相同参数调用

重载是编译型多态,重写是运行时多态

6.数组和链表的区别

数组

  • 内存通常是连续的。
  • 需要能通过下标直接定位元素,所以随机访问快,访问第 i 个元素是 O(1)
  • 插入、删除中间或开头元素时,后面的元素通常都要挪动,所以是 O(N)
  • 删除末尾元素一般是 O(1),但前提是末尾删除且不涉及缩容等额外操作。
  • 很多语言里的“动态数组”能自动扩容,但底层仍然是数组,扩容时通常要重新申请更大连续空间并拷贝元素,因此单次扩容可能是 O(N),不过均摊插入末尾常看作 O(1)

链表

  • 内存不要求连续
  • 每个节点除了存数据,还要存指针(或引用),所以额外内存开销更大
  • 不能像数组那样直接按下标访问,第 i 个元素要从头走过去,所以查找是 O(N)
  • 已知某个节点位置时,插入和删除可以做到 O(1)
  • 但如果要先“找到这个位置”,查找过程仍然要 O(N),所以很多时候实际总复杂度不是纯 O(1)

7.哈希表

哈希表是一种基于 key-value 存储的数据结构,底层通常用数组实现。

它通过哈希函数把 key 映射成数组下标,从而在平均情况下把查找、插入、删除的时间复杂度做到 O(1)。

不过由于不同 key 可能映射到同一位置,所以会产生哈希冲突,常见解决方式有拉链法和开放地址法。

当负载因子过高时,哈希表还需要扩容并进行 rehash。哈希表的优点是查询效率高,缺点是可能冲突、通常无序,而且最坏情况下会退化到 O(N)。

rehash:rehash 就是在哈希表扩容或容量变化后,因为元素下标依赖于 hash(key) % capacity,所以原有元素的位置会发生变化,需要把所有元素重新计算下标并插入到新的哈希表中。这个过程时间复杂度通常是 O(N)。

8.静态

C# 中的 static 表示静态,意思是成员属于类本身,而不是某个具体对象。

静态成员可以直接通过类名访问,并且在内存中只有一份,所有对象共享。

静态方法不能直接访问非静态成员,因为非静态成员属于对象,每个类的非静态成员都不同,而静态方法则是共享的,而非静态方法可以使用静态变量。

static 常用于共享数据、工具方法和静态工具类

9.const和readonly

两者都是常量,主要区别如下:

1.const是编译时常量,值必须在声明确定;而readonly则是运行时常量,可以在声明时赋值或者构造函数时赋值。

2.const默认是静态的,readonly不是static,除非手动static readonly

3.在修饰引用类型时,const一般只修饰string和null,而readonly则都可以。

10.反射和序列化

反射:反射是 C# 在运行时获取类型信息并动态操作对象的一种机制。通过反射可以获取类的属性、方法、字段、构造函数等,也可以动态创建对象和调用方法。它常用于框架开发、依赖注入、插件机制、ORM 和序列化等场景。优点是灵活,缺点是性能较低。

序列化:序列化是将对象转换为可存储或可传输格式的过程,比如 JSON、XML 等;反序列化则是把这些数据恢复成对象。序列化常用于文件存储、网络传输、接口通信和缓存。C# 中最常见的是 JSON 序列化,比如使用 System.Text.Json

11.匿名方法

一般是只使用一次的方法,不需要专门写一个方法,所以不需要写方法名。常与委托配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
delegate(参数列表){
方法体;
}
using System;
class Program{
static void Main(){
Action show = delegate(){
Console.WriteLine("Hello");
};
show();
}
}
//在这里的delegate就是匿名方法,没有方法名,赋值给了show
//匿名方法:没有名字的方法实现,通常用来赋值给委托。

12.委托和事件

委托是 C# 中一种类型安全的函数引用,类似于函数指针,但比函数指针更安全。委托可以引用签名匹配的方法,调用委托时本质上就是调用它所绑定的方法。委托还支持多播,也就是一个委托可以同时绑定多个方法,并在调用时按绑定顺序依次执行,常用 += 添加,-= 移除。

事件则是基于委托的一种封装,可以看作受限制的委托。事件的主要作用是实现发布订阅模式,它只允许类的内部触发,类的外部只能订阅或取消订阅,不能直接赋值、调用或清空事件,因此比直接使用委托更安全

13.闭包

闭包是指内部函数引用了外部函数中的变量,从而使这些变量在外部函数执行结束后仍然能够继续存在的一种现象。闭包的本质就是函数和其捕获的外部变量形成了一个整体。它常见于匿名方法和 Lambda 表达式中,主要作用是保存状态和延长变量生命周期。

14.接口是否可以继承接口。抽象类是否实现接口,抽象类是否可以继承实现类?

接口可以继承接口,而且可以多继承;抽象类可以实现接口,既可以全部实现,也可以只实现一部分;抽象类还可以继承普通类,因为抽象类本质上也是类。

15.Equals和 == 的区别

== 是运算符,Equals 是方法。对于值类型,它们通常都比较值是否相等;对于引用类型,默认情况下 == 比较的是引用地址是否相同,而 Equals 也默认比较引用,但 Equals 可以被重写,用来定义对象内容上的逻辑相等。像 string 这种引用类型重载了 ==,所以比较的是内容。一般来说,== 更偏运算符层面的比较,Equals 更偏逻辑相等比较。

16.浮点数精度问题

浮点数在计算机中通常采用近似存储,很多十进制小数无法被精确表示,因此会产生精度误差。

常见问题包括:

第一,不能直接用 == 判断两个浮点数是否相等,通常要用误差范围比较;

第二,连续运算可能导致误差累积,使理论上相同的计算结果不一致;

第三,不同平台或设备上的浮点计算结果可能存在细微差别。

解决方法:

  • 因此在使用浮点数进行数值比较时,通常使用Math.Abs(X-Y) < 0.00001来说明2个数是相等的。
  • 使用int或者long类型代替浮点数

17.协程的底层实现

在C#中,协程主要是依靠IEnumerator来实现的,他底层是一个返回一个迭代器。程序从StartCouroutine开始执行,当程序遇到yield return的时候,会记录下当前的函数执行位置以及变量的状态,协程开始等待,在等待具体的时间或者帧后开始继续执行协程,如果在次遇到yield 继续等待,完成上述的操作,直至协程函数执行完成

网络相关

1.TCP的3次握手和4次挥手

TCP 三次握手

  1. 第一次握手:客户端向服务器发送 SYN 报文,请求建立连接,并进入 SYN-SENT 状态。
  2. 第二次握手:服务器收到后返回 SYN+ACK 报文,表示同意建立连接,并进入 SYN-RCVD 状态。
  3. 第三次握手:客户端再向服务器发送 ACK 报文,表示确认,双方进入 ESTABLISHED 状态,连接建立。

TCP 四次挥手

  1. 第一次挥手:客户端向服务器发送 FIN 报文,表示客户端已经没有数据要发送了。
  2. 第二次挥手:服务器收到后返回 ACK 报文,表示确认客户端的关闭请求。
  3. 第三次挥手:服务器处理完剩余数据后,向客户端发送 FIN 报文,表示服务器也没有数据要发送了。
  4. 第四次挥手:客户端收到后返回 ACK 报文,确认关闭连接,随后连接正式结束。

image-20260321162105770

2.为什么TCP连接是3次握手而不是2次握手

1.TCP 采用三次握手,是为了防止已经失效的旧连接请求报文突然传到服务器,使服务器误以为客户端要建立连接,从而白白分配资源。如果采用两次握手,客户端之前滞留在网络中的旧 SYN 报文到达服务器后,服务器会误认为客户端重新请求建立连接,并立即分配资源;而三次握手中,服务器只有在收到客户端最后的确认 ACK 后,才认为连接真正建立,因此可以避免旧连接请求造成的资源浪费。

2.三次握手才能让双方确认自己和对方的发送和接受能力都正常

3.为什么TCP连接是3次握手而不是4次握手

因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。

  • 第一次握手:服务端确认“自己收、客户端发”报文功能正常。
  • 第二次握手:客户端确认“自己发、自己收、服务端收、客户端发”报文功能正常,客户端认为连接已建立。
  • 第三次握手:服务端确认“自己发、客户端收”报文功能正常,此时双方均建立连接,可以正常通信。

4.三次握手连接阶段,最后一次ACK包丢失,会发生什么

服务端:收不到最后确认时会超时重传,多次失败后关闭连接。

客户端:先认为连接已建立,之后发送数据若收到 RST,就知道连接已失效。

5.为什么连接的时候是3次,断开为什么需要4次挥手

TCP 建立连接时,服务器收到客户端的 SYN 后,可以将确认和建立连接请求一起发送,所以三次握手即可;

断开连接时,服务器收到客户端的 FIN 后,只能先确认,但若还有数据未发送完,不能立即关闭,因此需等数据发送完后再发送 FIN,故需要四次挥手。

6.Time_Wait是什么,以及为什么需要等待?

TIME_WAIT 是 TCP 连接断开时的一种状态,表示:主动关闭连接的一方,在发送最后一个 ACK 后,还要再等待一段时间,确保连接真正结束。

等待的原因:(通常等待的时间是2MSL(保证“最后 ACK 能到对方”以及“旧报文在网络中消失”))

  • 保证对方收到最后的 ACK:
    如果最后这个 ACK 丢了,对方还会重发 FIN,处于 TIME_WAIT 的这一方还能重新发送 ACK。也就是说客户端最后发送的ACK包可能会丢失,服务端收不到这个ACK包,就会重新给客户端发送Fin包,如果这个时候客户端已经关闭就无法收到这个Fin包,导致无法正常断开连接,因此需要等待防止ACK包丢失,服务器重新发送的FIn包无法接收

  • 防止旧连接中的报文干扰新连接
    等待一段时间后,网络中原来这条连接残留的旧报文基本都会消失,避免影响后面新的同一连接。

7.如果已经建立了连接,但是客户端出现故障了怎么办?

TCP 通过保活定时器和超时重传机制来判断客户端是否已经故障;如果在一定时间内收不到客户端的数据,服务器就发送探测报文,若多次探测仍无响应,就认为连接已经断开并关闭连接。

8.Time_Wait状态过多会产生什么后果?怎么处理?

**对于服务器:**如果短时间内有大量连接关闭,会产生大量 TIME_WAIT 连接,占用系统资源和连接表资源,严重时会导致新连接无法及时建立。

**对于客户端:**如果客户端出现大量 TIME_WAIT,会占用本地临时端口资源;而端口数量有限,端口被大量占用后,就可能无法再建立新的连接。

处理方法

  • 允许端口复用,提高端口利用率。
  • 减少短连接,尽量使用长连接或连接复用。
  • 调整系统参数,缩短 TIME_WAIT 持续时间。
  • 对客户端扩大可用端口范围。

9.Time_Wait是服务器状态还是客户端状态

Time_Wait一般是主动断开连接的一方的状态,一般情况是客户端主动断开连接。

10.TCP如何保证可靠性

**1. 校验和:**TCP 在发送数据时会根据首部和数据内容计算校验和,并将其放入报文段中;接收方收到报文后也会重新计算一次校验和,如果结果不一致,就说明数据在传输过程中发生了差错,这个报文就会被丢弃,从而保证接收到的数据是正确的。

**2. 序列号与确认应答:**TCP 会对发送的每一个字节进行编号,即序列号,接收方收到数据后会返回确认应答,告诉发送方下一个期望接收的字节序号是多少;发送方通过确认应答就能知道哪些数据已经成功到达,哪些数据还需要继续发送,从而保证数据能够按序、完整地传输。

**3. 超时重传:**发送方发送数据后会启动定时器,如果在规定时间内没有收到接收方的确认应答,就认为该数据可能在传输过程中丢失了,于是重新发送该报文段;这样即使网络中出现丢包,也能通过重新发送把丢失的数据补回来。

**4. 滑动窗口:**为了提高传输效率,TCP 不会每发送一个报文就停下来等待确认,而是允许发送方在窗口范围内连续发送多个报文段;当接收方的确认到达后,发送窗口再向前移动,继续发送后续数据,这样既提高了传输速度,又保证了数据可靠到达。

**5. 流量控制:**TCP 通过接收方通告窗口大小来进行流量控制,接收方会根据自己当前缓冲区的剩余空间告诉发送方还能接收多少数据,发送方就按照这个窗口大小来控制发送速度;这样可以防止发送方发得过快,导致接收方来不及处理而丢失数据。

**6. 拥塞控制:**TCP 在发送数据时还会根据网络的拥塞情况动态调整发送速度,例如在网络较通畅时逐步增大发送量,而在网络出现拥塞时及时减小发送量;这样可以避免大量数据一下子涌入网络造成严重堵塞,从而提高整个网络传输的可靠性。

11.拥塞控制

TCP使用四种方法来实现拥塞控制:慢开始,拥塞窗口,快重传,快恢复。

**慢开始:**不要一开始就发送大量的数据,由小到大逐渐增加拥塞窗口的大小,指数增加。

**拥塞避免:**拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口按线性规律缓慢增长。

快重传:我们可以剔除一些不必要的拥塞报文,提高网络吞吐量。比如接收方在收到一个失序的报文段后就立即发出重复确认,而不要等到自己发送数据时捎带确

认。快重传规定:发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。

快恢复:主要是配合快重传。当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞),但接下来并不执行慢

开始算法,因为如果网络出现拥塞的话就不会收到好几个重复的确认,收到三个重复确认说明网络状况还可以。

GC相关

GC优化点

1.装箱和拆箱:装箱就是将值类型转换为引用类型,拆箱则正好相反。

2.泛型优化

3.字符串变量:在常需要对字符串进行增删操作时,使用StringBuilder,因为字符串在修改的过程会创建新的字符串,而StringBuilder则是在原有的字符串上进行修改。

4.struct,class。 数据组合很小的情况小,使用struct来,class容易产生内存垃圾

5.容器使用,一些常用的数据结果的如list,queue等底层都是数组,最后在一开始就指定容器的大小。

Unity的垃圾回收

贝母垃圾回收器:一种保守类型的回收器,采取的方法也是标记 - 清楚的方式。先从还可达的对象开始做“标记”,沿着引用关系把还能访问到的对象都标出来;然后把没有被标记到的对象视为垃圾,再进行清除和复用。

默认采用增量回收:增量回收是把原本一次性完成的垃圾回收过程拆分到多个时间片或多帧中执行,从而减少单次卡顿。

性能优化

内存优化

CPU优化

GPU优化

设计模式

23种设计模式,大体分为创建型模式,结构型模式,行为型模式。

创建型设计模式:用于解决“对象怎么创建更合理”的问题。单例模式,工厂方法模式,抽象工厂模式,建造者模式,原型模式

结构型设计模式:用于解决“类和对象怎么组织更合理”的问题。适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式

行为型设计模式:用于解决“对象之间如何交互、职责怎么分配”的问题。策略模式,观察者模式,责任链模式,命令模式,状态模式,模板方法模式,迭代器模

式,中介者模式,备忘录模式,解释器模式,访问者模式

常见的设计模式

单例模式:提供一个全局访问点,只提供一个全局的静态实例,使用的时候,只需要通过这个全局访问点来使用。

优点:因为只有一个静态实例,所以避免了重复创建对象造成的资源浪费。

缺点:因为存在一个全局的静态实例,可能导致很多模块都能够使用到单例模式,这样会导致各个模块之间的耦合度较高,不容易进行代码维护。(高耦合,扩展性差,违法单一职责原则)

观察者模式:观察者模式本质上是 一种事件通知机制,也可以理解为发布-订阅模式。一个对象状态发生变化时,会通知所有订阅了该事件的观察者对象,让它们自动执行各自对应的处理逻辑。

优点:降低对象之间的耦合度。比如在游戏里,角色死亡后,可以通过事件通知 UI、任务系统、音效系统、成就系统分别做出响应,而不需要角色代码直接依赖这些模块。

缺点:观察者过大时,通知链可能带来性能开销。事件触发过于发散(一个事件的变化造成多个观察者执行逻辑)当发生错误时,不好进行问题的排查。依赖关系设计不当,可能造成循环通知或逻辑混乱

工厂模式将对象的创建过程封装起来,让调用方无需关心对象的具体创建细节(比如类的实例化、初始化参数等),只需通过 “工厂” 获取所需对象。也就你有一个工厂,你想要什么产品,工厂就根据你的需要生成对应的产品,你不需要知道具体的生产流程

优点:降低耦合度,将产品的创建工程与使用逻辑进行分开。调用方只依赖工厂和抽象类型,便于统一管理对象创建。

缺点:产品类型过多,可能会造成的工厂的职责过重。增加了一层工厂类,系统结构更为复杂。当新增产品时,会违法设计原则的开闭原则。

**抽象工厂模式:**将对象的相应的一类产品的创建工程封装起来,你不需要知道这类产品的创建的具体细节。

优点:适合创建一整套相互关联或相互依赖的对象。保证同一产品族中的对象风格一致、互相兼容。调用方只依赖抽象接口,便于替换整个产品族。

缺点:系统更复杂;新增产品等级结构困难,扩展麻烦(违法相应的设计原则)。

SOLID原则

单一职责原则:一个类只负责一项职责,只有一个引起它变化的原因。比如说:UserServer不要让他干很多工作,减少代码的耦合

优点:代码的逻辑能更清楚,每个类专职一个功能。代码的复用性更好。

缺点:类的数量会变的更多

**开闭原则:**对原有代码的修改关闭,对扩展新功能开启。对于一个类如果需要增加功能,不要修改原有的代码,而是增加新的代码来实现功能。

优点:不会破坏原有的类结构,减少bug。扩展性更强,新增功能不需要修改源代码。

缺点:需要设计依赖(设计接口,抽象类)

**里氏替换原则:**子类必须能够替换父类,并且不出现程序错误。所有引用父类的地方,必须能够透明地使用其子类,而不会导致程序出错。如果子类继承了父类,但行为和父类不一致,替换后程序逻辑就会出问题,这就违反了里氏替换原则。

优点:保证继承关系正确,必须行为一致才能继承。父类写好的逻辑,子类可以安全替换使用

缺点:

**依赖倒置原则:**高层模块不应该依赖低层模块,二者都应该依赖抽象。不要直接依赖具体实现,要依赖接口。(多用接口,减少具体实现的类)

优点:降低代码的耦合度,高层业务逻辑不依赖底层的实现。易于扩展,更换类实现影响更小

缺点:

**接口隔离原则:**接口尽力做的小而专,不需要大又胖。不要把很多不相关的方法都塞到一个接口里,让实现类被迫实现自己根本用不到的方法。

优点:降低耦合度,不需要依赖无关的方法。如果类的功能发生变化,影响范围更小。

缺点:接口数量会变多。设计难度会增加(更细的划分)

**合成复用原则:(组合优于继承)**尽量使用组合/聚合,而不是继承。也就是说:
复用代码时,优先考虑“把对象作为成员放进去”,而不是“直接继承它”。因为继承是强耦合,子类和父类绑定很紧,父类修改后,子类很容易受影响。而组合则是需要什么能力就组合对象,可以动态的替换内部组件。

优点:灵活性更高,需要什么能力就组合什么对象。耦合度更低,不直接依赖父类内部实现。

缺点:功能分散在多个组合对象中

Unity相关

1.Canvas的三种渲染模式:Screen Space - Overlay,Screen Space - Camera,WorldSpace。

ScreenSpace - Overlay:直接贴在屏幕的最上面,不会因为相机的移动或者变换而变化,常用来制作游戏中背包和血条面板。

ScreenSpace - Camera:UI同样贴在平面上,但是会受到相机来渲染。也就是会受到相机距离来影响,但是需要指定一个 Camera,UI 会根据摄像机来显示,可以控制 UI 与摄像机的距离、排序、层级

WorldSpace:UI 变成 3D 世界里的 “平面物体”,随相机视角变化,用于场景内的 UI。这时候 Canvas 不再是“贴屏幕”,而是像一块广告牌、显示器、按钮面板一样,真的放进 3D 世界中。Canvas 有真实的位置、旋转、缩放,会受场景透视影响,可以被别的 3D 物体遮挡,玩家走近看会变大,走远看会变小

2.CanvasScaler:

它的核心作用就是让你的 UI 在不同分辨率 / 屏幕尺寸下,能 “适配” 显示,不会出现手机上按钮挤成一团、电脑上 UI 小得看不清的情况。

它也有三种UI缩放模式:

Constant Pixel Size(恒定像素大小):不会根据屏幕分辨率的变化而变化,如:你在纸上画了一个 50x50 像素的按钮,把这张纸贴到手机屏幕(1080P)上,按钮大小刚好;但贴到电脑屏幕(4K)上,按钮就会显得特别小(因为 4K 屏幕像素更密)

Scale With Screen Size(随屏幕尺寸缩放):先定一个 “参考分辨率”(比如 1080x1920,手机主流分辨率),然后让 Canvas 根据当前屏幕和参考分辨率的比例,自动缩放整个 UI—— 保证 UI 在不同屏幕上的 “视觉大小” 基本一致。(等比例进行缩放)

**Constant Physical Size(恒定物理大小)**UI 元素的大小按 “物理尺寸”(厘米 / 英寸)定义,不管屏幕分辨率 / 尺寸如何,UI 的实际物理大小不变 —— 比如你定义按钮是 1 厘米宽,那在手机、平板、电脑屏幕上,按钮的物理宽度都是 1 厘米(也就是按照物理大小恒定

Constant Pixel Size:像素固定,分辨率变 UI 大小视觉上变;

Scale With Screen Size:参考分辨率缩放,跨屏幕视觉大小不变;

Constant Physical Size:物理尺寸固定,不管屏幕多大 UI 实际尺寸不变。

3.Graphic RayCaster(图形射线检测):

作用是让 Unity 能检测 “射线” 是否击中 Canvas 里的 UI 元素(按钮、文本、图片等)。理解成:给 UI 加了 “可被点击 / 触摸识别” 的能力。

Unity 的 UI 交互(点击、拖拽)本质是 “射线检测”—— 当你点击屏幕时,Unity 会从点击位置发射一条 “看不见的射线”,Graphic Raycaster 就是负责让这条射线能 “碰到” UI,并告诉程序 “用户点到了哪个 UI”。

该组件工作的前提:

是Graphic Raycaster 必须挂载在有 UI 的 Canvas 上(和 Canvas 的渲染模式无关);

被点击的 UI 元素(比如按钮)必须有 Graphic 组件(Image、Text、RawImage 等,这是 “可被射线检测” 的基础);

UI 元素的 Raycast Target(射线检测目标) 属性必须勾选(默认勾选,关掉就点不到了);

必须有 EventSystem 组件(场景里默认会带,负责管理所有 UI 交互事件)。

参数 通俗解释
Ignore Reversed Graphics 忽略 “反向” 的 UI(比如 3D 世界里的 UI,相机从背面看时,点不到)
Blocking Objects 哪些物体能 “挡住” UI 射线(比如 3D 物体挡住 WorldSpace 的 UI,就点不到 UI 了)
Blocking Mask 只让指定图层的物体挡住 UI 射线(精准控制遮挡规则)

不同 Canvas 模式下的表现

(1)ScreenSpace - Overlay/Camera 模式:射线是从屏幕点击位置垂直发射,只检测 Canvas 里的 UI,和 3D 世界无关(除非你设置了 Blocking Objects)。

→ 比如你点屏幕上的按钮,不管 3D 场景里有什么,只要按钮没被其他 UI 挡住,就能点到。

(2)WorldSpace 模式:射线是从相机发射到点击位置的 3D 射线,UI 是 3D 世界里的 “平面”,所以:

  • 如果有 3D 物体挡在相机和 UI 之间,射线会先击中 3D 物体,UI 就点不到了;

  • 只有射线先击中 UI,才能触发交互。

    → 比如游戏里的 3D 广告牌 UI,被墙挡住后,你点屏幕对应位置,只会点到墙,点不到广告牌。