基于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#语言,开发出高质量、高性能的游戏。
超级会员免费看
47

被折叠的 条评论
为什么被折叠?



