Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)
文章目录
前言
欢迎来到《Unity C#入门到精通:50天学习计划》的第26天!经过前几周对C#基础、面向对象、数据结构以及Unity核心机制的学习,我们已经打下了坚实的基础。今天,我们将迈向一个能显著提升代码抽象层次和复用性的高级主题——泛型编程(Generic Programming)。泛型是C#中一项强大的特性,它允许我们编写与具体数据类型无关的、通用的代码模板。在Unity开发中,熟练运用泛型不仅能减少冗余代码,还能提高代码的类型安全性和性能。本篇将深入探讨泛型类、泛型方法、泛型约束的概念与使用,并结合Unity API中的泛型应用,最后通过一个实战案例——创建泛型对象池,让你真正掌握这一利器。
一、什么是泛型编程?
1.1 为何需要泛型:代码复用的痛点
想象一下,你需要编写一个方法来交换两个整数变量的值,然后再编写一个类似的方法来交换两个浮点数,接着可能还需要交换两个字符串…
// 交换两个整数
public void SwapInts(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
// 交换两个浮点数
public void SwapFloats(ref float a, ref float b)
{
float temp = a;
a = b;
b = temp;
}
// 交换两个字符串
public void SwapStrings(ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
// 如果还需要交换 GameObject, Vector3, 自定义类... 代码会变得非常冗余
你会发现这些方法的逻辑完全相同,唯一的区别在于处理的数据类型不同。如果每种类型都需要写一个版本,代码将变得极其冗余且难以维护。这就是泛型编程要解决的核心痛点:如何编写一份代码,使其能够适用于多种数据类型?
1.2 泛型的核心思想:类型参数化
泛型的核心思想是类型参数化(Type Parameterization)。它允许我们在定义类、接口、方法时,不指定具体的类型,而是使用一个类型占位符(Type Placeholder),通常用大写字母 T
表示(也可以是其他字母,如 TKey
, TValue
)。这个占位符被称为类型参数(Type Parameter)。
当实际使用这些泛型定义时,我们需要为类型参数提供一个具体的类型实参(Type Argument)。编译器或运行时会根据提供的类型实参,生成针对该特定类型的代码版本。
类比: 就像一个饼干模具(泛型定义),你可以用它来制作不同口味的饼干(具体类型的实例),比如巧克力味(int
)、草莓味(string
)、抹茶味(GameObject
)。模具本身是通用的,但制作出来的饼干口味是具体的。
1.3 泛型带来的优势
使用泛型主要有以下几个显著优势:
- 代码复用性(Reusability): 一份泛型代码可以处理多种数据类型,大大减少了代码量和维护成本。
- 类型安全(Type Safety): 泛型在编译时进行类型检查。例如,如果你创建了一个
List<int>
,就不能向其中添加字符串,编译器会直接报错,避免了运行时的类型转换错误。 - 性能提升(Performance): 对于值类型(如
int
,float
,struct
),泛型避免了装箱(Boxing)和拆箱(Unboxing)操作,相比于使用object
类型进行通用处理,性能更高。 - 代码更清晰(Clarity): 泛型代码通常更能明确地表达其意图,因为它显式地处理了类型参数。
二、泛型类与泛型方法:定义与使用
2.1 泛型类的定义
泛型类是在类名后面加上尖括号 <T>
来定义的,其中 T
是类型参数。
using UnityEngine;
// 定义一个简单的泛型容器类
public class GenericContainer<T>
{
private T item; // 使用类型参数 T 作为字段类型
public void SetItem(T newItem)
{
this.item = newItem;
}
public T GetItem()
{
return this.item;
}
public void LogItemType()
{
// typeof(T) 可以获取类型参数在运行时的具体类型
Debug.Log($"The type of item is: {
typeof(T)}");
}
}
在这个例子中,GenericContainer<T>
可以存储任何类型的数据。T
在类定义中充当了一个类型的占位符。
2.2 泛型类的使用
使用泛型类时,需要提供具体的类型实参。
using UnityEngine;
public class GenericClassExample : MonoBehaviour
{
void Start()
{
// 创建一个存储整数的容器
GenericContainer<int> intContainer = new GenericContainer<int>();
intContainer.SetItem(100);
int number = intContainer.GetItem();
Debug.Log($"Got integer: {
number}"); // 输出: Got integer: 100
intContainer.LogItemType(); // 输出: The type of item is: System.Int32
// 创建一个存储字符串的容器
GenericContainer<string> stringContainer = new GenericContainer<string>();
stringContainer.SetItem("Hello Generics!");
string message = stringContainer.GetItem();
Debug.Log($"Got string: {
message}"); // 输出: Got string: Hello Generics!
stringContainer.LogItemType(); // 输出: The type of item is: System.String
// 创建一个存储GameObject的容器
GenericContainer<GameObject> goContainer = new GenericContainer<GameObject>();
goContainer.SetItem(this.gameObject); // 存储当前脚本所在的GameObject
GameObject go = goContainer.GetItem();
Debug.Log($"Got GameObject: {
go.name}"); // 输出: Got GameObject: [当前对象名]
goContainer.LogItemType(); // 输出: The type of item is: UnityEngine.GameObject
}
}
可以看到,同一个 GenericContainer<T>
类定义,通过提供不同的类型实参(int
, string
, GameObject
),就能创建出处理不同数据类型的具体实例,并且保证了类型安全。
2.3 泛型方法的定义
泛型方法是在方法名后面、参数列表前面加上尖括号 <T>
来定义的。类型参数 T
可以在方法的参数、返回值以及方法体内部使用。
using UnityEngine;
public class GenericMethodExample
{
// 定义一个泛型方法来打印任何类型的值
public void LogValue<T>(T value)
{
Debug.Log($"Logging value ({
typeof(T)}): {
value}");
}
// 定义一个泛型方法来比较两个值是否相等
public bool AreEqual<T>(T value1, T value2)
{
// 使用 Equals 方法进行比较,适用于大多数类型
// 对于自定义类型,需要确保 Equals 被正确实现
return value1.Equals(value2);
}
// 泛型方法也可以有返回值
public T GetFirst<T>(T[] array)
{
if (array == null || array.Length == 0)
{
// default(T) 返回类型 T 的默认值
// 引用类型是 null, 值类型是 0 或 false, struct 是其成员的默认值
return default(T);
}
return array[0];
}
}
2.4 泛型方法的使用
调用泛型方法时,通常编译器可以通过传入的参数**推断(Infer)**出类型实参,因此可以省略尖括号中的类型。当然,也可以显式指定。
using UnityEngine;
public class UseGenericMethod : MonoBehaviour
{
void Start()
{
GenericMethodExample example = new GenericMethodExample();
// 调用泛型方法,编译器自动推断类型
example.LogValue(123); // T 推断为 int
example.LogValue("Hello"); // T 推断为 string
example.LogValue(transform); // T 推断为 Transform
bool intsEqual = example.AreEqual(5, 5); // T 推断为 int
bool stringsEqual = example.AreEqual("A", "B"); // T 推断为 string
Debug.Log($"Are 5 and 5 equal? {
intsEqual}"); // 输出: true
Debug.Log($"Are 'A' and 'B' equal? {
stringsEqual}"); // 输出: false
int[] numbers = {
1, 2, 3 };
string[] names = {
"Alice", "Bob" };
int firstNumber = example.GetFirst(numbers); // T 推断为 int
string firstName = example.GetFirst(names); // T 推断为 string
// 也可以显式指定类型
// string firstNameExplicit = example.GetFirst<string>(names);
Debug.Log($"First number: {
firstNumber}"); // 输出: 1
Debug.Log($"First name: {
firstName}"); // 输出: Alice
// 处理空数组
float[] emptyFloats = {
};
float firstFloat = example.GetFirst(emptyFloats); // T 推断为 float
Debug.Log($"First float (empty): {
firstFloat}"); // 输出: 0 (float 的默认值)
}
}
三、泛型约束:为类型参数添加限制
虽然泛型的目标是通用,但有时我们希望对可以用作类型实参的类型施加一些限制(Constraints)。例如,我们可能希望确保类型参数 T
必须是某个类的子类,或者必须实现某个接口,或者必须有一个无参数的构造函数。这就是泛型约束的作用。
3.1 为何需要约束
考虑一个场景:你想在一个泛型方法中调用类型参数 T
的某个特定方法或访问其特定属性。但编译器不知道 T
具体是什么类型,无法保证该方法或属性一定存在。
public void DoSomething<T>(T obj)
{
// 编译错误!编译器不知道 T 是否有 .SomeMethod() 方法
// obj.SomeMethod();
}
泛型约束就是用来告诉编译器,类型参数 T
必须满足某些条件,这样在泛型定义内部就可以安全地使用这些条件所保证的成员了。
3.2 常见的泛型约束类型
约束使用 where
关键字,放在泛型定义的参数列表之后。一个类型参数可以有多个约束,用逗号分隔。
3.2.1 where T : struct
类型参数 T
必须是一个值类型(如 int
, float
, bool
, Vector3
, 自定义 struct
)。注意 Nullable<T>
类型除外。
public class ValueContainer<T> where T : struct
{
// ... 只能用于值类型
}
3.2.2 where T : class
类型参数 T
必须是一个引用类型(如 string
, GameObject
, 自定义 class
,接口,委托)。
public void ProcessReference<T>(T obj) where T : class
{
if (obj == null) // 可以安全地与 null 比较
{
Debug.Log("Object is null");
}
// ...
}
3.2.3 where T : new()
类型参数 T
必须有一个公共的、无参数的构造函数。这允许你在泛型代码中使用 new T()
来创建该类型的实例。new()
约束必须是所有约束中的最后一个。
public T CreateInstance<T>() where T : new()
{
return new T(); // 可以安全地调用无参构造函数
}
3.2.4 where T : <base class name>
类型参数 T
必须是指定的基类,或者是该基类的派生类。
// 假设有一个基类 Enemy
public abstract class Enemy : MonoBehaviour {
/* ... */ }
// 这个管理器只能处理 Enemy 或其子类 (如 Boss, Minion)
public class EnemyManager<T> where T : Enemy
{
private List<T> enemies = new List<T>();
public void AddEnemy(T enemy)
{
enemies.Add(enemy);
enemy.gameObject.SetActive(true); // 可以安全地访问 Enemy 或 MonoBehaviour 的成员
}
}
3.2.5 where T : <interface name>
类型参数 T
必须是指定的接口,或者实现了该接口的类型。
// 假设有一个接口 IAttackable
public interface IAttackable
{
void TakeDamage(float amount);
}
// 这个方法可以对任何可攻击的对象造成伤害
public void DealDamage<T>(T target, float damage) where T : IAttackable
{
target.TakeDamage(damage); // 可以安全地调用接口方法
}
3.2.6 where T : U
类型参数 T
必须是另一个类型参数 U
,或者是 U
的派生类。这称为裸类型约束,通常用于建立两个类型参数之间的继承关系。
public class AdvancedContainer<T, U> where T : U
{
// T 必须派生自 U (或就是 U)
}
3.3 Unity中的关键约束:where T : Component
在 Unity 开发中,最常用的约束之一是 where T : Component
(或者 where T : MonoBehaviour
,因为 MonoBehaviour
继承自 Component
)。这个约束表明类型参数 T
必须是 Unity 的一个组件类型。
为什么重要?
因为 Unity 的核心 API,如 GetComponent<T>
, AddComponent<T>
, FindObjectOfType<T>
等,都是用来操作组件的。通过添加 where T : Component
约束,我们可以在自己的泛型代码中安全地使用这些 API,并且可以访问 GameObject
、Transform
等 Component
类提供的通用属性和方法。
using UnityEngine;
public class ComponentFinder<T> where T : Component // 约束 T 必须是 Component 或其子类
{
public T FindComponentInScene(string objectName)
{
GameObject obj = GameObject.Find(objectName);
if (obj != null)
{
// 因为有约束,这里可以安全地调用 GetComponent<T>
T component = obj.GetComponent<T>();
if (component != null)
{
Debug.Log($"Found component {
typeof(T).Name} on {
objectName}");
return component;
}
else
{
Debug.LogWarning($"Object {
objectName} found, but component {
typeof(T).Name} not found.");
}
}
else
{
Debug.LogWarning($"Object {
objectName} not found in scene.");
}
return null; // 或者 default(T)
}
}
// 使用示例
public class UseComponentFinder : MonoBehaviour
{
void Start()
{
// 查找场景中名为 "Main Camera" 的对象上的 Camera 组件
ComponentFinder<Camera> cameraFinder = new ComponentFinder<Camera>();
Camera mainCam = cameraFinder.FindComponentInScene("Main Camera");
// 查找场景中名为 "Player" 的对象上的 Rigidbody 组件
ComponentFinder<Rigidbody> rbFinder = new ComponentFinder<Rigidbody>();
Rigidbody playerRb = rbFinder.FindComponentInScene("Player");
// 尝试查找不存在的组件或对象会打印警告
ComponentFinder<Light> lightFinder = new ComponentFinder<Light>();
Light directionalLight = lightFinder.FindComponentInScene("Directional Light"); // 假设它存在但没有 Light 组件
}
}
四、泛型在Unity API中的应用
Unity 大量使用了泛型来提供更类型安全、更易用的 API。熟悉这些泛型方法对于高效开发至关重要。
4.1 获取组件:GetComponent<T>()
这是最常用的泛型方法之一。它用于获取附加到同一 GameObject
上的特定类型的组件。
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Rigidbody rb;
private Animator anim;
void Start()
{
// 使用泛型版本获取组件,无需强制类型转换,且类型安全
rb = GetComponent<Rigidbody>();
anim = GetComponent<Animator>();
if (rb == null)
{
Debug.LogError("Rigidbody component not found on player!");
}
if (anim == null)
{
Debug.LogError("Animator component not found on player!");
}
// 对比旧的非泛型方法 (需要强制类型转换,且容易出错)
// rb = (Rigidbody)GetComponent(typeof(Rigidbody));
// rb = GetComponent("Rigidbody") as Rigidbody; // 基于字符串的版本更不推荐
}
}
优势:
- 编译时类型检查: 如果
Rigidbody
或Animator
组件不存在,GetComponent<T>()
会返回null
,但代码本身能编译。如果尝试获取一个非Component
类型(如GetComponent<int>()
),编译器会直接报错。 - 无需强制转换: 返回值直接是
T
类型,代码更简洁。
类似的泛型方法还有 GetComponents<T>()
, GetComponentInChildren<T>()
, GetComponentInParent<T>()
等。
4.2 查找对象:FindObjectOfType<T>()
此方法用于在整个场景中查找第一个激活的、类型为 T
的 MonoBehaviour
实例。
using UnityEngine;
public class GameManagerFinder : MonoBehaviour
{
private GameManager gameManager;
void Start()
{
// 查找场景中唯一的 GameManager 实例 (假设 GameManager 是一个 MonoBehaviour)
gameManager = FindObjectOfType<GameManager>();
if (gameManager == null)
{
Debug.LogError("GameManager not found in the scene!");
}
else
{
gameManager.StartGame(); // 调用 GameManager 的方法
}
// 对比旧的非泛型方法
// gameManager = (GameManager)FindObjectOfType(typeof(GameManager));
}
}
注意: FindObjectOfType<T>()
是一个相对昂贵的操作,因为它需要遍历场景中的所有 GameObject
。应避免在 Update()
等频繁调用的方法中使用。通常在 Start()
或 Awake()
中调用一次并缓存结果。
类似的泛型方法还有 FindObjectsOfType<T>()
,用于查找场景中所有类型为 T
的实例。
4.3 其他泛型API示例(可选)
AddComponent<T>()
: 向GameObject
添加一个新的T
类型组件,返回添加的组件实例。Rigidbody newRb = gameObject.AddComponent<Rigidbody>();
Resources.Load<T>(string path)
: 从Resources
文件夹加载指定路径下的资源,并将其转换为T
类型。Texture2D playerAvatar = Resources.Load<Texture2D>("Textures/PlayerAvatar"); GameObject enemyPrefab = Resources.Load<GameObject>("Prefabs/Enemy");
ScriptableObject.CreateInstance<T>()
: 创建一个T
类型的ScriptableObject
实例,其中T
必须继承自ScriptableObject
。MyGameSettings settings = ScriptableObject.CreateInstance<MyGameSettings>();
五、实践:创建泛型对象池
对象池(Object Pool)是 Unity 中常用的性能优化技术,尤其适用于需要频繁创建和销毁相同类型对象的场景(如子弹、特效、敌人)。我们可以利用泛型来创建一个通用的对象池,使其能够管理任何类型的 MonoBehaviour
对象。
5.1 对象池的设计思路
- 池化容器: 使用一个集合(如
Queue<T>
或List<T>
)来存储非活动的对象实例。 - 预制体(Prefab): 需要一个对象模板(Prefab)来创建新的实例。
- 获取对象(Get): 当需要对象时,先检查池中是否有可用的非活动对象。如果有,则取出、激活并返回;如果没有,则根据预制体创建一个新对象,并返回。
- 回收对象(Return): 当对象不再需要时,不销毁它,而是将其设置为非活动状态,并放回池中以供后续使用。
- 初始化/重置: 从池中获取对象时,可能需要重置其状态(如位置、速度、生命值等)。回收对象时,也可能需要清理状态。
5.2 泛型对象池的实现
下面是一个简单的泛型对象池实现:
using UnityEngine;
using System.Collections.Generic;
using System; // 需要引入 System 命名空间以使用 Action
public class GenericObjectPool<T> where T : MonoBehaviour // 约束 T 必须是 MonoBehaviour
{
private readonly T prefab; // 对象预制体
private readonly Queue<T> pool = new Queue<T>(); // 池容器
private readonly Transform parentTransform; // 池化对象的父节点(可选,用于整理层级)
private readonly Action<T> onGet; // 获取对象时的回调(用于初始化/重置)
private readonly Action<T> onReturn; // 回收对象时的回调(用于清理)
/// <summary>
/// 构造函数
/// </summary>
/// <param name="prefab">要池化的对象预制体</param>
/// <param name="initialSize">初始池大小</param>
/// <param name="onGetCallback">获取对象时调用的方法 (可选)</param>
/// <param name="onReturnCallback">回收对象时调用的方法 (可选)</param>
/// <param name="parent">池化对象的父级Transform (可选)</param>
public GenericObjectPool(T prefab, int initialSize, Action<T> onGetCallback = null, Action<T> onReturnCallback = null, Transform parent = null)
{
this.prefab = prefab;
this.parentTransform = parent;
this.onGet = onGetCallback;
this.onReturn = onReturnCallback;
// 预先填充池
for (int i = 0; i < initialSize; i++)
{
T obj = CreateNewObject();
pool.Enqueue(obj);
}
}
/// <summary>
/// 从池中获取一个对象
/// </summary>
/// <returns>可用的对象实例</returns>
public T Get()
{
T obj;
if (pool.Count > 0)
{
obj = pool.Dequeue(); // 从池中取出
}
else
{
obj = CreateNewObject(setActive: false); // 池空了,创建新的(先不激活)
}
// 调用获取回调(如果已设置)
onGet?.Invoke(obj); // C# 6.0 null 条件运算符 ?.
obj.gameObject.SetActive(true); // 激活对象
return obj;
}
/// <summary>
/// 将对象回收至池中
/// </summary>
/// <param name="obj">要回收的对象</param>
public void Return(T obj)
{
if (obj == null) return;
// 调用回收回调(如果已设置)
onReturn?.Invoke(obj);
obj.gameObject.SetActive(false); // 设置为非活动
pool.Enqueue(obj); // 放回池中
}
// 内部方法:创建新对象实例
private T CreateNewObject(bool setActive = false)
{
T newObj = GameObject.Instantiate(prefab, parentTransform);
newObj.gameObject.SetActive(setActive); // 根据需要设置初始活动状态
return newObj;
}
// 获取当前池中可用对象数量
public int GetPooledCount()
{
return pool.Count;
}
}
关键点解释:
where T : MonoBehaviour
: 约束了池只能管理MonoBehaviour
或其子类,这样可以安全地访问gameObject
和transform
等成员,并且可以使用Instantiate
。Queue<T>
: 使用队列可以方便地实现先进先出(FIFO)的获取逻辑。Action<T>
: 使用委托Action<T>
来定义获取和回收时的回调方法,增加了灵活性。调用者可以传入自定义的初始化和清理逻辑。parentTransform
: 提供一个父节点可以帮助在 Hierarchy 窗口中整理池化对象,避免混乱。onGet?.Invoke(obj)
: 使用 null 条件运算符?.
,只有当onGet
委托不为 null 时才调用Invoke
。
5.3 如何使用泛型对象池
假设你有一个 Bullet
脚本(继承自 MonoBehaviour
)和一个对应的预制体。
using UnityEngine;
// 示例:子弹脚本
public class Bullet : MonoBehaviour
{
public float speed = 10f;
private Rigidbody rb;
private GenericObjectPool<Bullet> pool; // 持有对象池引用,方便回收
void Awake()
{
rb = GetComponent<Rigidbody>();
if (rb == null) rb = gameObject.AddComponent<Rigidbody>();
rb.useGravity = false;
}
// 设置所属的对象池 (通常在 Get 时由外部设置)
public void SetPool(GenericObjectPool<Bullet> pool)
{
this.pool = pool;
}
// 子弹的初始化逻辑(在 Get 时被调用)
public void Initialize(Vector3 startPosition, Quaternion rotation)
{
transform.position = startPosition;
transform.rotation = rotation;
rb.velocity = transform.forward * speed;
// 可以在这里启动一个协程,N秒后自动回收
StartCoroutine(ReturnToPoolAfterDelay(3f));
}
// 子弹的清理逻辑(在 Return 时被调用)
public void ResetState()
{
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
// 可能需要重置其他状态...
}
void OnCollisionEnter(Collision collision)
{
// 碰撞到物体后回收
ReturnToPool();
}
private System.Collections.IEnumerator ReturnToPoolAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
ReturnToPool();
}
private void ReturnToPool()
{
// 确保 StopAllCoroutines 在 SetActive(false) 之前调用,或者在 ResetState 中处理
StopAllCoroutines();
pool?.Return(this);
}
}
// 示例:玩家射击脚本
public class PlayerShooting : MonoBehaviour
{
public Bullet bulletPrefab; // 在 Inspector 中指定子弹预制体
public Transform firePoint; // 子弹发射点
public int initialPoolSize = 20;
private GenericObjectPool<Bullet> bulletPool;
void Start()
{
// 创建子弹对象池
// 传入初始化和重置方法作为回调
bulletPool = new GenericObjectPool<Bullet>(
bulletPrefab,
initialPoolSize,
OnGetBullet, // 获取子弹时的回调
OnReturnBullet, // 回收子弹时的回调
null // 可选:指定父 Transform
);
}
void Update()
{
if (Input.GetButtonDown("Fire1")) // 按下鼠标左键或Ctrl
{
Shoot();
}
}
void Shoot()
{
// 从池中获取子弹
Bullet bulletInstance = bulletPool.Get();
// 设置子弹属于哪个池(用于自我回收)
bulletInstance.SetPool(bulletPool);
// 注意:初始化逻辑现在由 OnGetBullet 回调处理了
// bulletInstance.Initialize(firePoint.position, firePoint.rotation);
}
// --- 对象池回调方法 ---
// 当从池中获取子弹时调用
void OnGetBullet(Bullet bullet)
{
Debug.Log("Getting bullet from pool.");
// 在这里进行初始化设置
bullet.transform.position = firePoint.position;
bullet.transform.rotation = firePoint.rotation;
// bullet.GetComponent<Rigidbody>().velocity = firePoint.forward * bullet.speed; // 移到 Bullet.Initialize 里更好
bullet.Initialize(firePoint.position, firePoint.rotation); // 调用子弹自身的初始化方法
}
// 当子弹返回池中时调用
void OnReturnBullet(Bullet bullet)
{
Debug.Log("Returning bullet to pool.");
// 在这里进行清理(如果 Bullet 自身的 ResetState 不够的话)
bullet.ResetState(); // 调用子弹自身的重置方法
}
}
通过这种方式,我们创建了一个可以管理 Bullet
对象的池。如果需要池化其他类型的对象(如 Enemy
, Effect
),只需创建新的 GenericObjectPool<Enemy>
或 GenericObjectPool<Effect>
实例即可,无需重写对象池的核心逻辑。
5.4 泛型事件管理器基类的思路(备选实践)
另一个常见的泛型应用是创建一个泛型的事件管理器基类。事件系统常用于模块解耦。我们可以定义一个泛型的事件基类,或者一个泛型的事件参数类。
// 泛型事件参数
public class GameEventArgs<T>
{
public T Data {
get; private set; }
public GameEventArgs(T data) {
Data = data; }
}
// 泛型事件管理器(简化示例)
public static class EventManager<TEvent> where TEvent : struct // 约束为值类型,例如用枚举定义事件类型
{
private static Dictionary<TEvent, Action<object>> eventDictionary = new Dictionary<TEvent, Action<object>>();
public static void Subscribe(TEvent eventType, Action<object> listener)
{
if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
{
thisEvent += listener;
eventDictionary[eventType] = thisEvent;
}
else
{
eventDictionary.Add(eventType, listener);
}
}
public static void Unsubscribe(TEvent eventType, Action<object> listener)
{
if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
{
thisEvent -= listener;
eventDictionary[eventType] = thisEvent;
}
}
public static void Trigger(TEvent eventType, object eventArgs = null)
{
if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
{
thisEvent?.Invoke(eventArgs);
}
}
}
// 使用示例
public enum GameplayEvent {
PlayerDied, ScoreChanged, EnemySpawned }
public class UIManager : MonoBehaviour
{
void OnEnable() {
EventManager<GameplayEvent>.Subscribe(GameplayEvent.ScoreChanged, UpdateScoreUI); }
void OnDisable() {
EventManager<GameplayEvent>.Unsubscribe(GameplayEvent.ScoreChanged, UpdateScoreUI); }
void UpdateScoreUI(object data) {
/* 更新UI */ }
}
public class PlayerHealth : MonoBehaviour
{
void Die() {
EventManager<GameplayEvent>.Trigger(GameplayEvent.PlayerDied); }
}
这个简单的泛型事件管理器允许使用枚举等值类型作为事件标识,提供了一定程度的类型安全和代码复用。更复杂的事件系统可能会使用更复杂的泛型结构。
六、总结
今天我们深入学习了 C# 中的泛型编程及其在 Unity 开发中的应用,这是提升代码质量和开发效率的关键一步。核心要点回顾:
- 泛型的核心价值:通过类型参数化实现代码的高度复用,同时保证类型安全和性能。
- 泛型类与泛型方法:掌握了使用
<T>
定义泛型类和泛型方法的语法,以及如何在实例化或调用时提供类型实参。 - 泛型约束 (
where
):理解了约束的必要性,并学习了常见的约束类型(class
,struct
,new()
, 基类, 接口),特别是 Unity 中常用的where T : Component
。 - Unity API 中的泛型:熟悉了
GetComponent<T>
,FindObjectOfType<T>
,AddComponent<T>
,Resources.Load<T>
等常用泛型 API,它们让代码更简洁、更安全。 - 实践应用:通过实现一个泛型对象池
GenericObjectPool<T>
,我们看到了泛型在解决实际问题(如性能优化)中的强大威力,也了解了泛型在事件系统设计中的潜力。
掌握泛型是 C# 进阶的重要标志。它鼓励我们思考更抽象、更通用的解决方案,从而编写出更优雅、更健壮、更易于维护的 Unity 项目代码。在后续的学习和实践中,请有意识地寻找应用泛型的机会,你会发现它能为你节省大量时间和精力。明天,我们将学习另一个处理异步和时序任务的利器——协程(Coroutine)。