当前位置:   article > 正文

【Unity&C#】简单整理了一份面试题_unity c# 高级面试题

unity c# 高级面试题

不是很全,记录学习一下~希望对您有所帮助!

一、 C#

1. 类的构造函数为什么不能加static修饰?

先解释一下静态构造方法

  1. 静态构造函数既没有访问修饰符,也没有参数,只有static 类名()
  2. 在创建第一个类实例或任何静态成员被引用时,.Net将自动调用静态构造函数来初始化类
  3. 一个类只能有一个静态构造函数
  4. 无参构造函数可以与静态构造函数共存
  5. 最多运行一次
  6. 静态构造函数不可以被继承
  7. 如果没有写静态构造函数,而类中包含带有初始值的静态成员,编译器会自动生成默认的静态构造函数
  8. 如果静态构造函数引发异常,CLR会捕获该异常,并且不会再次调用静态构造函数。该类型将保持未初始化状态,任何对该类型成员的进一步访问(如调用其他静态方法)都会导致TypeInitializationException异常

在类的构造函数前加上static会报错

  1. 静态构造函数执行优先于任何成员级别的构造函数
  2. 构造函数格式public + 类名,如果加上static会报错(因为静态函数不能访问成员变量,静态构造函数只执行一次),因为静态的优先级高,这个时候还没有成员变量的概念,如果能访问肯定就报错了
  3. 显然静态构造函数不能使用this/base来调用

2. 重载、重写的区别

  1. 重载是在当前类,重写一般是在子类中
  2. 重载是方法名相同、参数类型不同,重写是方法名相同参数列表也必须相同
  3. 在使用时,重载方法被相同对象不同的参数类型调用,重写方法被不同对象以相同参数调用
  4. 重载方法在编译时多态,重写方法在运行时多态

3. Mono与.Net的关系

.Net是一个语言平台,Mono为.Net提供了集成开发环境,集成实现了.Net的编译器、CLR和基础类库,使得.Net既可以运行在Windows,也可以运行在Linux、Unix、Mac OS等

4. 引用类型、值类型的基类是什么

引用类型基类是System.Object
值类型基类是System.ValueType(同时也隐式继承System.Object)

5. 隐藏方法和重写方法的区别

隐藏方法
通过在派生类中使用new关键字来显示隐藏基类中的方法,隐藏方法不会覆盖基类的方法,而是隐藏他们,如果通过基类引用调用该方法,仍然会调用基类的方法

using System;

public class BaseClass
{
    public void Show()
    {
        Console.WriteLine("BaseClass Show method");
    }
}

public class DerivedClass : BaseClass
{
    public new void Show()
    {
        Console.WriteLine("DerivedClass Show method");
    }
}

public class Program
{
    public static void Main()
    {
        BaseClass baseClass = new BaseClass();
        baseClass.Show(); // 调用 BaseClass 的 Show 方法

        DerivedClass derivedClass = new DerivedClass();
        derivedClass.Show(); // 调用 DerivedClass 的 Show 方法

        BaseClass baseDerived = new DerivedClass();
        baseDerived.Show(); // 调用 BaseClass 的 Show 方法
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

重写方法
通过在派生类中使用override关键字来覆盖基类中的虚方法或抽象方法,重写方法会替代基类的方法实现,如果通过基类引用来调用该方法,实际调用的是派生类的方法

using System;

public class BaseClass
{
    public virtual void Show()
    {
        Console.WriteLine("BaseClass Show method");
    }
}

public class DerivedClass : BaseClass
{
    public override void Show()
    {
        Console.WriteLine("DerivedClass Show method");
    }
}

public class Program
{
    public static void Main()
    {
        BaseClass baseClass = new BaseClass();
        baseClass.Show(); // 调用 BaseClass 的 Show 方法

        DerivedClass derivedClass = new DerivedClass();
        derivedClass.Show(); // 调用 DerivedClass 的 Show 方法

        BaseClass baseDerived = new DerivedClass();
        baseDerived.Show(); // 调用 DerivedClass 的 Show 方法
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

主要区别

  1. 隐藏方法用new关键字,重写方法用override关键字
  2. 隐藏方法被基类引用调用时,调用的是基类的方法,被派生类引用调用时,调用的是派生类方法
  3. 重写方法,只有基类引用装基类对象时调用的才是基类方法,如果是子类引用装子类对象,或者父类引用装子类对象,那么调用的都是子类重写后的该方法
  4. 隐藏方法不支持运行时多态性,重写方法支持运行时多态性

6. 虚函数实现原理

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

7. ref和out关键字的区别

ref修饰的参数在使用前必须赋值,在方法内部可改可不改,ref又进又出
out修饰的参数在使用前不需要赋值,在方法内部必须赋值,out不进只出

引用参数和输出参数都不会创建新的存储位置,只是方法接受了 这个变量的地址

如果ref参数是值类型,原先的值类型数据,会随着方法里的数据改变而改变
如果ref参数是引用类型,方法里重新赋值后,原来对象堆中的数据会改变
如果引用类型再次创建新对象并赋值给ref参数,引用地址会重新指向新对象对数据
方法结束后形参和新对象都会消失,实参还是指向原始对象,只不过数据改变了

8. 委托、事件的区别

委托是一个类,该类内部维护者一个字段,指向一个方法。

  1. 委托可以用=来赋值,事件不可以
  2. 委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用
  3. 委托是一个类型,事件修饰的是一个对象
  4. 委托就是一个类,也可以实例化,通过委托的构造函数来把方法赋值给委托实例
  5. 触发委托有两种方式:①委托实例?.Invoke(参数列表)②委托实例(参数列表)
    事件可以看做一个委托类型的变量,通过事件注册或取消多个委托或方法
  6. 通过+=为事件注册多个委托实例或多个方法
  7. 通过-=为事件注销多个委托实例或方法

9. 访问权限修饰符

  1. public:任何地方都可以访问
  2. private:只能在同一个类中访问
  3. protected:只能在同一个类或派生类中访问
  4. internal:只能在同一个程序集内访问
  5. protected internal:只能在同一个程序集内或派生类中访问
  6. private protected:只能在同一个类或同一个程序集内的派生类中访问
public class BaseClass
{
    public int publicField = 1;
    private int privateField = 2;
    protected int protectedField = 3;
    internal int internalField = 4;
    protected internal int protectedInternalField = 5;
    private protected int privateProtectedField = 6;
}

public class DerivedClass : BaseClass
{
    public void AccessFields()
    {
        Console.WriteLine(publicField);              // 可访问
        // Console.WriteLine(privateField);          // 不可访问
        Console.WriteLine(protectedField);           // 可访问
        Console.WriteLine(internalField);            // 可访问
        Console.WriteLine(protectedInternalField);   // 可访问
        Console.WriteLine(privateProtectedField);    // 可访问
    }
}

public class OtherClass
{
    public void AccessFields()
    {
        BaseClass baseClass = new BaseClass();
        Console.WriteLine(baseClass.publicField);    // 可访问
        // Console.WriteLine(baseClass.privateField); // 不可访问
        // Console.WriteLine(baseClass.protectedField); // 不可访问
        Console.WriteLine(baseClass.internalField);  // 可访问
        Console.WriteLine(baseClass.protectedInternalField); // 可访问
        // Console.WriteLine(baseClass.privateProtectedField); // 不可访问
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

10. 为什么在使用foreach遍历集合时,不能修改集合的结构

所有实现了IEnumerable接口的集合都可以使用foreach进行遍历。
foreach使用了集合的迭代器,如果在迭代过程中修改原集合的结构(如添加或删除),会引发InvalidOperationException
这是因为当集合在迭代过程中被修改时,迭代器的状态会变得无效,从而引发异常
在这里插入图片描述

个人见解:
想使用foreach遍历某种类型,需要实现IEnumerable接口中的方法GetEnumerator,
这个方法需要一个IEnumerator类型的返回值,可以自定义一个类继承IEnumerator,然后手动实现MoveNext、Current、Reset,详情见通过理解C# foreach原理看协程
这个迭代器的状态在使用foreach时就已经定了(里面的索引index,容器的大小),当通过foreach删除某个元素后,集合的内容变了,而迭代器的状态没变,前后不一致,继续运行肯定报错,有可能是索引越界,也有可能是空指针,咱也不清楚具体是啥错,所以报了一个异常InvalidOperationException

11. 下列函数double add(int a, int b)进行重载, 错误的是

A. int add(int a, int b, int c)
B. int add(double a, double b)
C. double add(double a, double b)
D. int add(int a, int b)
  • 1
  • 2
  • 3
  • 4

12. List的性能消耗主要体现在什么地方

13. 小数类型和浮点类型的区别和使用时机

  1. 精度
  • decimal类型具有更高的精度,适用于需要精确计算的场景(如财务和货币计算)
  • floatdouble类型的精度较低,适用于科学和工程计算
  1. 范围
  • decimal类型的范围较小,但能精确表示十进制数
  • float和double类型的范围较大,能表示更广泛的数值
  1. 性能
  • decimal类型的计算速度较慢,因为需要进行更多的精确计算
  • float和double类型的计算速度通常比decimal类型快,因为他们是硬件原生支持的类型
  1. 使用时机
  • 在处理货币、财务和其他需要高精度的小数运算时,应该使用decimal类型
  • 在进行科学计算、图形处理和需要处理非常大或非常小的数值时,应该使用float和double类型

14. 值类型和引用类型都有哪些

  • 值类型
  1. 整数类型:byte、sbyte、short、ushort、int、uint、long、ulong
  2. 浮点类型:float、double
  3. 高精度类型:decimal
  4. 布尔类型:bool
  5. 字符类型:char
  6. 结构体:struct
  7. 枚举:enum
  • 引用类型
  1. 类:class
  2. 接口:interface
  3. 数组:一维、二维、多维、交错
  4. 字符串:string
  5. 委托:delegate
  6. 动态类型:dynamic

15. 有什么方法可以实现多继承效果

C#不支持直接的类多继承,但可以通过以下方法模拟多继承的行为

  1. 接口
    C#支持接口的多重实现,可以使用接口来模拟多继承,每个类可以实现多个接口,并且接口可以包含方法和属性的定义
  2. 组合
    通过组合,可以将其他类的实例包含在你的类中,并在你的类中调用这些实例方法
    这是一种“有一个”而不是“是一个”的关系
  3. 接口与组合结合使用

16. 如果类A,同时实现接口IA,IB,但是IA,IB接口中都有一个名为Test的方法,类A实现这两个接口后该怎么确定调用的Test方法是哪一个

通过显示接口实现来区分

using System;

public interface IA
{
    void Test();
}

public interface IB
{
    void Test();
}

public class A : IA, IB
{
    // 显式实现 IA 接口的 Test 方法
    void IA.Test()
    {
        Console.WriteLine("IA Test method");
    }

    // 显式实现 IB 接口的 Test 方法
    void IB.Test()
    {
        Console.WriteLine("IB Test method");
    }
}

class Program
{
    static void Main()
    {
        A a = new A();

        // 通过 IA 接口调用 Test 方法
        IA ia = a;
        ia.Test(); // 输出 "IA Test method"

        // 通过 IB 接口调用 Test 方法
        IB ib = a;
        ib.Test(); // 输出 "IB Test method"
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

17. 反射的实现原理

可以在加载程序运行时,动态获取和加载程序集,并且可以获取到程序集的信息
反射即在运行期动态获取类、对象、方法、对象数据等的一种重要手段
主要使用的类库:System.Reflection
核心类:

  1. Assembly描述了程序集
  2. Type描述了类这种类型
  3. ConstructorInfo描述了构造函数
  4. MethodInfo描述了所有方法
  5. FieldInfo描述了类的字段
  6. PropertyInfo描述类的属性
    通过以上核心类可在运行时动态获取程序集中的类,并执行类构造产生类对象,动态获取对象的字段或属性值,更可以动态执行类方法和实例方法

18. for、foreach优点和缺点都有哪些

for循环

  • 优点:
  1. 索引访问:for循环允许通过索引访问元素,因此可以方便地处理索引相关的逻辑,如访问特定范围内的元素或以特定步长跳跃
  2. 灵活性:可以轻松修改循环变量,跳出循环,或者根据复杂条件来控制循环
  3. 效率:对于某些集合类型(如数组),for循环可能比foreach更高效,因为foreach需要获取集合的枚举器,而for直接使用索引访问
  • 缺点:
  1. 可读性:在某些情况下,特别是涉及复杂的循环条件或步骤时,for循环可能比foreach更难读懂
  2. 错误易发:使用for循环容易出错,例如下标越界、索引变量初始化错误、忘记更新循环变量等等

foreach循环

  • 优点:
  1. 简单性:foreach循环语法简单,适合遍历集合中的所有元素,不需要关心索引和边界问题
  2. 安全性:foreach循环能防止越界和索引相关错误,因为它不适用显示索引
  3. 可读性:代码更加简洁和易读,特别适合需要对集合中的每个元素进行相同操作的场景
  • 缺点:
  1. 灵活性较低:foreach循环不能修改集合的结构(如添加或删除元素),在循环内部也不能直接修改当前元素(除非元素是引用类型并且修改其内部状态)
  2. 效率可能较低:对于某些集合类型,foreach可能比for循环效率低,因为它需要创建和管理枚举器对象
  3. 不适用索引访问:foreach不提供对元素的索引访问,不能轻易获取当前元素的索引

19. 泛型什么时候替换为明确的类型,什么时候被擦除?

在C#中,泛型参数类型在编译时不会被擦除,而是会被保留,并在运行时替换为具体的类型。
这与Java的泛型擦除机制不同(在编译时,Java的泛型机制使用类型擦除来转换泛型类型为原始类型(通常是Object),并插入必要的类型转换。这样做的目的是为了保持与旧版本Java的兼容性)。
在C#中,泛型类型参数的行为可以通过以下几个方面来理解:

  1. 编译时替换
  • 当编译器处理泛型类型时,它会生成通用的IL(中间语言)代码,并在必要时使用特定类型参数替换泛型类型参数。
  • 这样做的好处是保留了类型信息,使得类型检查和类型安全能在编译时和运行时得到保障
  1. 运行时替换
  • 在运行时,CLR(公共语言运行时)会根据泛型类型参数创建具体的类型实例
  • 例如,当你实例化一个泛型类时,如List<int>,CLR会生成一个专门针对int类型的List实例
  1. 类型参数约束
  • C#允许在定义泛型时对泛型参数进行约束
  • 例如限制类型参数必须实现某个接口或继承自某个类
  • 这些约束在编译时被检查,并在运行时强制执行
  1. 值类型和引用类型
  • 对于值类型(例如intdouble),CLR会为每个具体的值类型创建一个单独的泛型类型实例。这意味着List<int>List<double>会生成不同的IL代码
  • 对于引用类型(例如stringobject等),CLR会创建一个共享的泛型类型实例,从而避免代码膨胀
  1. 反射
  • 由于泛型类型参数在运行时不会被擦除,因此你可以通过反射机制获取泛型类型的具体类型参数。
  • 这使得你能够在运行时动态地检查和操作泛型类型
  1. 示例代码
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> intList = new List<int>();
        intList.Add(1);
        intList.Add(2);

        List<string> stringList = new List<string>();
        stringList.Add("Hello");
        stringList.Add("World");

        PrintList(intList);
        PrintList(stringList);
    }

    static void PrintList<T>(List<T> list)
    {
        foreach (T item in list)
        {
            Console.WriteLine(item);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 在这个示例中,PrintList<T>是一个泛型方法,它可以接受任何类型的List<T>参数。
  • 在编译时,编译器会生成通用的IL代码,并在运行时根据传递的具体类型实例化T
  • 这确保了类型安全,并在运行时保留了类型信息

20. List的Capacity和Count有什么区别?什么情况下使用Capacity?

Count

  • 表示List<T>中当前实际包含的元素数量
  • 用于获取当前元素的数量

Capacity

  • 表示当前List<T>在不重新分配内存的情况下可以包含的元素数量
  • 用于内存管理和性能优化,特别是在预先知道元素数量或需要批量添加元素时
  • 在性能敏感的应用中,合理管理Capacity可以减少内存分配和数据复制的开销
List<int> numbers = new List<int>(100); // 预先分配容量为 100

List<int> numbers = new List<int>();
numbers.Capacity = 100; // 预先设置容量
for (int i = 0; i < 100; i++)
{
    numbers.Add(i);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

21. string相加和StringBuilder.Append的主要区别是什么

22. 多线程和协程的区别是什么

二、Unity

1. 继承自MonoBehaviour的脚本为什么不能通过构造函数实例化对象

  1. Unity的组件系统基于GameObject和Component的架构,每个MonoBehaviour都是一个组件,可以附加到GameObject上,Unity引擎本身管理这些组件的创建、销毁和生命周期
  2. 通过AddComponent方法将MonoBehaviour脚本附加到GameObject上
  3. 实例化预制体时,其中包含附加的MonoBehaviour脚本
    生命周期方法不会被调用
  4. 由于MonoBehaviour对象的生命周期由Unity引擎控制,直接通过new关键字实例化会绕过Unity的管理系统,可能会导致以下问题①缺少初始化(Unity对MonoBehaviour对象的初始化包括调用特点的生命周期方法),这些方法在直接通过构造函数创建对象时不会被调用
    对象不会被正确管理
  5. 缺少关联,通过构造函数创建的MonoBehaviour对象不会自动关联到GameObject上,因此无法作为正常组件使用
  6. 如果需要自定义初始化逻辑,可以定义一个初始化方法,而不是依赖构造函数
    无法序列化、反序列化
  7. Unity引擎通过特定的序列化机制来保存和加载场景和对象状态。通过构造函数创建的对象不受Unity的序列化系统管理,可能导致状态丢失和无法正确保存游戏进度等问题
    可能引发异常
    如果使用构造函数来实例化脚本 然后手动对成员变量进行初始化,由于脚本还未启动,可能会报空指针异常
  8. 在构造函数中,成员变量可能尚未被正确初始化,尤其是那些依赖Unity引擎初始化的成员
public class MyScript : MonoBehaviour
{
    public GameObject otherObject;

    public MyScript()
    {
        otherObject.transform.position = Vector3.zero; // 可能会报空指针异常
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在上述代码中,otherObject可能尚未被初始化,这会导致NullReferenceException

2. 简述进程、线程、协程的概念

进程

  • 保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,不同进程间可以进行进程间通信,上级挂靠单位是操作系统。
  • 一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源)
  • 进程是资源分配的最小单位

线程

  • 线程从属于进程,也被称作轻量级进程,是程序的实际执行者。
  • 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  • 一条线程指的是一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
  • 一个线程只有一个进程
  • 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
  • 线程拥有自己独立的栈和堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)

协程

  • 协程是伴随主线程一起运行的一段程序
  • 协程与协程之前是并行执行,与主线程也是并行执行,同一时间只能执行一个协程
  • 提起协程自然是想到线程,因为协程的定义就是伴随主线程来运行的
  • 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制
  • 协程和线程一样共享堆,不共享栈,协程是由程序员在协程代码里显示调度
  • 协程是单线程下由应用程序级别实现的并发

3. 简述协程的底层原理

  • 协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义一个迭代方法
  • StartCoroutine接收到的是一个IEnumerator,这是个接口,并且是枚举器或迭代器的意思
  • yield是C#的一个关键字,也是一个语法糖,背后的原理会生成一个类,并且也是一个枚举器,而且不同于return,yield可以出现多次
  • yield实际上就是返回一次结果,因为我们要一次次枚举一个值出来,所以多个yield其实是个状态模式,第一个yield是状态1,第二个yield是状态2,每次访问会基于状态知道当前应该执行哪一个yield,取得哪一个值

4. 什么是DrawCall,如何降低

Unity中,每次引擎准备数据并通知GPU的过程称为一次DrawCall
DrawCall越高对显卡的消耗就越大
降低DrawCall的方法

  • 动态批处理
  • 静态批处理
  • 高级特性Shader降级为统一的低级特性Shader

5. Text和Text Mesh Pro的区别、优缺点

  1. Text是像素渲染放大之后就会模糊,使用Text父物体的放大缩小会影响子物体Text的清晰度
  2. TMP不会,它是网格渲染,TMP会把字体生成一个类似于贴图的东西,然后读取贴图的坐标来获取对应的文字,更换文字的消耗会比Text大
  3. TMP更适合用于不会变动的文字,特别是在量大的情况下,性能比Text高一些
  4. 需要经常变动的文字用Text好点
  5. TMP在字体库很大的情况下查找更换会比较慢

6. 红点系统的实现

红点系统基于MVC思想,将分为三层:数据层、驱动层、显示层

  • 数据层中的数据结构,考虑到需要层级的联系,所以以节点为核心,每个节点会持有其父结点和子节点,有点像双向链表的前驱后继,但是它构成的不是链表而是树
  • 当一个节点发生变化,他会通知到其父节点,父节点会自行处理变化去通知他自己的父节点,有点递归的意思,如果是数量通知,子节点的消息会以此累计到自己的父节点中,具体看需求
  • 整个系统数据层、驱动层与显示层是剥离的,显示层需要显示什么节点的内容,以该节点的key去注册,数据层与显示层实现了观察者模式,即可收到每次该节点状态变化的通知,并实时更新界面

更多详细内容可以看下面文章
Unity之红点树系统多层级高效能
Unity手游实战:从0开始SLG——独立功能扩展(三)用树实现客户端红点系统

7. 常用的性能优化有哪些

  1. 渲染优化
  • 减少Draw Calls
    使用静态批处理和动态批处理
    合并网格(Mesh)以减少渲染调用
    使用GPU实例化
  • 使用LOD(Level Of Detail)
    根据对象的距离切换不同细节层次的模型
  • 减少实时光照
    使用光照贴图
    减少实时光源数量,使用烘焙光源
  • 优化阴影
    降低阴影质量
    使用阴影距离和阴影裁剪优化阴影渲染
  • 使用高效的后处理效果
    避免不必要的后处理效果,使用优化的后处理链
  1. 脚本优化
  • 避免使用Update方法
    使用事件系统代替持续检查
    使用协程(Coroutine)处理定时任务
  • 对象池
    使用对象池重用频繁创建和销毁的对象
  • 减少GC
    避免频繁分配和释放内存
    使用集合代替数组,避免foreach循环
    使用结构体而不是类来减少GC压力
  1. 内存管理
  • 优化资源加载
    使用Addressables或AssetBundle进行资源管理
    延迟加载和异步加载资源
  • 及时释放未使用的资源
    使用Resources.UnloadUnusedAssets()来释放未使用的资源
    手动调用GC.Collect()进行垃圾回收(慎用)
    调用Addressables.Release(handle)或AssetBundle.UnloadAllAssetBundles(false)
  1. 使用性能分析工具
  • 使用Unity Profiler
    分析性能瓶颈,找出CPU和GPU的热点
  • 使用外部工具
    如Xcode instruments(IOS)、Android Profiler、Vulkan、RenderDoc等等
  1. 平台特定优化
  • 移动设备
    降低分辨率和质量设置
    降低纹理压缩格式(例如图集)
  • PC和主机
    使用高效的多线程和并行计算
    优化Shader和特效
  1. 动画、网络、物理层面的优化

8. 协程的概念和原理

Unity的协程是一种用于执行延迟任务或分布在多个帧上的长时间运行任务的机制。
协程允许在逻辑上连续的代码在不同的时间段执行,而不会阻塞主线程。
这对于处理动画、等待时间或异步任务非常有用

  1. 执行流程
  • 启动流程:当调用到StartCoroutine时,Unity会记录协程的执行状态,并开始执行协程函数的代码
  • 执行到yield指令:协程执行到yield return指令时,会暂停并返回控制权给调用者。协程的当前状态和位置会被保存
  • 恢复执行:在下一帧或指定条件满足时,Unity会恢复协程的执行,从上次暂停的地方继续执行
  • 结束协程:协程执行完所有代码或被手动停止时结束
  1. 实现原理
    Unity的协程基于C#的迭代器实现。
    IEnumerator接口允许协程在yield指令处中断,并在以后继续执行。
    协程的实现依赖于Unity的引擎循环,每帧都会检查所有运行的协程,并决定是否恢复他们的执行
using System.Collections;
using UnityEngine;

public class CoroutineExample : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(MyCoroutine());
    }

    private IEnumerator MyCoroutine()
    {
        Debug.Log("Coroutine started");
        yield return new WaitForSeconds(2f);
        Debug.Log("Coroutine resumed after 2 seconds");
        yield return null;
        Debug.Log("Coroutine finished");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  1. 应用场景
  • 动画和特效:通过协程分段执行复杂的动画和特效
  • 异步任务:处理需要等待时间的异步任务,如加载资源或网络请求
  • 时间控制:实现定时器、冷却时间等功能

9. 协程与生命周期函数的关系

  1. Awake和OnEnable:
    这些函数在对象被创建或启动时调用,通常用于初始化。
    在这些函数中启动协程并不常见,因为对象可能还未完全准备好
  2. Start:
    在第一次调用Update之前调用,适合启动需要在对象初始化完成后执行的协程任务
  3. FixedUpdate:
    在物理更新阶段调用,适用于处理物理相关的操作。
    在此函数中启动协程,可以确保协程在物理更新后按需回复
  4. Update:
    在每帧调用,用于处理常规更新操作。
    在此函数中启动协程,可以确保协程在每帧按需恢复
  5. LateUpdate:
    在每帧的所有Update函数调用后调用,用于处理需要在所有更新后执行的操作。
    可以在此函数中启动协程,以确保在所有更新完成后执行协程任务。
  6. OnDisable和OnDestroy:
    在对象被禁用或销毁时调用。
    协程会自动停止,可以在这些函数中调用StopCoroutine或StopAllCoroutine手动管理协程

10. 如何从子线程回到主线程

使用SynchronizationContext

  1. 获取主线程的SynchronizationContext
    在启动时获取主线程的SynchronizationContext
using System.Threading;
using UnityEngine;

public class MainThreadContext : MonoBehaviour
{
    private static SynchronizationContext _mainThreadContext;

    void Awake()
    {
        _mainThreadContext = SynchronizationContext.Current;
    }

    public static void RunOnMainThread(Action action)
    {
        _mainThreadContext.Post(_ => action(), null);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 在需要从子线程回到主线程的地方使用它
using System.Threading;
using UnityEngine;

public class Example : MonoBehaviour
{
    void Start()
    {
        Thread thread = new Thread(ThreadFunction);
        thread.Start();
    }

    void ThreadFunction()
    {
        // Simulate some work on a background thread
        Thread.Sleep(2000);

        // Run code on the main thread
        MainThreadContext.RunOnMainThread(() =>
        {
            Debug.Log("This is executed on the main thread");
        });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

11. 逐像素光照模型的特点

A. 渲染效率更高
B. 渲染效果更平滑
C. 只能用来计算漫反射
D. 渲染次数更少

12. 如何加载、卸载一个Texture2D对象(需要区分StreamingAssets和Resources)

13. 使用UGUI实现一个圆形头像有哪些方式

14. 什么是DrawCall?有什么方法可以减少DrawCall?

15. 简述一下游戏屏幕适配方式

16. 要实现修改Image组件的显示大小,但是触发区域不变的方法有哪些

17. 如何对某些对象进行截图

18. Layout Element各个属性的作用

在这里插入图片描述

  • Ignore Layout:如果勾选,该元素将被布局系统忽略,不会影响其父布局组的布局计算
    使用场景:
    1.临时隐藏元素:当你希望某个元素在布局中临时不可见或不占用空间时,可以设置ignoreLayouttrue
    2.动态布局:在需要动态添加或移除元素时,可以使用ignoreLayout控制哪些元素应该参与布局计算
    3.特定元素固定位置:如果某个元素需要固定在特定位置,而不受布局组的影响,可以设置ignoreLayouttrue
  • Min Width:用于设置UI元素在布局系统中占据的最小宽度。这意味着,当父布局组计算布局时,该元素的宽度不会小于设置的最小宽度值
    1.确保元素的可见性:设置最小宽度可以确保某些UI元素始终保持可见,不会因父布局组的大小变化而过于缩小
    2.响应式布局:在响应式布局中,确保某些关键元素始终有足够的宽度来显示其内容
    3.一致性:在同一布局组内,不同元素的最小宽度可以保持一致性,使布局更加整齐和美观
  • Min HeightMin Width大致同理
  • Preferred Width:用于设置UI元素在布局系统中占据的理想宽度。布局系统会尝试根据该值分配元素的宽度,但如果空间不足,它会根据其他布局规则(如Min WidthFlexible Width)进行调整
    1.控制布局比例:通过设置Preferred Width可以控制不同UI元素在布局中的相对比例
    2.自适应布局:在响应式设计中,可以根据不同元素的内容或设计需求,设置理想宽度,从而使布局更加美观和实用
    3.保持一致性:确保同一布局中的元素在有足够空间时,保持一致的宽度
  • Preferred Height使用和Preferred Width大致同理

举例说明:
假如说现在有一个父节点Father,
其挂载了Horizontal Layout Group,勾选ControlChildSize和ChildForceExpand,
其下有三个Image都挂载了Layout Element,且设置了Preferred Width分别为100、200、300,
当Father宽度设置为600时,三个Image宽度分别是100、200、300
当Father设置宽度为300,三个Image将按比例平分,最后宽度分别为50、100、150

  • Flexible Width:用于指定UI元素在布局系统中能够占据的额外宽度。当布局系统在分配空间时,会首先满足Min WidthPreferred Width,然后根据剩余的可用空间和个元素的Flexible Width属性值,来分配额外的宽度
    1.弹性布局:Flexible Width允许UI元素根据剩余空间动态调整大小,从而使布局更具弹性和响应性
    2.比例分配:通过设置不同的Flexible Width值,可以控制各元素在布局中的相对比例。例如,如果一个元素的Flexible Width设置为100,另一个设置为200,那么在剩余空间分配时,第二个元素将获得更多的空间
    3.布局优化:确保在屏幕或窗口尺寸发生变化时,布局能够自动调整,提供更好的用户体验
  • Flexible HeightFlexible Width使用大致相同
  • Layout Priority:用于指定多个布局元素在同一个父布局容器中的优先级。它决定了在进行布局计算时,哪个元素应该优先考虑。

19. 动态批处理和静态批处理是什么?如何在使用的时候打断他们?

20. RawImage和Image有什么使用区别?

21. RawImage也可以使用Sprite2D,为什么不用它来显示UI?

22. 不使用InputField如何实现文本自动换行且高度自适应?

23. 如何实现打字机效果,都有哪些方法

24. 如果父节点和子节点都挂载了ContentSizeFitter,哪个优先级更高呢

25. 请说明MonoBehaviour中Awake、Start、Update、LateUpdate、FixedUpdate的区别

AwakeOnEnableStartFixedUpdateUpdateLateUpdateOnDisableOnDestroy

  • Awake:用于在脚本实例被加载时初始化对象。通常在对象的生命周期内只调用一次。
  • Start:用于在脚本实例启用后的第一个帧更新前初始化对象,也是只调用一次
  • Update:用于处理每帧更新的逻辑
  • LateUpdate:用于在每帧的Update方法之后进行处理
  • FixedUpdate:用于以固定时间间隔处理物理相关的逻辑

FixedUpdate通常先于Update调用,但不一定总是如此,具体取决于物理步长(Time.fixedDeltaTime)和帧率
在帧率较高或较低的情况下,FixedUpdateUpdate的调用顺序可能会有所不同

26. Unity提供了哪几种光源,主要区别是什么

  1. Directional Light(方向光)
  • 特性:① 模拟从一个方向照射过来的平行光线,类似于太阳光 ② 光线是平行的,没有衰减,光源位置对效果没有影响,只考虑方向
  • 用途:适用于大范围的均匀光照,如户外场景中的阳光
  • 性能:对性能影响较小,适合用来照亮整个场景
  1. Point Light(点光源)
  • 特性:① 从一个点向所有方向发射光线,光线在距离增加时衰减 ②有范围和衰减,光强随着距离减小
  • 用途:适用于模拟灯泡、火把等发光物体
  • 性能:可能对性能有一定影响,特别是在场景中有大量点光源时
  1. Spot Light(聚光灯)
  • 特性:① 从一个点向一个圆锥形区域发射光线,有方向和角度限制 ② 有范围、角度、衰减
  • 用途:适用于手电筒、舞台灯光、车灯等需要聚焦照明的场景
  • 性能:对性能有一定影响,特别是在场景中有复杂阴影或多个聚光灯时
  1. Area Light(区域光)
  • 特性:① 从一个矩形区域发射光线,光线从区域的每一点均匀发射 ② 只在烘焙光照模式下可用,不能实时使用
  • 用途:适用于模拟窗户、天窗等发出均匀光线的面光源
  • 性能:对性能影响较小,因为仅在烘焙光源时使用,不适用于实时计算
  1. Emissive Materials(自发光材质)
  • 特性:① 使用材质的自发光属性来模拟发光物体 ② 光源位置由材质上的发光区域决定,不是真正的光源类型
  • 用途:适用于屏幕、霓虹灯、发光纹理等场景
  • 性能:对性能影响较小,但不能生成动态阴影

27. 怎么使用Layer和Tag

  • Layers:用于管理物理和渲染相关的操作,可以设置碰撞矩阵和相机剔除掩码
  • Tags:用于识别和分类游戏对象,可以通过脚本查找和操作特定标签的对象

28. MeshRender中material和sharedMaterial的区别

在Unity中,MeshRenderer组件有两个属性与材质相关:materialsharedMaterial,他们之间的区别在于如何使用和共享材质实例,影响到材质的独立性和性能。以下是它们的详细区别和使用方法:

material

  • 独立实例:material属性返回的是一个材质的独立实例。这意味着对该材质属性的修改不会影响到其他使用相同材质的对象
  • 适用场景:当你希望对某个对象的材质进行独立修改而不影响其他对象时,使用material属性
  • 自动化实例:如果你对material属性进行修改,Unity会在后台自动为该对象创建一个新的材质实例,即使多个对象原本共享同一个材质

sharedMaterial

  • 共享实例:sharedMaterial属性返回的是一个材质的共享实例。这意味着对该材质属性的修改会影响到所有使用相同材质的对象
  • 适用场景:当你希望多个对象共享同一个材质实例,并且希望对该材质的修改影响到所有这些对象时,使用sharedMaterial属性
  • 不自动实例化:使用sharedMaterial属性进行修改不会创建新的材质实例,因此多个对象可以真正共享同一个材质,节省内存和提高性能

性能考虑

  • 内存占用:使用material属性会创建新的材质实例,这会增加内存占用,特别是当有大量对象使用相同的材质时
  • 性能优化:使用sharedMaterial属性可以减少内存占用,因为多个对象共享同一个材质实例,有助于优化性能,特别是在需要频繁调整材质属性的情况下

注意事项

  1. 避免在运行时频繁使用material
  2. 使用sharedMaterial进行批量修改

29. MipMap有什么作用,texture是否一定需要生成mipmap

30. Animator和Animation的区别是什么

  • Animator
    在这里插入图片描述
    Animator是Unity的新动画系统的一部分,称为Mecanim。它提供了更强大的功能和灵活性,适用于复杂的动画控制和状态管理

主要特性和功能

  1. 动画状态机
    • 使用Animator,可以创建复杂的动画状态机(Animator Controller),在不同的动画状态之间进行平滑过渡
  2. 参数控制
    • 可以使用参数(如触发器、布尔值、整数、浮点数)来控制动画状态的切换
  3. Blend Trees
    • 支持混合树(Blend Trees),用于根据参数值平滑混合多个动画
  4. 动画层
    • 可以创建多个动画层,分别控制不同部分的动画,每个层可以有不同的权重和混合模式
  5. 动画事件
    • 可以在动画中设置事件,在特定的时间点调用脚本中的函数
  6. 与脚本交互
    • 可以通过脚本控制动画参数,实现复杂的动画逻辑
using UnityEngine;

public class AnimatorExample : MonoBehaviour
{
    private Animator animator;

    void Start()
    {
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        // 根据输入控制动画参数
        if (Input.GetKeyDown(KeyCode.Space))
        {
            animator.SetTrigger("Jump");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • Animation
    在这里插入图片描述
    Animation是Unity的旧动画系统,通常用于简单的动画控制。它使用动画剪辑直接控制对象的变换、旋转、缩放等属性
    主要特性和功能
  1. 简单动画播放
    • 直接播放动画剪辑,没有复杂的状态机和参数控制
  2. 动画剪辑
    • 可以直接在Animation组件中管理和播放动画剪辑
  3. 不支持状态机
  • 不支持复杂的动画状态机和过渡,适用于简单的动画需求
  1. 脚本控制
    • 可以通过脚本直接控制动画的播放、暂停、停止等操作
using UnityEngine;

public class AnimationExample : MonoBehaviour
{
    private Animation animation;

    void Start()
    {
        animation = GetComponent<Animation>();
    }

    void Update()
    {
        // 根据输入播放动画
        if (Input.GetKeyDown(KeyCode.Space))
        {
            animation.Play("JumpAnimation");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

总结

  • Animator
    • 使用Mecanim系统,支持动画状态机、参数控制、混合树、动画层等高级功能
    • 适用于复杂动画控制和管理
    • 适用于角色动画、状态切换、参数控制等复杂动画需求
    • 更灵活、功能更强大,推荐用于大多数新项目
  • Animation
    • 适用于旧的动画系统,适用于简单的动画需求
    • 适用于简单的对象动画,如开门、旋转等基本动画效果
    • 可以在某些简单场景中更快速地实现动画效果
    • 直接播放动画剪辑,没有复杂的状态管理

31. GameObject.DontDestroyOnLoad的主要作用是什么?什么情况下会使用这个接口?

三、Lua

1. 以下Lua程序输出结果是

print(type(a))

a = 10
print(type(a))

a = print
print(type(a))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2. Lua中ipairs和pairs的区别

四、其他

1. 使用Android Studio开发一个功能(例如手机震动),在Unity中怎么调用它

步骤一

  1. 创建Android Library项目
  • 打开Android Studio,选择File -> New -> New Project,然后选择Android Library
  • 设置项目名称和包名,然后点击Finish
  1. 实现功能
  • 打开src/main/java/your/package/name/目录下MainActivity.java(可以重命名为VibrationPlugin.java,因为这是一个库项目,不需要MainActivity.java)
  • 编写代码实现
  1. 导出Android Library
  • 点击Build -> Build Bundle(s) / APK(s) -> Build APK(s),将生成的.aar文件(在build/outputs/aar/目录下)拷贝出来备用
    步骤二
  1. .aar文件导入Unity项目
  • 将导出的.aar文件拷贝到Unity项目的Android/Plugins/Android/目录下
  1. 创建一个脚本来调用Android插件
  2. 使用这个功能

2. 向量乘法中的点乘和叉乘分别表示什么____

3. 下面代码实现的功能是____

mul(UNITY_MATRIX_MVP, v.vertex)

4. AssetBundle的打包压缩格式有哪些是常用的,他们的区别是什么

5. 前向(Forward Rendering)和延迟渲染(Deferred Rendering)的主要区别和优劣是什么,什么情况选择使用前向渲染管线,什么情况会考虑使用延迟渲染

6. Canvas绘制时主要的性能瓶颈有哪些,都怎么优化

  • 性能瓶颈
  1. 重建布局和绘制
    • 每次修改Canvas或其子元素时,都会触发重建布局和重新绘制。频繁的修改会导致性能下降
  2. 过度使用Canvas组件
    • 每个Canvas都会有自己的渲染批次(Batch),过多的Canvas会导致更多的渲染批次,从而降低性能
  3. 频繁的重新计算
    • 改变Canvas元素的属性(如大小、位置、旋转等)会导致重新计算和重新绘制,特别是在使用复杂布局时
  4. 过度使用嵌套Canvas
    • 嵌套的Canvas会增加每次重新绘制的复杂性,导致性能下降
  5. Canvas的Render Mode设置不当
    • 不同的Render Mode(Screen Space - Overlay,Screen Space - Camera,World Space)有不同的性能表现,选择不当会影响性能
  • 优化方法
  1. 减少Canvas重建频率
    • 尽量减少对Canvas及其子元素的频繁修改,避免不必要的属性更改
    • 将频繁变化的UI元素放在单独的Canvas中,避免影响整个UI层次结构
  2. 合并Canvas
    • 合并尽可能多的UI元素到一个Canvas中,以减少渲染批次
    • 使用Canvas Group来控制子元素的透明度、交互性和显示状态,而不是单独的Canvas
  3. 使用Canvas分层
    • 将静态UI元素(不常变化的部分)和动态UI元素(频繁变化的部分)放在不同的Canvas中,这样,静态部分不会在每次更新时重新绘制
  4. 优化Canvas Render Mode
  • 根据实际需求选择合适的Render Mode
    • Screen Space - Overlay:适用于全屏UI,性能较好
    • Screen Space - Camera:适用于需要深度排序的UI
    • World Space:适用于3D UI,但性能较差
  1. 减少嵌套Canvas
  • 避免深层次的Canvas嵌套,尽量将所有UI元素放在同一层级或少量层级中
  1. 优化更新频率
  • 使用CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuildCanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild来控制什么时候进行布局和绘制重建
  • 通过脚本控制UI更新频率,例如每帧更新改为按需更新
  1. 使用对象池
  • 对于频繁创建和消耗的UI元素,使用对象池来复用实例,减少内存分配和垃圾回收的开销
  1. 减少使用透明度
  • 尽量减少透明UI元素的使用,因为透明元素的绘制成本更高
  • 如果必须使用透明度,尽量将透明度相似的元素放在一起,以减少过度绘制
  1. 优化纹理和图像
  • 确保使用适当分辨率和压缩格式的纹理,减少内存使用
  • 使用图集(Sprite Atlas)将多个小图合并成一个大图,减少材质切换和渲染批次

7. 什么情况下选择GPU Instantcing优化draw call,什么情况下选择static batching?

8. 如果程序运行时GC带来较大的性能波动,可能是哪些问题引起的,该怎么优化

9. 内存碎片化是怎么发生的?产生的后果是什么?

10. 列举一些熟悉的设计模式并简述使用场景

11. 多人游戏的同步方案一般都有哪几种,简述其原理

12. 手机游戏运行时占用内存较大带来的主要问题是什么

13. 目前热更方案(Xlua,ILRuntime)的主要热更原理是什么

参考答案:

一、C#

11. D

重载函数:返回值不同,参数类型不同,参数个数不同
A 参数个数不同,正确
B 返回值类型不同,正确
C 参数类型不同,正确
D 参数类型和个数相同,调用有歧义

12.

  1. 内存分配与释放:添加或删除元素时,可能会触发内部数组的重新分配和复制操作,特别是在动态增长时。频繁的分配和释放内存会带来性能开销
  2. 插入与删除:在中间位置插入或删除元素会导致后续元素的移动,这可能涉及大量的数据复制操作,特别是对于大型List而言
  3. 大量元素的查找与移除同上所述
  4. 装箱拆箱
  5. 多线程访问:如果多个线程同时访问或修改List,需要考虑线程安全性,可以通过锁或其他同步机制来保护List,这可能也会带来额外的开销

二、Unity

11. B

A 逐像素渲染效率肯定不高,计算量大,由于需要在每个像素上进行光照计算,逐像素光照的计算量通常比逐顶点光照大。这对于性能要求较高的应用(如实时渲染)可能会有一定的挑战
B 逐像素光照可以在每个像素级别上进行插值和计算,使得光照效果在多边形表面上更加平滑和自然。这种平滑效果尤其在高光部分和阴影过渡处表现得更加自然
C 精确度高,每个像素都经过光照计算,因此可以获得更精确的光照效果。逐像素光照能够考虑到每个像素的法线和光照方向,产生更细致的阴影和反射效果
D 不解释了

12.

Resources.Load<Texture2D>("Textures/MyTexture"), Resources.UnloadAsset(texture);
StreamingAssets文件夹中的文件不会被Unity压缩打包,可以直接读取文件数据(如图片、文本等等)。它适用于存储需要按原样访问的大文件

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class TextureLoader : MonoBehaviour
{
    private Texture2D texture;

    void Start()
    {
        StartCoroutine(LoadTextureFromStreamingAssets("Textures/MyTexture.png"));
    }

    IEnumerator LoadTextureFromStreamingAssets(string relativePath)
    {
        string path = System.IO.Path.Combine(Application.streamingAssetsPath, relativePath);
        using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(path))
        {
            yield return www.SendWebRequest();
            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError("Failed to load texture: " + www.error);
            }
            else
            {
                texture = DownloadHandlerTexture.GetContent(www);
                GetComponent<Renderer>().material.mainTexture = texture;
            }
        }
    }
}

void OnDestroy()
{
    if (texture != null)
    {
        Destroy(texture);
        texture = null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

13.

  • 使用Mask组件
  • 使用Shader
  • 使用Sprite Mask
  • 使用RectMask2D

14.

DrawCall是指在渲染过程中,由CPU向GPU发送的绘制指令。
每个DrawCall代表一次从CPU到GPU的绘制指令传递,它通常包括渲染一个对象的几何信息、材质、着色器参数等
当场景中的对象数量增多或材质、着色器变化频繁时,DrawCall的数量会急剧增加,导致性能下降

减少DrawCall

  • 合并网格(①将多个小的网格合并为一个大网格,从而减少需要绘制的对象数量②静态批处理、动态批处理)
  • 使用实例化渲染
  • 减少材质数量
  • 使用图集
  • 使用四边形绘制(对于2D游戏或GUI元素,使用四边形而不是三角形来减少顶点数量)
  • 合并材质属性
  • 简化着色器
  • 层级视图优化
  • 使用LOD
  • 剔除

15.

 
using UnityEngine;
using UnityEngine.UI;
 
public class ScalerMatchSetting : MonoBehaviour
{
    public bool Debug = false; //开启会在Update调用,方便调试
    CanvasScaler scaler;
    private void Awake()
    {
        scaler = GetComponent<CanvasScaler>();
        AutoSetMatch();
    }
 
    private void AutoSetMatch()
    {
        if (scaler)
        {
            // 16 / 9 = 1.777777.....   判断  宽高比大于 1.78则视为分辨率大于 16:9
            float ratio = (float)Screen.width / (float)Screen.height;
            scaler.matchWidthOrHeight = ratio > 1.78 ? 1 : 0;
        }
    }
 
#if UNITY_EDITOR_WIN
    private void Update()
    {
        if (Debug)
        {
            AutoSetMatch();
        }
    }
#endif
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

16.

  • 在Image的父对象上添加RectMask2D组件,可以显示部分Image的内容
  • 通过修改Image组件的材质属性,可以控制图像的缩放,而不影响其触发区域

17.

  1. 创建一个新的Camera
  2. 设置RenderTexture
  3. 渲染并读取像素数据
  4. 保存截图

19.

在 Unity 中,动态批处理和静态批处理是两种用于优化渲染性能的方法,它们通过减少需要发送到 GPU 的绘制调用数量来提高效率。

以下是它们的定义、区别和打断方式:

动态批处理 (Dynamic Batching):
定义:
动态批处理是指在运行时将多个相似的对象(例如使用相同材质的对象)合并成一个批次进行渲染。这种方法适用于小型对象(顶点数少于 300 个),可以在场景中节省大量的绘制调用。

特点:

  • 自动启用:在 Unity 项目设置中启用后,Unity 会自动处理。
  • 适用于小型对象:顶点数较少的对象。
  • 运行时合并:批处理在运行时进行。

打断方式:

  • 不同的材质:使用不同材质的对象无法合并。
  • 不同的变换:不同的位置信息(如旋转、缩放、位置)会打断批处理。
  • 超过顶点限制:对象顶点数超过 300 个。
  • 启用某些渲染选项:例如 GPU 实例化。

静态批处理 (Static Batching)
定义:
静态批处理是在编辑器模式下,将不移动的静态对象合并成一个批次进行渲染。这种方法适用于大场景中的静态对象,例如地形、建筑物。

特点:

  • 手动启用:需要在编辑器中标记对象为静态。
  • 适用于静态对象:不会在运行时移动的对象。
  • 编辑器预处理:批处理在编辑器模式下完成。

打断方式:

  • 对象不标记为静态:对象必须在编辑器中标记为静态。
  • 不同的材质:使用不同材质的对象无法合并。
  • 启用某些渲染选项:例如 GPU 实例化。

启用动态批处理(有些Unity版本可能找不到这个选项,原因是已经默认启用了)
在 Unity 项目设置中启用动态批处理:

  • 打开 Unity 菜单:Edit > Project Settings > Player。
  • 在 “Other Settings” 部分,勾选 “Dynamic Batching”。

启用静态批处理
在编辑器中为对象启用静态批处理:

  • 选择对象。
  • 在 Inspector 面板中,勾选 “Static” 选项
    在这里插入图片描述
    在这里插入图片描述

20.

RawImageImage是Unity中两个用于在UI中显示图像的组件。
它们的主要区别在于它们支持的图像类型以及一些具体的用途和功能。
以下是它们的详细区别和各自的使用场景:
RawImage

  • 用途:用于显示纹理(Texture),而不是精灵(Sprite)
  • 支持的图像类型:支持Texture2DRenderTexture等所有类型的纹理
  • 应用场景:适用于需要显示非精灵纹理的情况,例如直接显示相机渲染的纹理、视频帧、动态生成的纹理等
using UnityEngine;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    public RawImage rawImage;
    public Texture2D texture;

    void Start()
    {
        // 将纹理分配给 RawImage
        rawImage.texture = texture;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Image

  • 用途:用于显示精灵(Sprite),主要用于UI元素(按钮、图标等)
  • 支持的图像类型:只支持Sprite类型
  • 应用场景:适用于需要显示精灵图像的情况,通常用于按钮、图标、背景等UI元素
using UnityEngine;
using UnityEngine.UI;

public class Example : MonoBehaviour
{
    public Image image;
    public Sprite sprite;

    void Start()
    {
        // 将精灵分配给 Image
        image.sprite = sprite;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

使用区别

  • 图像类型:
    • RawImage用于显示Texture
    • Image用于显示Sprite
  • 性能:
    • Image组件有一些优化,特别是与SpriteAtlas配合使用时,可以减少draw call
    • RawImage通常用于特定用途的纹理显示,可能没有Image那么高效
  • 功能:
    • Image提供了更多与精灵相关的功能,比如九宫格切割、填充模式(如圆形进度条)、精灵的渲染模式等
    • RawImage更简单,适用于显示原始纹理数据

更多见 Image与RawImage

21.

虽然RawImage可以用来显示Sprite2D类型的纹理,但在大多数情况下,使用Image组件会更适合显示UI元素
专为UI设计的功能

  1. 九宫格切图:
  • Image支持九宫格切割,这对于制作可缩放的UI元素(如按钮、面板等)非常重要
  • RawImage不支持九宫格切割
  1. 填充模式
  • Image支持各种填充模式(水平、垂直、径向等),可以方便地实现进度条、加载条等效果
  • RawImage不支持这些填充模式
  1. 性能优化:
  • Image组件有专门针对精灵的优化,特别是与SpriteAtlas配合使用时,可以减少draw call,提高渲染性能
  • RawImage组件没有这些优化

性能和兼容性

  1. 图集支持
  • Image可以与Sprite Atlas配合使用,减少draw call,提高渲染性能
  • RawImage不能直接利用Sprite Atlas的优化
  1. 批处理
  • Image组件在渲染多个UI元素时,有更好的批处理能力
  • 使用RawImage可能会打破批处理,导致更多的draw call

功能完整性

  1. 其他UI组件的支持
  • Image组件与其他UI组件(如ButtonToggleSlider等)配合使用时,有更好的兼容性
  • RawImage组件在这些情况下可能需要更多的自定义处理

尽管RawImage可以显示Sprite2D类型的纹理,但在显示UI元素时,Image组件更为适合。
Image组件提供了更多的功能和优化,专门针对UI的需求进行了设计和优化。
因此,建议在需要显示UI元素时,优先选择Image组件。

22.

  1. 创建Text组件
    • Best Fit:勾选
    • Vertical Overflow:设置为Overflow
    • Horizontal Overflow:设置为Wrap
    • 配置其他文本属性,如FontFont SizeColor
  2. 添加Content Size Fitter组件
    • Horizontal FitVertical Fit都设置为Preferred Size
  3. 添加Layout Element组件
    • 按需要设置Preferred WidthPreferred Height
    • 勾选设置Flexible WidthFlexible Height
  4. 视情况调整TextRectTransform
  5. 新建脚本加入以下代码调试查看
using UnityEngine;
using UnityEngine.UI;

public class AutoAdjustingText : MonoBehaviour
{
    public Text uiText;

    void Start()
    {
        // 示例文本
        string text = "This is a sample text that will automatically wrap and adjust the height of the text component.";
        UpdateText(text);
    }

    void UpdateText(string newText)
    {
        uiText.text = newText;

        // 触发布局重新计算
        LayoutRebuilder.ForceRebuildLayoutImmediate(uiText.rectTransform);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

23.

  1. 使用协程
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class TypewriterEffect : MonoBehaviour
{
    public Text uiText;
    public string fullText;
    public float delay = 0.1f;

    void Start()
    {
        StartCoroutine(ShowText());
    }

    IEnumerator ShowText()
    {
        for (int i = 0; i <= fullText.Length; i++)
        {
            uiText.text = fullText.Substring(0, i);
            yield return new WaitForSeconds(delay);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  1. 使用Update方法和计时器
using UnityEngine;
using UnityEngine.UI;

public class TypewriterEffectUpdate : MonoBehaviour
{
    public Text uiText;
    public string fullText;
    public float delay = 0.1f;

    private float timer;
    private int currentIndex;

    void Update()
    {
        if (currentIndex < fullText.Length)
        {
            timer += Time.deltaTime;
            if (timer >= delay)
            {
                currentIndex++;
                uiText.text = fullText.Substring(0, currentIndex);
                timer = 0;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  1. 使用DoTween
    使用 DoTweenDOText 方法来逐字显示文本,并设置线性插值使其效果平滑
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class TypewriterEffectDoTween : MonoBehaviour
{
    public Text uiText;
    public string fullText;
    public float typingDuration = 3.0f;

    void Start()
    {
        // 确保文本为空,然后开始打字机效果
        uiText.text = "";
        // 使用 DoTween 的 DOText 方法来实现打字机效果
        uiText.DOText(fullText, typingDuration).SetEase(Ease.Linear);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

可以通过链式调用添加更多效果,例如每个字母显示时的缩放效果或者颜色渐变

uiText.DOText(fullText, typingDuration).SetEase(Ease.Linear).OnStepComplete(() =>
{
    uiText.transform.DOScale(1.2f, 0.1f).From();
});
  • 1
  • 2
  • 3
  • 4
  1. 为了减少字符串截取带来的垃圾回收问题,可以通过更优化的方法来实现打字机效果
    • 使用字符串数组代替字符串截取:预先创建一个字符数组,将完整的字符串复制到该数组中,然后逐步更新显示的字符,避免频繁创建新的字符串实例
    • 使用StringBuilderStringBuilder是处理字符串拼接的更高效的类,可以减少不必要的字符串创建和垃圾回收
    • 减少更新频率:如果打字机的速度非常快,可以适当减少每秒的更新次数
IEnumerator TypeText()
{
    StringBuilder stringBuilder = new StringBuilder();
    float timePerCharacter = time / content.Length;

    for (int i = 0; i < content.Length; i++)
    {
        stringBuilder.Append(content[i]);
        text.text = stringBuilder.ToString();
        yield return new WaitForSeconds(timePerCharacter);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

24.

通常会按照层级关系依次执行,但具体的执行顺序可能会因为具体的布局和其他组件的依赖关系而有所不同。
一般来说,Unity的组件更新顺序是这样的:

  1. **Transform Hierarchy:**首先会更新Transform层级结构,从根节点开始向下依次更新所有子节点的Transform
  2. **Layout Components:**在更新完Transform之后,Unity会更新所有与布局相关的组件,包括ContentSizeFitter

在这种情况下,子节点的ContentSizeFitter通常会先于父节点执行。这是因为父节点的布局更新会影响子节点,而子节点的ContentSizeFitter需要在父节点的布局之前调整好自己的大小,以便父节点能够正确地进行布局计算。
所以,实际情况是,子节点Text会根据输入内容自动设置宽高,父节点Image的宽高为0,原因是父节点Image缺少一个水平或垂直布局组,尽管ContentSizeFitter按照层级结构从上到下依次更新,但在某些情况下,父节点可能无法在同一帧内捕捉到子节点尺寸的变化,导致父节点未能及时调整大小。
解决方法就是在父节点上添加一个布局组件,如VerticalLayoutGroupHorizontalLayoutGroup

三、Lua

1. nil number function

2.

ipairs用于迭代数组部分的一个迭代器函数,它从索引1开始,按顺序遍历,直到遇到第一个nil为止,因此ipairs主要用于数组或列表

local t = {10, 20, nil, 30}

for i, v in ipairs(t) do
    print(i, v)
end
-- 10
-- 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

pairs是一个通用的迭代器函数,它用于遍历表的所有键值对,无论键是数字还是字符串,也无论键是否是连续的。它适用于散列表(哈希表)或具有非连续键的表。也可用于数组表,但是它不会按顺序遍历,输出顺序可能不固定

local t = {10, 20, 30}

for k, v in pairs(t) do
    print(k, v)
end
  • 1
  • 2
  • 3
  • 4
  • 5

由于ipairs只遍历数组或列表,所以当表里有混合数据时,和pairs相比,输出结果会有不同

local mixedTable = {10, 20, 30, a = 1, b = 2, c = 3}

for i, v in ipairs(mixedTable) do
    print(i, v)
end
-- 1   10
-- 2   20
-- 3   30

for k, v in pairs(mixedTable) do
    print(k, v)
end
-- 1   10
-- 2   20
-- 3   30
-- a   1
-- b   2
-- c   3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

ipais只遍历数组部分
pairs遍历表中的所有元素,包括数组部分,也包括键值对部分,但是不保证输出结果顺序。

四、其他

2.

点乘的几何意义是一个向量在另一个向量方向上的投影乘以另一个向量的模
结果

  • 小于 0 表示两向量方向相反
  • 大于0表示方向相同
  • 等于0表示正交(垂直)

叉乘的结果是一个向量,其方向与原来的两个向量所构成的平面垂直。这个方向遵循右手法则
方向

  • 将右手的食指指向第一个向量的方向
  • 将中指指向第二个向量的方向
  • 大拇指的方向就是叉乘结果的方向

结果
叉乘结果的大小等于原来两个向量所构成的平行四边形的面积

应用

  • 在计算曲面或多边形的法向量时,常用叉乘来找到垂直于表面的向量
  • 求面积
  • 求力矩:表示力作用点相对于旋转轴的旋转效果

3.

将顶点位置从对象空间变换到裁剪空间
对象空间:v.vertex表示顶点在对象的局部坐标系中的位置
模型-视图-投影变换:通过乘以UNITY_MATRIX_MVP矩阵,顶点被依次变换到世界空间、相机视角空间,最后变换到裁剪空间

持续更新中~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/954576
推荐阅读
相关标签
  

闽ICP备14008679号