Unity C#集合与随机物体生成:从List管理到预制体实例化实战 1. 项目概述与核心目标今天是我们“Unity学习90天”计划的第三天一个非常关键的节点。前两天我们搭建了环境写了第一个“Hello World”算是和Unity打了个招呼。从今天开始我们要动真格的了目标是让Unity这个强大的引擎按照我们的意志动起来。今天的任务标题很明确“认识C# 集合与常用类并实现生成随机位置的 10 个立方体”。这看似是一个简单的功能但背后串联起了从C#基础语法到Unity引擎操作的一条完整链路。很多新手朋友在学Unity时会陷入一个误区要么死磕C#语法书感觉离做出东西遥遥无期要么直接跟着教程拖拽组件知其然不知其所以然一旦需要自己实现新功能就无从下手。我们今天这个项目就是一座桥它要求你运用刚学的C#知识集合、类去解决一个具体的、可视化的Unity问题生成物体。当你点击运行看到10个立方体随机散落在场景中时你收获的不仅仅是一个酷炫的效果更是一套“用代码驱动引擎”的思维模式。这适合所有Unity初学者无论你是想转行游戏开发还是做交互艺术、模拟仿真今天的内容都是你代码工具箱里必不可少的第一批工具。2. 核心思路与方案设计要实现“生成随机位置的10个立方体”我们不能蛮干得先拆解。这个目标可以分解为三个子任务第一如何用C#代码在Unity中创建一个立方体第二如何生成一个符合要求的随机位置第三如何高效地管理这10个立方体而不是写十遍几乎相同的代码。这第三个任务正是我们引入C#集合Collection的原因。为什么是集合想象一下如果你有10个朋友你可能会用脑子记也可能会写一张名单。这张名单就是一个“集合”它把多个同类型的个体你的朋友们组织在一起方便你进行统一操作比如群发消息。在编程中当我们需要处理一组同类型的数据比如10个立方体的引用、100个敌人的血量、1000个道具的信息时使用集合是最高效、最清晰的方式。直接使用十个独立的变量代码会变得冗长且难以维护。在C#众多的集合类型中我们今天重点使用ListT。它是一个动态数组你可以随时往里面添加或移除元素而不用事先声明它到底能装多少东西这非常符合我们“先生成再管理”的需求。我们还会用到Random类来生成随机数以及Vector3这个Unity引擎中表示三维位置的核心类。整个方案的流程图在脑海中是这样的程序启动 → 创建一个空的ListGameObject用于存放立方体引用 → 进入一个循环10次的for循环 → 在每次循环中实例化一个立方体预制体 → 为它计算一个随机坐标 → 将这个新立方体的引用添加到List中。这样我们通过一个循环和一個集合就优雅地解决了问题。3. 核心细节解析与实操要点3.1 理解GameObject、Transform与Vector3在动手写代码前必须厘清几个核心概念否则代码写了也不知道自己在操作什么。GameObject你可以把它理解为Unity场景中的“空壳”或“容器”。所有在场景中看得见、摸得着碰撞的东西都是一个GameObject。一个立方体、一个光源、一个摄像机都是GameObject。它本身没有形状、没有行为但它可以挂载各种组件Component来赋予其功能。Transform组件这是每个GameObject与生俱来、不可删除的组件。它定义了GameObject在三维空间中的状态包括位置Position、旋转Rotation和缩放Scale。我们代码中要修改的“随机位置”本质上就是修改这个GameObject所挂载的Transform组件里的Position属性。Vector3这是一个结构体用于表示一个三维向量。在Unity中它最常用的就是表示一个点的坐标。Vector3有三个浮点数类型的成员x, y, z。例如Vector3(0, 0, 0)表示世界坐标系的原点。我们通过Random.Range(min, max)方法为x, y, z分别生成一个随机数就能组合成一个新的随机位置向量。注意Random.Range对于整数和浮点数有细微差别。Random.Range(0, 10)返回0到9的整数包含0不包含10。Random.Range(0.0f, 10.0f)返回0.0到10.0的浮点数包含0.0包含10.0。在我们的场景中使用浮点数版本更合适。3.2 深入C# List 集合ListT是我们今天的管理器。这里的T是泛型参数意味着这个列表可以存放任何你指定的类型。我们存放的是GameObject类型所以声明为ListGameObject。关键操作初始化ListGameObject cubeList new ListGameObject();这行代码创建了一个最初为空的列表。添加元素cubeList.Add(newCube);将新的立方体引用newCube添加到列表的末尾。访问元素cubeList[0]可以访问列表中的第一个元素索引从0开始。这在后续你想对某个特定立方体进行操作时非常有用。遍历元素通过foreach (GameObject cube in cubeList)循环可以轻松地对列表中的每一个立方体执行相同操作比如批量修改颜色、销毁等。为什么要用List而不是数组数组 (GameObject[]) 的长度是固定的你在创建时必须确定它的大小。而List是动态的你不需要关心初始容量它会随着元素的添加自动扩容。在这个项目中我们虽然知道是10个但使用List是更好的实践因为未来需求可能变化比如用户输入一个数字来生成立方体List的灵活性更能适应这种变化。3.3 预制体Prefab的创建与使用我们不可能每次都通过代码从头开始组合一个立方体虽然可以但效率低。Unity的最佳实践是使用预制体。创建预制体在场景中右键 - 3D Object - Cube创建一个立方体。然后将其从Hierarchy窗口拖拽到Project窗口的某个文件夹如“Prefabs”中。这样一个蓝色的立方体图标就出现了这就是你的预制体。之后可以删除场景中的那个立方体实例。在代码中实例化预制体首先你需要一个公共字段或使用[SerializeField]私有字段在脚本中引用这个预制体。public GameObject cubePrefab; // 在Inspector面板中将预制体拖拽到这里然后在代码中使用Instantiate方法GameObject newCube Instantiate(cubePrefab, randomPosition, Quaternion.identity);Instantiate方法有三个参数要实例化的预制体、生成的位置、生成的旋转。Quaternion.identity表示“无旋转”是一个标准值。实操心得务必区分“预制体资源”和“实例化对象”。预制体是模板存储在Project窗口实例化对象是副本出现在Hierarchy窗口。修改预制体属性会影响所有已生成和未来生成的实例修改场景中某个实例的属性只影响它自己除非应用回预制体。4. 完整实现步骤与代码详解下面我们一步步实现这个功能。我会先给出完整的代码然后逐块拆解。4.1 创建脚本与挂载在Project窗口右键 - Create - C# Script命名为RandomCubeGenerator。双击用IDE如Visual Studio打开。将脚本拖拽到Hierarchy窗口的任意一个GameObject上通常可以创建一个空的GameObject命名为“GameManager”来挂载它。4.2 完整脚本代码using System.Collections.Generic; // 引入集合命名空间这是使用List的关键 using UnityEngine; public class RandomCubeGenerator : MonoBehaviour { // 1. 声明公共变量用于在Inspector面板关联预制体 public GameObject cubePrefab; // 2. 声明一个私有的List用于在运行时管理生成的立方体 private ListGameObject cubeList new ListGameObject(); // 3. 定义生成区域的范围可调参数方便调试 public float rangeX 10f; public float rangeY 5f; public float rangeZ 10f; void Start() { // 4. 输入验证确保预制体已赋值避免运行时错误 if (cubePrefab null) { Debug.LogError(Cube Prefab is not assigned in the Inspector!); return; // 如果没赋值直接退出不再执行后续代码 } GenerateRandomCubes(10); // 5. 调用生成函数 } /// summary /// 生成指定数量、随机位置的立方体 /// /summary /// param namecount要生成的立方体数量/param void GenerateRandomCubes(int count) { // 6. 循环生成立方体 for (int i 0; i count; i) { // 6.1 计算随机位置 // Random.Range会返回一个在[-range, range]区间内的随机浮点数 float posX Random.Range(-rangeX, rangeX); float posY Random.Range(-rangeY, rangeY); float posZ Random.Range(-rangeZ, rangeZ); Vector3 randomPosition new Vector3(posX, posY, posZ); // 6.2 实例化立方体预制体 // Instantiate的第二个参数就是生成的位置 GameObject newCube Instantiate(cubePrefab, randomPosition, Quaternion.identity); // 6.3 可选为立方体起个有辨识度的名字 newCube.name RandomCube_ i; // 6.4 将新立方体的引用加入到List中 cubeList.Add(newCube); // 6.5 可选打印日志便于调试 Debug.Log($Generated cube {i} at position: {randomPosition}); } // 7. 生成完成后打印总结信息 Debug.Log($Generation complete. Total cubes in list: {cubeList.Count}); } // 8. 扩展功能示例按空格键销毁所有生成的立方体 void Update() { if (Input.GetKeyDown(KeyCode.Space)) { DestroyAllCubes(); } } /// summary /// 销毁List中记录的所有立方体并清空列表 /// /summary void DestroyAllCubes() { // 9. 遍历List销毁每一个GameObject foreach (GameObject cube in cubeList) { Destroy(cube); } // 10. 清空List防止持有已销毁对象的引用“野指针” cubeList.Clear(); Debug.Log(All cubes have been destroyed and list cleared.); } }4.3 代码逐块解析与配置第1-3部分变量声明public GameObject cubePrefab;这行代码在Inspector面板创建一个可拖拽的槽位。你需要将之前创建的立方体预制体拖拽到这里。使用公共字段是快速原型开发时的常用方法。private ListGameObject cubeList new ListGameObject();我们初始化了一个私有的列表。它只在脚本内部可见用于运行时逻辑管理。public float rangeX, rangeY, rangeZ;这三个公共变量定义了立方体生成的随机范围。设置为公共变量意味着你可以在Unity编辑器的Inspector窗口中实时调整这些值无需修改代码就能立即看到效果变化比如把范围调大立方体就散得更开。这是一种非常高效的调试和工作流。第4部分输入验证防御性编程在Start方法里我们首先检查cubePrefab是否被赋值。如果没有则用Debug.LogError输出一条红色错误信息到Console窗口并直接return。这是一个好习惯能避免因为粗心导致的“空引用异常”让问题在第一时间暴露出来。第5-7部分核心生成逻辑GenerateRandomCubes(10)是核心函数。在循环中Random.Range(-rangeX, rangeX)生成每个坐标轴上的随机值。new Vector3(posX, posY, posZ)将三个随机数组合成一个位置向量。Instantiate(cubePrefab, randomPosition, Quaternion.identity)在指定位置创建立方体实例。cubeList.Add(newCube)将实例的引用存入列表完成了“创建”与“管理”的衔接。第8-10部分扩展功能——销毁我们在Update中监听空格键。按下时调用DestroyAllCubes方法。这里展示了集合的另一个强大之处批量操作。通过一个简单的foreach循环我们就能销毁所有立方体。最后务必调用cubeList.Clear()。因为Destroy(cube)只是销毁了游戏对象但列表里还保存着对这些“已失效对象”的引用。清空列表可以避免后续逻辑错误地访问这些无效引用。4.4 在Unity编辑器中的配置将脚本挂载到场景中的一个GameObject如“GameManager”上。在Project窗口找到你的立方体预制体。选中挂载了脚本的GameObject在Inspector面板找到Random Cube Generator脚本组件。你会看到Cube Prefab是一个空槽。将Project窗口中的立方体预制体拖拽到这个槽上。你还可以调整Range X/Y/Z的值比如都设为5。点击运行按钮。你应该立刻在场景中看到10个立方体随机分布在以原点为中心、边长为10的立方体空间内。Console窗口会输出生成日志。5. 深度优化与概念延伸实现了基础功能后我们可以思考如何做得更好并引出更多相关概念。5.1 避免重叠更智能的随机生成我们当前的随机生成立方体可能会重叠在一起。如何避免一个简单的方法是使用“拒绝采样”生成一个随机位置后检查这个位置是否与已生成的任何立方体太近如果太近就重新生成。void GenerateRandomCubesWithoutOverlap(int count, float minDistance) { for (int i 0; i count; i) { int attempts 0; bool positionFound false; Vector3 randomPosition Vector3.zero; while (!positionFound attempts 100) // 防止无限循环 { attempts; randomPosition new Vector3( Random.Range(-rangeX, rangeX), Random.Range(-rangeY, rangeY), Random.Range(-rangeZ, rangeZ) ); positionFound true; // 先假设位置可用 foreach (GameObject existingCube in cubeList) { if (Vector3.Distance(randomPosition, existingCube.transform.position) minDistance) { positionFound false; // 距离太近位置不可用 break; } } } if (positionFound) { GameObject newCube Instantiate(cubePrefab, randomPosition, Quaternion.identity); newCube.name Cube_ i; cubeList.Add(newCube); } else { Debug.LogWarning($Failed to find non-overlapping position for cube {i} after 100 attempts.); } } }这段代码引入了Vector3.Distance方法来计算两点距离并通过一个while循环来寻找合适位置。attempts计数器是为了防止在空间过于拥挤时陷入死循环。5.2 认识更多C#常用类与集合今天我们用了ListT它是顺序集合元素有顺序通过索引访问。C#中还有其他强大的集合DictionaryTKey, TValue字典键值对集合。如果你需要根据某个“名字”或“ID”快速找到对应的立方体字典是绝佳选择。例如private Dictionarystring, GameObject cubeDict new Dictionarystring, GameObject(); // 添加 cubeDict.Add(Cube_0, newCube); // 访问速度极快 GameObject targetCube cubeDict[Cube_0];HashSet 哈希集一个不包含重复元素的集合。如果你需要确保生成的立方体位置绝对唯一可以将位置向量存入HashSetVector3来检查唯一性。Queue 队列和Stack 栈代表先进先出FIFO和后进先出LIFO的数据结构。在实现对象池、撤销操作等功能时非常有用。常用类方面除了Random你还会频繁接触MathfUnity的数学工具库包含Mathf.Clamp钳制数值、Mathf.Lerp线性插值等极其常用的静态方法。String和StringBuilder处理文本。简单拼接用或$””但如果在循环中进行大量字符串拼接使用StringBuilder性能会好得多。DateTime和TimeSpan处理日期和时间常用于记录游戏时间、计时器等。5.3 单例模式Singleton的简单应用在我们的例子中RandomCubeGenerator脚本挂载在一个GameObject上。如果整个游戏只需要一个这样的管理器我们可以将其实现为一个简单的单例方便其他脚本访问。public class RandomCubeGenerator : MonoBehaviour { public static RandomCubeGenerator Instance; // 静态实例 public GameObject cubePrefab; private ListGameObject cubeList new ListGameObject(); void Awake() { // 简单的单例实现 if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 可选跨场景不销毁 } else { Destroy(gameObject); } } // ... 其他方法保持不变 // 现在其他脚本可以这样调用RandomCubeGenerator.Instance.GenerateRandomCubes(5); }这样在任何脚本中你都可以通过RandomCubeGenerator.Instance来访问这个生成器的方法和属性。这是一种常用的全局访问点设计模式但需谨慎使用避免造成代码过度耦合。6. 常见问题、调试技巧与性能考量6.1 常见问题排查表问题现象可能原因解决方案运行后没有任何立方体生成1.cubePrefab未在Inspector中赋值。2. 脚本未挂载到激活的GameObject上。3. 生成代码未在Start或Awake中调用。1. 检查Inspector面板确保预制体已拖拽赋值。2. 检查Hierarchy确保挂载脚本的对象是激活的对勾显示。3. 确保GenerateRandomCubes方法在Start或Awake中被调用。立方体全部堆在原点(0,0,0)Instantiate方法只传入了预制体未传入位置参数或位置参数计算错误始终为Vector3.zero。检查Instantiate的第二个参数。确保randomPosition的计算逻辑正确且rangeX/Y/Z不为0。生成立方体时程序卡死或无响应可能陷入了死循环例如在GenerateRandomCubesWithoutOverlap中minDistance设置过大或空间过小导致永远找不到合适位置。为循环添加最大尝试次数如代码中的attempts 100并在失败时给出警告。合理设置minDistance和生成范围。按下空格键后立方体视觉消失但日志报错只执行了Destroy未执行cubeList.Clear()。列表中的引用变成“空引用”后续访问可能报错。确保在销毁所有对象后调用cubeList.Clear()。预制体修改后已生成的实例没变化修改了场景中的实例而非Project中的预制体资源。如需批量修改应修改预制体资源。如需单独修改某个实例直接修改即可。6.2 调试Debug技巧使用Debug.Log这是你最好的朋友。像我们在代码中做的那样在关键步骤如生成成功、销毁完成打印信息能让你清晰地了解程序执行流程。使用Debug.LogError和Debug.LogWarning分别用于输出错误红色和警告黄色。错误用于必须中断流程的问题如预制体未赋值警告用于需要注意但可能不影响运行的情况如未找到合适位置。在IDE中设置断点在Visual Studio等IDE中点击代码行号左侧灰色区域可以设置断点。运行Unity并触发代码时程序会在此暂停你可以查看所有变量的当前值逐行执行这是排查复杂逻辑问题的利器。使用Unity的Inspector将临时变量设为public或使用[SerializeField]属性可以在运行时实时观察它们的值对于调试随机数、位置等非常直观。6.3 性能考量与优化思路生成10个立方体对性能毫无压力。但如果数量变成1000个、10000个呢绘制调用Draw Call每个使用不同材质的物体会产生一个Draw Call。如果10000个立方体使用同一个材质Unity会进行动态合批大幅减少Draw Call。确保你的立方体预制体使用的是同一个材质球。对象池Object Pooling对于需要频繁创建和销毁的对象如子弹、敌人使用Instantiate和Destroy开销很大。对象池的核心思想是预先创建一批对象并禁用需要时激活并放到指定位置用完后再禁用放回池中而不是真正销毁。这能有效避免内存碎片和GC垃圾回收压力。List或Queue是实现对象池的常用数据结构。脚本效率在Update中避免进行复杂的计算或查找如GameObject.Find。我们的销毁操作由按键触发没问题。但如果每帧都要遍历成百上千个对象就需要考虑更高效的数据结构如空间分区算法或优化遍历条件。今天这个项目就像你学会了用积木块C#基础搭出了一座小桥集合与类的应用并成功让小车立方体开了过去。它验证了“代码能创造世界”的基本逻辑。当你掌握了用List管理10个立方体未来管理成百上千的游戏单位、道具、技能效果其核心思想都是一脉相承的。试着修改代码让立方体生成在一个球体空间内或者让它们生成后缓缓上升这能立刻巩固你对Vector3和循环的理解。编程的学习就是在这样一个个小目标的实现与扩展中逐渐积累起掌控复杂系统的能力。