52、基于Unity 3D的C编程:高级特性与技巧

基于Unity 3D的C#编程:高级特性与技巧

在使用Unity 3D进行C#编程时,有许多高级特性和技巧可以帮助我们更好地开发游戏。下面将详细介绍其中的一些关键内容。

1. 反射对象与库使用优化

通过 UpdateReflection 类将更新事件发送到 Mirror 对象,我们可以得到一个反映原始对象位置和旋转的对象。看似简单的 gameObject.Mirror() 函数调用,背后其实执行了很多步骤。

在Unity 3D的小型C#类中,限制库的使用量是一个有趣的技巧。当添加 using System; 等指令时,会引入大量需要访问的内存,这些内存虽然在游戏运行时不一定会被使用,但在代码编译时会被占用。为了方便使用,可以采用如下方式:

using Vect3 = UnityEngine.Vector3;

这样可以将 Vector3 缩写为 Vect3 ,有助于减少命名空间冲突,还能加快MonoDevelop中自动完成功能的名称选择速度。通常情况下,为了以防需要使用库的其他部分,还是使用常规名称,但了解这样的技巧在遇到特殊情况时会很有用。

Unity 3D开发者致力于支持多种不同平台,这也决定了代码的适用性。Unity 3D是学习C#的好平台,在掌握了语言的基本原理后,可以探索使用完整.NET框架的其他开发环境。

2. 析构函数

在创建对象的同时,Unity 3D会负责清理这些对象。如果我们能快速清理对象,就能减少Unity 3D进行清理的频率和时间。

析构函数与构造函数相反。C#是一种具有垃圾回收机制的语言,在大多数情况下,当对象不再被引用时,不需要手动清理数据。但在使用不安全代码时,可能需要进行一些清理工作。

2.1 基本示例

Destructors 项目中,打开附加到主摄像机的 Example.cs 组件和 DestroyMe.cs 文件。 DestroyMe 类同时包含构造函数和析构函数。析构函数可以通过类标识符前的波浪号 ~ 来识别。当对象被垃圾回收时,析构函数会被调用。

Destructors_Scene 场景中,附加到 Destructors 游戏对象的 Destructors.cs 文件中,我们创建了 SelfDestructor 类的实例。创建新的 SelfDestructor 时,会给它一个名称,以便知道何时清理了什么对象。运行游戏场景时,游戏开始会在控制台打印“Bob - omb says hello.”,几秒后会打印“Bob - omb says goodbye.”。

在代码中分配和销毁 SelfDestructor Bomber 时,日志会立即显示“hello”和“goodbye”。由于 Bomber 继承自 Object ,可以将其存储为 UnityEngine.Object 类型的变量。创建类级变量时,有时会在编辑器运行时创建这些变量,这可以通过将 Bomber 变量更改为 SelfDestructor 类型来观察到,会出现神秘的“says goodbye.”日志。

一般来说,Unity会让.NET在后台自动运行垃圾回收(GC)周期。C#中的GC通常是自动进行的,对于结构体等对象,GC会很快完成。只有在处理大量类对象时,才需要密切关注创建的对象数量。自动GC会在随机间隔进行,在PC上通常对性能影响不大,但在移动设备上,GC可能会导致帧率突然下降。为了避免这种情况,建议创建对象池,以减少频繁创建和销毁对象的问题。

2.2 清除事件或委托

析构函数的一个重要用途是清除事件或委托。通过一个简单的委托和事件调度器,我们可以添加 Debug.Log 函数来记录调度器类的创建和销毁。

创建一个接受调度器作为构造函数参数的类,在构造函数中分配调度器的事件,在析构函数中使用 -= 运算符移除对事件调度器的引用。运行代码时,会看到调度器创建、清理器连接到调度器、倒计时递减,最后清理器和调度器依次销毁的日志。

2.3 OnDestroy方法

继承自 MonoBehaviour 的类使用 OnDestroy() 方法来清理自身内存,而不是使用析构函数的形式。当 MonoBehaviour 对象被销毁时, OnDestroy() 方法会被调用,并在控制台打印相应信息。

创建一个立方体原始对象,并将 MonoBehaviour 类附加到该立方体游戏对象上。创建事件调度器,将 MonoBehaviour 类的事件监听器分配给调度器的事件。调用调度器的事件,触发 UsesOnDestroy 类的自我销毁调用 Destroy(this) ,最终场景中的立方体没有附加任何脚本,控制台会打印“OnDestroy Called.”。

2.4 垃圾回收的真相

垃圾回收的实际工作方式有些复杂。理论上,当 CountDown 小于0时,对象应该销毁,但实际上 UsesOnDestroy 类在应该销毁后还会收到96次销毁调用。断开监听器与调度器的连接可以停止额外的调用,但脚本不会立即销毁,直到函数执行完毕才会调用 OnDestroy 方法。

需要注意的是,垃圾回收是“懒惰”的,对象在销毁后通常还会在内存中驻留几秒。在创建大量对象后,很难确定内存何时会被释放,而且释放时通常是一次性的,可能会导致帧率下降。

2.5 析构函数总结

析构函数用于需要手动清理类的情况。对于基于Unity 3D类对象的类,不建议使用 OnDestroy() 方法清理额外数据;对于非基于Unity 3D类的类,可以使用析构函数,但创建非基于Unity 3D的类并不常见。

当类向事件添加委托时,析构函数可以用于移除委托,但不能使用 -= 运算符完全移除对事件监听器的引用,这会在内存中留下一些痕迹,影响垃圾回收。垃圾回收会定期进行,通常每次遍历堆的间隔约为一秒。堆中的内存可能会变得混乱,垃圾回收会尽量清理堆,但过于频繁的处理会影响CPU性能。因此,在性能和内存管理之间需要找到平衡,使用结构体而不是类可以减少垃圾回收的需求,但如果结构体作为类的一部分存储,则仍然需要进行清理。

3. 并发与协程

之前讨论的代码都是按顺序执行的,每条语句都要等待前一条语句完成任务。而协程可以打破这种顺序执行的模式。

3.1 Yield关键字

IEnumerator 接口在Unity 3D中有重要用途,它不仅可以用于遍历数组,还支持 yield 关键字。 yield 关键字的主要作用是允许计算机在执行函数时暂停,让 Update() 循环等后续操作可以回到该函数继续执行。

例如,一个填充场景的缓慢函数可能需要很长时间才能完成,使用 yield 可以避免Unity 3D在函数执行时锁定。具体操作如下:

// 普通函数
void FillUpObjects()
{
    // 填充40000个随机放置的立方体
    for (int i = 0; i < 40000; i++)
    {
        // 创建立方体的代码
    }
}

// 使用协程的函数
IEnumerator FillWithYield()
{
    for (int i = 0; i < 40000; i++)
    {
        // 创建立方体的代码
        yield return null;
    }
}

在调用时,使用 StartCoroutine() 来启动协程:

StartCoroutine(FillWithYield());

这样在游戏开始时不会出现锁定,我们可以看到立方体逐渐填充场景。但使用协程创建对象的速度会变慢,不过 Update() 循环会继续运行,例如在协程启动后, Debug.Log(count++); 会继续打印数字。

3.2 协程的基本示例

在Unity 3D场景中,创建一个附加到主摄像机的新脚本 Concurrent ,使用 StartCoroutine(DelayAStatement()); 启动一个 IEnumerator 函数。 DelayAStatement 函数中,使用 yield return new WaitForSeconds(); 创建一个并发任务,暂停函数执行,等待指定时间后继续执行。

IEnumerator DelayAStatement()
{
    Debug.Log("Started at: " + Time.fixedTime);
    yield return new WaitForSeconds(2f);
    Debug.Log("Ended at: " + Time.fixedTime);
}

通过这种方式,可以同时启动多个并发协程,这些协程会按照顺序开始和结束,适用于处理事件和时间相关的任务。

3.3 设置定时器

作为游戏开发工程师,编写一个灵活且可复用的计时函数很重要。一个好的计时函数可以用于游戏中的爆炸计时、怪物波次间隔或屏幕上的倒计时等。

使用 UseTimedAction() 函数,并将 TheAction() 函数作为变量分配给 TimedAction() 函数。 TheAction() 函数可以执行各种操作,如打印信息、触发爆炸等。

void UseTimedAction()
{
    StartCoroutine(TimedAction(5f, TheAction));
}

IEnumerator TimedAction(float delay, Action action)
{
    yield return new WaitForSeconds(delay);
    action();
}

void TheAction()
{
    Debug.Log("Doing the thing!");
}

还可以通过在协程中添加循环来创建重复事件的定时器。使用 Mathf.Lerp 等函数进行线性插值,实现值的平滑变化。

IEnumerator GetUpdateValue()
{
    float startValue = 6f;
    float endValue = 9f;
    float duration = 3f;
    float elapsedTime = 0f;

    while (elapsedTime < duration)
    {
        float currentValue = Mathf.Lerp(startValue, endValue, elapsedTime / duration);
        Debug.Log(currentValue);
        elapsedTime += Time.deltaTime;
        yield return null;
    }
}

使用 StartCoroutine() 启动这个协程:

StartCoroutine(GetUpdateValue());

另外,还可以创建一个重复定时器:

IEnumerator RepeatTimer(float delay)
{
    while (true)
    {
        Debug.Log("Starting timer");
        yield return new WaitForSeconds(delay);
    }
}

通过这种方式,可以灵活控制定时器的启动和停止,并且一个协程可以控制另一个协程,例如设置一个布尔变量作为开关来控制协程的执行。

综上所述,在Unity 3D的C#编程中,合理运用反射对象、析构函数和协程等高级特性和技巧,可以提高游戏开发的效率和性能,同时避免一些常见的问题。

下面是这些操作的流程图:

graph TD;
    A[开始] --> B[创建对象];
    B --> C{是否需要清理};
    C -- 是 --> D[使用析构函数或OnDestroy];
    C -- 否 --> E[继续执行];
    E --> F{是否有耗时任务};
    F -- 是 --> G[使用协程];
    F -- 否 --> H[顺序执行代码];
    G --> I[使用yield控制执行流程];
    I --> J[完成任务];
    D --> K[对象清理完成];
    H --> J;
    J --> L[结束];
    K --> L;

同时,为了更清晰地对比不同操作的特点,我们可以列出以下表格:
| 操作类型 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 常规函数 | 执行速度快,逻辑清晰 | 可能导致程序锁定 | 简单、快速完成的任务 |
| 协程 | 避免程序锁定,可并行执行 | 创建对象速度慢 | 耗时任务 |
| 析构函数 | 自动清理对象,减少内存占用 | 可能存在垃圾回收延迟 | 需要手动清理数据的对象 |

基于Unity 3D的C#编程:高级特性与技巧

4. 协程的进一步应用与注意事项
4.1 协程的嵌套与控制

协程不仅可以独立运行,还可以相互嵌套和控制。一个协程可以启动另一个协程,并且可以通过共享变量来实现对其他协程的控制。例如,我们可以设置一个布尔变量作为“开关”,来决定某个协程是否继续执行。

// 控制协程的布尔变量
bool isRunning = true;

IEnumerator OuterCoroutine()
{
    // 启动内部协程
    StartCoroutine(InnerCoroutine());

    // 模拟一些操作
    yield return new WaitForSeconds(5f);

    // 停止内部协程
    isRunning = false;
}

IEnumerator InnerCoroutine()
{
    while (isRunning)
    {
        Debug.Log("Inner coroutine is running...");
        yield return new WaitForSeconds(1f);
    }
    Debug.Log("Inner coroutine has stopped.");
}

在上述代码中, OuterCoroutine 启动了 InnerCoroutine ,并在5秒后将 isRunning 设置为 false ,从而停止 InnerCoroutine 的执行。

4.2 协程的性能考虑

虽然协程可以避免程序锁定,但过多的协程可能会对性能产生影响。每个协程都需要一定的内存和CPU资源来管理,因此在使用协程时需要谨慎考虑。

以下是一些优化协程性能的建议:
- 减少不必要的协程 :只在必要时使用协程,避免创建过多的协程实例。
- 合理设置等待时间 :避免使用过短的等待时间,以免协程频繁唤醒,增加CPU负担。
- 及时停止协程 :当协程不再需要时,及时停止它,释放资源。

5. 高级内存管理技巧
5.1 对象池的实现

为了减少频繁创建和销毁对象对性能的影响,我们可以使用对象池技术。对象池是一种预先创建一定数量的对象,并在需要时重复使用这些对象的技术。

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    public GameObject prefab;
    public int poolSize = 10;

    private List<GameObject> pool;

    void Start()
    {
        // 初始化对象池
        pool = new List<GameObject>();
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            pool.Add(obj);
        }
    }

    public GameObject GetObject()
    {
        // 查找可用的对象
        foreach (GameObject obj in pool)
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

        // 如果没有可用对象,创建一个新的对象
        GameObject newObj = Instantiate(prefab);
        pool.Add(newObj);
        return newObj;
    }

    public void ReturnObject(GameObject obj)
    {
        // 将对象返回对象池
        obj.SetActive(false);
    }
}

在上述代码中, ObjectPool 类实现了一个简单的对象池。在 Start 方法中,我们预先创建了一定数量的对象,并将它们设置为非激活状态。 GetObject 方法用于获取一个可用的对象,如果没有可用对象,则创建一个新的对象。 ReturnObject 方法用于将对象返回对象池。

5.2 内存泄漏的检测与避免

内存泄漏是指程序在运行过程中,由于某些原因导致内存无法被释放,从而使内存占用不断增加。在Unity 3D的C#编程中,常见的内存泄漏原因包括未正确释放资源、事件和委托未正确移除等。

为了检测内存泄漏,我们可以使用Unity的内存分析工具,如Profiler。通过分析内存使用情况,我们可以找出哪些对象占用了过多的内存,并及时进行处理。

以下是一些避免内存泄漏的建议:
- 正确释放资源 :在使用完资源后,及时调用相应的释放方法,如 Dispose 方法。
- 移除事件和委托 :在对象销毁时,确保移除所有的事件和委托,避免引用导致对象无法被垃圾回收。
- 避免循环引用 :循环引用会导致对象之间相互引用,从而使它们无法被垃圾回收。

6. 总结与展望
6.1 总结

在Unity 3D的C#编程中,我们学习了许多高级特性和技巧,包括反射对象、析构函数、并发与协程等。这些特性和技巧可以帮助我们提高游戏开发的效率和性能,同时避免一些常见的问题。

  • 反射对象 :通过 UpdateReflection 类和 Mirror 对象,我们可以实现对象的位置和旋转的镜像。同时,通过优化库的使用,可以减少内存占用。
  • 析构函数 :析构函数用于手动清理对象,在使用不安全代码或需要手动清理数据时非常有用。但需要注意垃圾回收的延迟和内存管理的平衡。
  • 并发与协程 :协程可以打破代码的顺序执行模式,避免程序锁定,适用于处理耗时任务。同时,协程还可以相互嵌套和控制,实现更复杂的逻辑。
6.2 展望

随着游戏开发技术的不断发展,Unity 3D和C#也在不断更新和完善。未来,我们可以期待更多的高级特性和技巧的出现,帮助我们开发出更加优秀的游戏。

同时,我们也需要不断学习和掌握新的知识,提高自己的编程水平。在实际开发中,要根据具体的需求和场景,合理运用各种技术和技巧,以达到最佳的开发效果。

下面是一个总结不同技术适用场景的表格:
| 技术 | 适用场景 |
| ---- | ---- |
| 反射对象 | 需要实现对象镜像、优化库使用的场景 |
| 析构函数 | 需要手动清理数据、使用不安全代码的场景 |
| 协程 | 处理耗时任务、需要并行执行的场景 |
| 对象池 | 频繁创建和销毁对象的场景 |

另外,为了更清晰地展示整个开发过程中不同操作的流程,我们可以绘制如下流程图:

graph LR;
    A[需求分析] --> B[选择技术方案];
    B --> C{是否需要反射对象};
    C -- 是 --> D[实现反射对象];
    C -- 否 --> E{是否需要手动清理对象};
    E -- 是 --> F[使用析构函数];
    E -- 否 --> G{是否有耗时任务};
    G -- 是 --> H[使用协程];
    G -- 否 --> I[使用常规函数];
    D --> J[开发与测试];
    F --> J;
    H --> J;
    I --> J;
    J --> K{是否频繁创建销毁对象};
    K -- 是 --> L[实现对象池];
    K -- 否 --> M[继续开发];
    L --> M;
    M --> N[发布游戏];

通过合理运用这些高级特性和技巧,并结合实际需求进行开发,我们可以在Unity 3D中更好地使用C#语言,开发出高质量、高性能的游戏。

提供了一个基于51单片机的RFID门禁系统的完整资源文件,包括PCB图、原理图、论文以及源程序。该系统设计由单片机、RFID-RC522频射卡模块、LCD显示、灯控电路、蜂鸣器报警电路、存储模块和按键组成。系统支持通过密码和刷卡两种方式进行门禁控制,灯亮表示开门成功,蜂鸣器响表示开门失败。 资源内容 PCB图:包含系统的PCB设计图,方便用户进行硬件电路的制作和调试。 原理图:详细展示了系统的电路连接和模块布局,帮助用户理解系统的工作原理。 论文:提供了系统的详细设计思路、实现方法以及测试结果,适合学习和研究使用。 源程序:包含系统的全部源代码,用户可以根据需要进行修改和优化。 系统功能 刷卡开门:用户可以通过刷RFID卡进行门禁控制,系统会自动识别卡片并判断是否允许开门。 密码开门:用户可以通过输入预设密码进行门禁控制,系统会验证密码的正确性。 状态显示:系统通过LCD显示屏显示当前状态,如刷卡成功、密码错误等。 灯光提示:灯亮表示开门成功,灯灭表示开门失败或未操作。 蜂鸣器报警:当刷卡或密码输入错误时,蜂鸣器会发出报警声,提示用户操作失败。 适用人群 电子工程、自动化等相关专业的学生和研究人员。 对单片机和RFID技术感兴趣的爱好者。 需要开发类似门禁系统的工程师和开发者。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值