c#中的功能优势

装箱和拆箱

性能消耗的直接体现

cs 复制代码
 int iterations = 10000000;  // 进行一千万次迭代
        Stopwatch stopwatch = new Stopwatch();

        // 非装箱测试
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            int x = i;  // 纯值类型操作,无装箱
        }
        stopwatch.Stop();
        Console.WriteLine("无装箱操作耗时: " + stopwatch.ElapsedMilliseconds + " ms");

        // 装箱测试
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            object x = i;  // 装箱操作
        }
        stopwatch.Stop();
        Console.WriteLine("装箱操作耗时: " + stopwatch.ElapsedMilliseconds + " ms");

        // 拆箱测试
        object obj = 42;
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            int x = (int)obj;  // 拆箱操作
        }
        stopwatch.Stop();
        Console.WriteLine("拆箱操作耗时: " + stopwatch.ElapsedMilliseconds + " ms");
cs 复制代码
无装箱操作耗时: 5 ms
装箱操作耗时: 120 ms
拆箱操作耗时: 85 ms

装箱拆箱的开销

  • 内存分配 :装箱时需要在堆上 为每个值类型分配新的内存 空间。这种堆内存的分配栈内存操作耗时更多。
  • 垃圾回收 :装箱后生成的对象会存放在堆上 ,当它们不再使用时需要进行垃圾回收处理 ,这增加了内存管理的负担
  • 类型检查 :拆箱时,必须检查对象的实际类型是否与拆箱目标类型匹配,这进一步增加了开销。

概念介绍

装箱和拆箱只适用于 值类型引用类型 之间的转换。

和父子类的类型转换没关系,如果从object转为class类对象,不涉及装箱拆箱

cs 复制代码
int num = 42;
object obj = num;  // 装箱:值类型转换为引用类型
int unboxed = (int)obj;  // 拆箱:从引用类型转换回值类型

把值类型变为引用类型才是装箱,

把引用类型返回为原始的值类型,才是拆箱

引用类型更加通用,更像c#中数据处理 的箱子,值类型更加本质,所以拆箱就是还原初始

无论是将子类转换为父类(向上转换 ),还是将父类转换为子类(向下转换),都不会涉及值类型与引用类型之间的转换。

向上转换(子类转父类)

  • 自动发生,因为子类对象包含父类的部分,因此可以安全地将子类赋值给父类类型的变量。

向下转换(父类转子类)

  • ++需要显式的类型转换++,因为父类可能并不是子类类型。在运行时,如果父类引用的对象实际是子类类型,则转换是安全的;否则会抛出异常。

向上转换(子类转换为父类)基本没有性能开销,因为这是一个隐式的转换,编译器知道子类对象包含父类的部分,直接处理引用的指向,没有额外操作。

向下转换(父类转换为子类)则有一个小的性能开销,因为运行时需要检查实际的对象类型,确保它是正确的子类类型。

这类类型转换通常性能开销很小,主要是类型检查的开销,而不会涉及内存分配或GC。所以,从性能角度看,父子类之间的引用类型转换非常轻量,与装箱/拆箱的性能开销不可同日而语。

虽然父子类转换有少量性能消耗,但与装箱和拆箱相比几乎可以忽略。

装箱类比: 想象你要把一颗小珠子(值类型)放进一个小盒子(引用类型)里,然后将盒子交给某人(即赋值给一个 object 类型)。尽管珠子很小,直接交给别人也很容易,但你仍然要:

  • 先找到一个盒子(分配内存)。
  • 把珠子放进去(值类型包装为引用类型)。
  • 然后把盒子递给对方。

这一过程就像装箱操作,尽管珠子很小,但找到盒子、放进去并传递的操作仍然需要一点时间和精力。

拆箱类比: 想象你现在接到一个装着珠子的盒子,你需要:

  • 打开盒子(解引用)。
  • 检查盒子里的内容是否是珠子(类型检查)。
  • 拿出珠子(转换为值类型)。

如果盒子里装的东西不是你期望的珠子,那么操作就会失败,这就像拆箱时如果类型不匹配会抛出异常。这个过程同样需要时间,尽管你只是想取出一个简单的值。

  • 装箱/拆箱 可以类比为在处理每一个非常小的任务时,却要进行一些额外的准备工作,比如包装、检查、再拆包,虽然每次操作的耗时都很短,但如果频繁执行,累积起来就显得冗长。例如:

    • 你给同一个朋友发很多小文件,但每次你都要压缩成 ZIP 文件再发送,而对方解压再查看。
    • 每次邮寄一个信封都需要先找到信封、写地址、贴邮票、送到邮局,再由邮局送到对方那里,虽然每次操作单独看起来不多,但每天寄几十封信可能会消耗大量时间。
  • 值类型直接使用 则可以类比为:直接把珠子或者信递给对方,没有任何额外的操作。这显然是更高效的。

实际消耗的衡量

  • 单次 装箱拆箱 的操作成本大致是普通操作的 几十倍上百倍。尽管单个操作的成本在微秒级别(例如 10^-6 秒),但在需要进行高频率的操作中,这种消耗会迅速累积。
  • 高频场景 下,例如大规模循环中频繁进行装箱/拆箱操作,可能导致程序的整体性能下降显著,甚至引发内存垃圾回收(GC)的频繁运行,进一步加重系统负担。

编程中的实际影响

如果在程序中某个关键的循环或计算过程中,每次都进行装箱/拆箱操作,就好像你频繁为每个小任务准备过多的手续,这不仅增加了不必要的开销,还可能影响程序的响应速度和性能。

虽然在单次操作中装箱和拆箱的性能损耗不大,但在高频率、大规模操作下,如在循环中频繁装箱/拆箱时,性能损失会变得很显著。因此,对于性能要求较高的应用(如游戏开发、大数据处理等),避免不必要的装箱拆箱操作非常重要。

总结:装箱/拆箱的开销相当于为简单任务额外包装和解包装,单次看起来不多,但频繁操作会迅速累积影响性能

Object和object

小写的 object 指的是 C# 的 所有类型的基类 ,即所有类型(包括值类型和引用类型)的最基类。任何类型(值类型、引用类型、自定义类、内置类型如 intstring)都继承自 object

where T : object

约束的意思T 可以是任何类型,包括值类型(如 int)、引用类型(如 stringList<T> 等),没有具体限制。

实际上,where T : object 是默认的泛型约束,即使不写 这个约束,C# 中泛型也默认是约束为 object

UnityEngine.Object 是 Unity 引擎中特有的一个基类,用于所有 Unity 对象(如 GameObjectTextureAudioClip 等)。它和 C# 的 object 基类不同。

where T : Object

约束的意思T 必须是 UnityEngine.Object 或其派生类,例如 GameObjectMonoBehaviourTexture 等 Unity 引擎的对象。

为什么不要继承object的?

因为UnityEngine.Object是基于unity开发管理创造的最基类,会附加很多最底层的属性和功能,让敲代码更方便,比如

cs 复制代码
public class MyUnityClass<T> where T : UnityEngine.Object
{
    public void LoadResource(T resource)
    {
        Debug.Log(resource.name);  // 只有继承自 UnityEngine.Object 的对象有 name 属性
    }
}

GameObject

可见的、可交互的对象都是 GameObject 的实例

lightCamera 也是 GameObject 的子类,作为场景中的特殊对象使用。它们实际上是 GameObject 的一个具体化,代表了灯光和摄像机。

Component

  • Component 是附加在 GameObject 上的类,它提供了对象的功能和特性。例如,TransformRendererColliderScript 等都是 Component 类型。
  • Component 本身不能存在于场景中,必须附加到 GameObject 上。

Resources异步加载

Resources.LoadAsync 实际上返回的是 ResourceRequest 对象,而不是 AsyncOperation。虽然 ++ResourceRequest 继承自 AsyncOperation++ ,它在某些方面++对异步资源加载做了更具体的处理++。

AsyncOperation :是 Unity 提供的一个基础类,用于表示异步操作的状态。它有属性如 isDone(表示操作是否完成),以及其他用于管理异步操作的成员(progress)。

ResourceRequest :是 AsyncOperation 的子类,**++专门用于处理资源加载++**操作。

ResourceRequest 在继承的基础上额外具有 asset 属性,这个属性为加载完成后获取的**++实际资源++**。

泛型约束复习

ps:泛型可以声明但不使用,除了没有意义其他都没什么

where T : struct

  • 描述 :限制泛型参数 T 必须是一个值类型(非引用类型)。

  • 确保泛型类型参数是值类型,如 intfloatbool 等。

2. where T : class

  • 描述 :限制泛型参数 T 必须是一个引用类型。

  • 确保泛型类型参数是引用类型,如 stringobject、自定义类等。

3. where T : new()

  • 描述 :限制泛型参数 T 必须有无参数的公共构造函数。

  • 可能需要在泛型作用域里,创造T的实例对象,允许在泛型类或方法中创建 T 的实例。

  • (那如果是带参的呐?)不常用,而且不好管控,带参数都不晓得你是想干嘛

4. where T : BaseClass

  • 描述 :限制泛型参数 T 必须是该类派生类或本身。

  • 这允许泛型类型参数具有基类的所有成员。

5. where T : IInterface

  • 描述 :限制泛型参数 T 必须实现指定的接口 IInterface

  • 这个约束保证了泛型类型参数具有接口 IInterface 中定义的成员。

6. 多个约束

  • 描述:可以同时应用多个约束,组合使用这些约束可以更精确地限制泛型参数的类型。

cs 复制代码
public class MyCombinedClass<T> 
    where T : class, new() // T 必须是引用类型并且具有无参数构造函数
{
    public T CreateInstance()
    {
        return new T(); // 使用无参数构造函数创建实例
    }
}

类对象的本源记录

创建类对象后,这个类对象真正是什么类型就已经被记录了,无论后面怎么拿父类装,编译的时候也能知道真正原来的类型

即使你将对象赋值给其父类类型的变量。++编译器++ 和++运行时系统能够利用这些信息(已写的代码创造新的类对象时)++ 进行类型检查。

创建一个对象时,C# 的运行时系统会记录对象的实际类型,这些信息是由运行时++类型系统++管理的,并在**++对象的生命周期内保持不变++**

编译时的类型检查

  • 编译器的类型推断 :在编译时,编译器可以根据变量声明和赋值操作++推断出对象的类型信息++ 。例如,编译器知道 Dog 继承自 Animal,因此它可以处理 Dog 类型的对象赋值给 Animal 类型的变量。

  • 生成类型检查代码 :编译器会生成代码来执行运行时的类型检查。使用 is 关键字时,编译器生成的代码会根据对象的实际类型信息来++判断++对象是否是某个指定类型的实例。

既然系统和编译器知道类型,那还要反射干嘛

动态检查和操作类型信息的机制,在运行时动态获取和操作类型信息

使用反射来获取类型的详细信息,编译时未知或无法预知的情况下尤其有用。

cs 复制代码
using System;
using System.Reflection;

public class Program
{
    public static void Main()
    {
        Type type = typeof(Dog);
        Console.WriteLine($"Type Name: {type.Name}");
        
        // 获取所有属性
        PropertyInfo[] properties = type.GetProperties();
        foreach (var property in properties)
        {
            Console.WriteLine($"Property: {property.Name}");
        }
    }
}
cs 复制代码
public class Example
{
    public void SayHello(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
}

public class Program
{
    public static void Main()
    {
        Example example = new Example();
        MethodInfo method = typeof(Example).GetMethod("SayHello");
        method.Invoke(example, new object[] { "World" });
    }
}
cs 复制代码
public class Example
{
    public void Display()
    {
        Console.WriteLine("Display method called.");
    }
}

public class Program
{
    public static void Main()
    {
        Type type = typeof(Example);
        object obj = Activator.CreateInstance(type);
        MethodInfo method = type.GetMethod("Display");
        method.Invoke(obj, null);
    }
}

反射在序列化和反序列化操作中也非常重要,它允许程序在运行时了解对象的结构,并将对象转换为其他格式(如 JSON、XML)或从这些格式转换回来。

依赖注入和框架支持

  • 框架和库:许多依赖注入框架和库(如 ASP.NET Core、Unity、Ninject)使用反射来动态解析和注入依赖关系。这种方式允许框架在运行时处理复杂的依赖关系和对象生命周期。

委托的父子关系和类的继承是两套规则

委托加上泛型之后,也得到了一个类似的父子类的规则,但委托的规则不同于一般的类之间的继承,而是默认下各个委托类型互不相干,但在加上in后子委托可以装父委托,为逆变,或者加上out后父委托可以装子委托,为协变

协变和逆变

为了保障数据不在委托传递中遗失,又想给委托增加"继承的感觉",所以在委托的这套互不干涉的规则中,诞生了in和out,从而限制这种"继承"的安全

委托的"继承感",出于他:参数类-->进委托-->给返回值类,的三段式流程,所以,如果要保障 在子类数据多于父类的情况下,不会出现数据丢失,就只能在两头进行限制,

参数类如果硬要扩展出"继承",就只允许传入数据更少的父类,这种数据安全的考虑下,父类的数据更少,子类容纳的范围更大,数据范围大的很自然的就可以容下数据范围小的,子类反而"似乎"可以装父类了,所以叫逆变(用in 关键字唤起逆变的继承规则)

而在返回值类,如果硬要扩展出"继承",利用现有的规则会很方便,父类可以装子类的规则发挥作用,从而让返回类型可以装子类,所以叫协变(用out关键字唤起协变的继承规则)

out 只能有一个,in 可以有多个

out 只能用于返回值 ,而**++一个委托或者方法只能有一个返回类型++** ,所以通常只能对一个泛型参数使用 out。这意味着,如果委托有多个泛型参数,只有返回值的泛型参数可以标记为 out

in 可以用于多个输入参数 ,因为一个委托可以有多个输入参数,所以多个泛型参数都可以标记为 in,以表示它们可以接受父类类型的输入。

概括总结的说,c#的对函数传入参数的管理本就是为了严谨而保障类型的一对一,不允许类型替代,而in是对这种严谨规则的一种扩展,参数类型的一对一才是最常态,所以in和out会是"变",函数委托的拓展的变

cs 复制代码
public delegate TResult MyDelegate<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
--------------------------------------------------------------------------------

public delegate void MyDelegate<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

public static void Main()
{
    MyDelegate<int, string, object> del = (x, y) => { /* do something */ };
    // 由于返回类型是 void,所以 out TResult 没有实际作用
    //虽然没用上但是可以这样写了out不用
}

协变 (out) 使用泛型的例子:

cs 复制代码
public class Animal { }
public class Dog : Animal { }

public delegate T AnimalFactory<out T>();  // 协变泛型参数

public class Example
{
    public void Test()
    {
        AnimalFactory<Dog> dogFactory = () => new Dog();   // 返回 Dog 类型
        AnimalFactory<Animal> animalFactory = dogFactory;  // 协变允许子类委托赋值给父类委托

        Animal animal = animalFactory();  // animalFactory 实际上返回了 Dog 对象
    }
}

逆变 (in) 使用泛型的例子:

cs 复制代码
public class Animal { }
public class Dog : Animal { }

public delegate void AnimalHandler<in T>(T animal);  // 逆变泛型参数

public class Example
{
    public void Test()
    {
        AnimalHandler<Animal> animalHandler = (animal) => Console.WriteLine("Handling animal");
        AnimalHandler<Dog> dogHandler = animalHandler;  // 逆变允许父类委托赋值给子类委托

        dogHandler(new Dog());  // dogHandler 实际上可以处理 Dog 对象
    }
}

修饰函数参数三剑客refoutin

其中ref只能修饰函数参数,而in和out还有别的任务

掌管逆变和协变

为什么 ref 不能有逆变或协变?

ref 参数既允许读又允许写,因此在类型转换中它不能安全地支持逆变或协变。++协变只允许输出,而逆变只允许输入++ ,而 ref 同时有输入输出的需求,这会带来潜在的类型不安全问题。

in的相关

与ref一样,都是引用参数原型,in只是得到原型,不允许修改,而ref则允许任意修改和读写,

也因此ref要求传过来的参数一定要有实实在在的原型

out在这方面要求一定要在传入该参中修改,而不管原型有无,因为重点在于要输出内容给要引用的原型

in 在泛型中的使用:逆变

传递的参数是 只读的

ref 允许方法内部修改原始对象,in 则是只读引用,允许读取但不允许修改。

值类型与引用类型都可以使用 in

对于值类型(尤其是大型结构体),in 关键字可以避免拷贝带来的性能损耗

in 在方法参数中的使用:按引用传递的参数是 **只读的,**方法内部不能修改该值

ref :按引用传递参数,方法内部可以++读写该值++。

out :按引用传递参数,方法++内部必须对该值进行赋值++,相当于用于输出结果。

in 的限制和潜在问题

in 关键字只能用于泛型类型的输入参数,而不能用于返回值。

逆变只适合用于 输入场景

可以安全地传入一个更具体的类型(如 Dog)给处理更泛化类型的代码(如 Action<Animal>

当涉及 输出场景(即返回值)时,逆变会导致类型不安全。

C# 自带了谓词功能

Predicate<T> 委托

public delegate bool Predicate<inT>(T obj);

加了in就是逆变了

in的作用,把泛型的父子关系延伸到委托上,这样委托之间也可以互相装

加上 in 后,泛型委托的父类和子类之间可以互相转换,从而提高了类型兼容性

定义在 System 命名空间

内置的委托

接受单个参数并返回一个布尔值的方法

CustomYieldInstruction

是Unity 提供的一个基类

继承它并实现自己的逻辑来定义自定义的等待条件

重写 keepWaiting 属性

keepWaiting 决定了协程是否应该继续等待

属性keepwaiting返回 true,协程就会继续等待,直到返回 false 时协程才会继续执行

yield return null

return 数字也一样

yield return 的作用是让++协程在执行到该语句时暂停++ ,并++将控制权交还给 Unity 的主线程++

使当前帧不再执行,变为下一帧继续执行

cs 复制代码
IEnumerator EndlessCoroutine()
{
    while (true)
    {
        yield return null;  // 每帧暂停一次,永远循环
    }
}

IEnumerator EndlessCoroutine()
{

        yield return null; //只是等待到下一帧,不搭配while,就是等到下一帧就结束
}

只有 yield return null,那么这个协程会在每一帧都暂停,并在下一帧继续执行,从而进入一个无限循环。除非手动停止这个协程,否则它将会永远执行下去

协程如何结束?

协程的结束取决于 IEnumerator 对象的++迭代是否完成++ 。当 ++MoveNext() 返回 false 时++,协程就会停止执行。因此,协程结束的条件是:

  • ++迭代器不再有任何待执行的代码(即 MoveNext() 返回 false)++。

状态机是什么?

状态机(State Machine)是一个数学模型,可以描述某个对象的状态,以及从一种状态转变为另一种状态的规则。在编程中,状态机用来管理复杂的流程和逻辑,在不同的状态下执行不同的行为。状态机包含:

  • 状态:当前的状况,可能是"正在等待","正在加载"等。
  • 转换:从一个状态变到另一个状态的条件(例如,异步操作完成时,状态从"加载中"转换为"加载完成")。

在 Unity 协程的上下文中,可以理解为每次 yield return 就是状态之间的转换过程。当 Unity 检测到满足特定条件时(比如下一帧开始),协程从"暂停"状态转换为"继续执行"状态。

IEnumerator 是怎么做到逐帧执行代码的?

协程系统碰到 yield return 时,它会暂停协程的执行

然后等待到合适的时机(比如下一帧或某个条件完成),再调用 MoveNext() 恢复协程的执行

IEnumerator 是一个接口,它主要用来定义一个迭代器。迭代器通过++MoveNext() 来逐步执行代码块。++ 在 Unity 的协程中,IEnumerator 不仅仅用作迭代器,它更像是一个能够++暂停和恢复的流程控制工具++。

Unity 协程系统

Unity 的协程系统使用的是一种状态机模式

系统实际上是基于 yield return 的模式驱动的,

这个系统可以识别和处理多种特定类型的返回值

yield return 一个 AsyncOperation 对象时

---->等待这个异步操作完成

不需要 AsyncOperation 继承

协程系统会自动处理这些返回值类型的特殊逻辑

Unity 协程系统的一个特性,它允许 yield return 等待某些特殊的对象类型,包括 AsyncOperation

yield return 并不要求返回的对象一定要是 IEnumerator

Unity 的协程系统识别和处理多种类型的返回值

null:暂停协程,直到下一帧。

WaitForSeconds:++暂停协程(协程系统的停止)++指定的秒数。

AsyncOperation:暂停协程,直到异步操作完成

as 关键字

as 关键字用于尝试将对象转换为指定类型。如果转换成功,返回目标类型的对象;如果转换失败,返回 null

抽象类

机制确保了所有的抽象方法都有具体实现

如果一个子类继承了抽象类但没有实现抽象类中的所有抽象方法,那么这个子类也必须被声明为抽象类

抽象类允许你定义一组共享的接口和基本行为

cs 复制代码
public abstract class Shape

public abstract void Draw(); // 抽象方法,没有实现
    
public void Move() // 具体方法,可以在子类中使用
    {
        Console.WriteLine("Shape moved");
    }

接口(Interface)

多态,,多种形态可以统一,因为接口可以继承,所以有多态性,多态就是多种形态,可以容纳多种形态,接口可以用自身一个类容纳各种不同的形态,所以叫多态

而这种多态搭配上继承本身,发挥出了极大的价值,或者说可能这两者就是一体的,

需要使用的相同的内容,通过接口实现规定,而继承了接口这件事本身,由于接口的多态性,让接口在外部可以统一管理整个接口频道,

所有继承了这个接口的类,都一定会实现里面的方法,通通可以使用接口来管理这一切的一切,而且每个实现了接口的类,建立的连接只存在于与接口相联系,

总的来说是,接口的合同规定性和继承性,使得它发挥出了减耦的作用,

减少思考量,对程序员很重要,同时考虑太多东西会出错的,而出错同样需要时间成本,写码方面,电脑有cpu,我们同样有cpu,而耦合就是消耗性能的原因之一,电脑cpu倒可以买更好的,我们的cpu自己烧了,电脑cpu就算是i99,没你的清晰思路也要崩

AsyncOperation

主要返回的Load观测

1. 场景加载和卸载

  • SceneManager.LoadSceneAsync : 异步加载场景,并返回 AsyncOperation 对象。

    cs 复制代码
    AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("SceneName");
  • SceneManager.UnloadSceneAsync : 异步卸载场景,也会返回 AsyncOperation 对象。

    cs 复制代码
    AsyncOperation asyncOperation = SceneManager.UnloadSceneAsync("SceneName");

2. 资源加载

  • Resources.LoadAsync : 异步加载资源并返回 ResourceRequest 对象,ResourceRequestAsyncOperation 的子类。

    cs 复制代码
    ResourceRequest resourceRequest = Resources.LoadAsync("ResourcePath");

3. 场景异步操作

  • SceneManager.LoadSceneAsync : 这个方法不仅加载场景,还可以++指定是否将新场景作为附加场景加载++ (即将场景添加到当前场景中),也会返回一个 AsyncOperation 对象。

    cs 复制代码
    AsyncOperation asyncOperation = SceneManager.LoadSceneAsync("SceneName", LoadSceneMode.Additive);

4. 其他异步操作

  • Application.RequestUserAuthorization : 请求用户授权访问设备特定功能(如位置服务),并返回 AsyncOperation 对象。

    cs 复制代码
    AsyncOperation asyncOperation = Application.RequestUserAuthorization(UserAuthorization.WebCam | UserAuthorization.Microphone);

++AsyncOperation++ asyncLoad=++SceneManager++ .++LoadSceneAsync++(sceneName);

异步操作和游戏主循环的帧是共享的,并不独立

异步操作利用游戏帧的空闲时间处理

可以给异步任务添加观察

并没有继承自任何其他类

AsyncOperation 作为一个独立的类,但它没有继承自其他功能类。

AsyncOperation 是一个管理异步任务的类,它提供了基本 isDoneprogress

派生类(如 AssetBundleRequestResourceRequest),它能处理更为具体的资源加载操作。

cs 复制代码
private bool isAreaLoaded = false;
public IEnumerator LoadArea(string sceneName)


  AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); // 异步加载场景

if (!isAreaLoaded)
 while (!asyncLoad.isDone)
            {
                yield return null; // 等待场景加载完毕
            }
           
isAreaLoaded = true;

yield return null;

  • 这个语句意味着:在异步加载过程中,程序会暂停(在yield return null这一条语句的地方暂停住,等待下一帧的来临后,在一次往下进行,在这即又回到了while去判断)并等待下一帧继续执行

异步操作利用游戏帧的空闲时间进行处理,因此主线程不会被大任务(比如加载大场景)阻塞。

异步加载时执行的步骤

在异步加载的过程中,Unity 会逐步执行上述操作,以下是可能的分阶段加载过程:

  1. 资源加载:场景中的模型、纹理、声音、动画资源逐步加载到内存中。
  2. 对象实例化:加载进度到一定程度后,场景中的物体会逐步生成,准备就绪。
  3. 脚本初始化:加载完资源后,脚本会根据场景需求逐步初始化,运行逻辑。
  4. 剩余资源加载:场景中的剩余资源(例如小部分纹理或声音)继续加载,而玩家可以在游戏中活动。

Unity 并不会自动帮你处理视野范围内的资源加载和卸载

处理这种可见范围内加载资源的需求

场景分块、Occlusion Culling、以及手动加载和卸载场景

场景分块 (Scene Loading in Chunks)

可以将整个游戏世界分成多个小场景,称为"子场景"或"区块"

SceneManager.LoadSceneAsync()用来动态加载子场景的工具

玩家靠近某一区域时,异步加载该区域的场景。

视锥体剔除 (Frustum Culling)

Unity 自动,相机视野外的物体将不会被渲染

减少渲染负担

资源的加载和内存使用依然存在

遮挡剔除 (Occlusion Culling)

视野被遮挡时可以被跳过渲染

手动资源管理

更复杂的开放世界场景,通常需要手动管理

例如,Resources.LoadAsync 动态加载特定模型、贴图、声音等,并在玩家远离时手动卸载这些资源

:::

据玩家的位置

分区域管理:将大场景分割为多个小场景,玩家接近某一区域时加载

触发器:入某个范围时触发异步加载

LOD (Level of Detail) 系统:远显低分辨率,近则高质。

cs 复制代码
 if (
Vector3.Distance(player.position, targetArea.position)
 < 50f)
{
if (!isAreaLoaded) 
{
StartCoroutine(LoadArea("AreaName"));
}

}


//同理远离场景
if (
Vector3.Distance(player.position, targetArea.position)
 > 100f) 
{        
if (isAreaLoaded)             
StartCoroutine(UnloadArea("AreaName"));
}

协程

StartCoroutine 的执行逻辑

  • StartCoroutine 本身是立即返回的 :当你调用 StartCoroutine 时,它**++不会阻塞调用它的函数。它会立即返回,并且接下来函数中的代码会继续执行。++**

  • 协程的调度StartCoroutine 的作用是注册一个协程,该协程将在后续的帧中异步执行,并不会阻止当前帧内的其他操作。

  • 协程在主线程中运行 :协程不是一个独立的线程,它仍然在 Unity 的主线程上执行。通过 yield return 语句,协程可以暂停执行并在下一帧或更远的帧中恢复,执行异步操作的效果。

使用协程与普通方法进行延时销毁的最大区别主要体现在++代码的可读性、执行方式以及性能上++。让我们从两种方法进行比较,看看它们各自的特点。

C# 是同步执行的,除非通过计时器或某种方式手动追踪时间。

Update 方法来手动跟踪时间,或者使用 Invoke 来设置延时

cs 复制代码
void Start()
{
    Invoke("DestroyObject", 5f); // 5秒后调用DestroyObject
}

void DestroyObject()
{
    Destroy(gameObject);
}
cs 复制代码
float timer = 0f;
bool shouldDestroy = false;

void Update()
{
    if (shouldDestroy)//设置某些条件更改状态
    {
        timer += Time.deltaTime;  // 追踪经过的时间
        if (timer >= 5f)
        {
            Destroy(gameObject);  // 5秒后销毁
        }
    }
}

避免复杂的时间追踪或手动管理状态

cs 复制代码
//定义协程
IEnumerator DestroyAfterTime(float waitTime)
{
    yield return new WaitForSeconds(waitTime);  // 暂停协程5秒
    Destroy(gameObject);
}
//使用协程
StartCoroutine(DestroyAfterTime(5f)); // 开启协程,5秒后销毁

对于多任务处理

普通方法还要额外的逻辑去调整时间之间的关系,需要额外逻辑来处理多个时间相关任务。

协程可以处理多个并行任务,不需要额外逻辑来管理多个任务状态。

灵活性

普通计时任务并不灵活,设定之后想改会动到很多其他的逻辑,如果要做的任务不是很大还好,太大很容易不敢动逻辑,通常用于简单任务。

协程效率就高,可以轻松暂停、恢复、停止任务,适合异步任务和复杂操作。即使不要了也不会联系过多其他代码逻辑

扩展性

普通方法,难以扩展到更复杂的异步操作或组合任务。

协程天然适合处理异步操作,支持等待多种条件的完成。

协程的问题

协程在运行时依赖于 MonoBehaviour

启动协程的对象被销毁或禁用,协程也会自动停止,且不会触发任何警告

管理对象的生命周期不当,可能会导致协程意外终止,影响程序的逻辑。

轻量、异步的任务

延时执行、异步加载资源

相关推荐
一点媛艺1 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生2 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程3 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk4 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*4 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue4 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man4 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang