Unity随机数进阶:超越Random.Range的加权随机、几何分布与状态管理实战

📅 2026/7/26 20:57:55
Unity随机数进阶:超越Random.Range的加权随机、几何分布与状态管理实战
1. 项目概述从Random.Range的舒适区走出来如果你在Unity里做过任何需要随机性的功能比如刷怪、抽卡、道具掉落或者地图生成那你对Random.Range这个函数一定再熟悉不过了。它就像工具箱里那把最趁手的螺丝刀简单直接拿起来就能用。但问题是很多开发者包括一些有几年经验的朋友工具箱里好像就只有这一把螺丝刀。需要拧螺丝用它需要撬东西也用它甚至需要敲钉子的时候还是试图用它去敲——结果就是要么功能实现得磕磕绊绊要么程序运行起来总有些难以捉摸的“小毛病”。Random.Range确实好用但它只是Unity随机数系统UnityEngine.Random类的冰山一角。这个类底下藏着不少更强大、更精细的工具能帮你解决Random.Range搞不定的问题或者让你的随机逻辑更健壮、更可控。今天我们不聊那些深奥的数学原理就从一个干了十多年游戏开发的老兵视角分享几个我踩过坑、救过急的实战技巧。这些技巧能帮你处理更复杂的随机需求比如“带权重的随机选择”、“可重复的随机序列”、“在球面上均匀撒点”以及如何避免那些让随机数“不随机”的常见陷阱。别再只用Random.Range了是时候升级你的随机工具箱了。2. 核心需求解析为什么Random.Range不够用在深入技巧之前我们得先搞清楚光靠Random.Range会遇到哪些天花板。理解了这些痛点你才能明白后面那些技巧的价值所在。2.1 需求一非均匀概率分布加权随机这是最经典的需求。Random.Range(0, 3)给你0、1、2三个整数每个概率是均等的33.3%。但现实需求往往是掉落普通道具的概率是70%稀有道具25%史诗道具5%。你用Random.Range硬写也不是不行但代码会变得又臭又长而且不易维护和调整。// 笨拙的写法逻辑分散不易读 int randomValue Random.Range(0, 100); if (randomValue 70) { // 普通道具 } else if (randomValue 95) // 70 25 95 { // 稀有道具 } else { // 史诗道具 }当道具类型增加到10种、20种时这段代码会变成一场维护噩梦。我们需要一种更优雅、声明式的权重配置和选择方法。2.2 需求二可重复的随机序列游戏开发中经常需要“确定性随机”。比如你用随机种子生成了一张地图希望玩家每次用同一个“地图种子”都能得到完全一样的地图布局。或者在联网游戏中为了保持所有客户端状态同步你们需要一套生成完全相同随机序列的机制。标准的Random.Range依赖于全局的随机状态一旦被调用状态就改变了无法简单地“回退”或“重现”某个特定序列。你需要能够创建独立的、自带状态的随机数生成器RNG实例。2.3 需求三特定几何空间内的随机Random.Range只能给你一条直线一维或一个平面矩形区域二维通过组合上的随机点。但很多需求是三维的、非矩形的。比如在一个圆形或环形区域内随机生成敌人。在一个球体表面或内部均匀地生成星星。沿着一条曲线如贝塞尔曲线随机分布物体。 这些都需要额外的数学处理直接Random.Range出来的点分布是不均匀的例如在圆内直接随机角度和半径点会向中心聚集。2.4 需求四随机序列的“洗牌”与采样从一个集合如牌组、怪物列表、任务池中随机抽取若干个不重复的元素。用Random.Range去索引并移除元素在集合较大时效率会越来越低因为要移动数组元素。我们需要高效的“洗牌”算法如Fisher-Yates和“无放回随机采样”算法。2.5 需求五随机状态的隔离与管理Unity的Random类是静态类其状态种子、内部迭代器是全局共享的。这带来了一个隐藏风险A系统调用了Random.Range改变了全局随机状态可能导致B系统接下来生成的随机数偏离预期。在大型项目中多个模块AI决策、特效播放、 loot生成如果都依赖全局随机会形成难以调试的耦合和不确定性。我们需要将随机状态“局部化”。3. 实战技巧一实现优雅的加权随机选择加权随机或者说按概率分布随机是游戏设计的基石。上面提到了笨拙的if-else链我们来看看更专业的做法。3.1 方法一别名采样法Alias Method—— 适用于大量选项当你的选项非常多比如成百上千个并且需要频繁抽样时别名采样法是时间复杂度O(1)的王者。它的原理有点复杂但思想是巧妙的将每个选项的概率“拆分”和“组合”最终构建一个结构使得每次抽样只需要生成两个随机数并进行一次数组查找。对于大多数日常游戏开发我们可能不需要自己实现完整的别名采样。但要知道有这种高效方案存在对于道具库极其庞大的游戏如某些ARPG或抽卡游戏在性能优化阶段值得考虑。Unity本身没有内置但社区有优秀的实现库。3.2 方法二累计概率区间法 —— 最直观实用的方法这是最常用、也最容易理解的方法。我们为每个选项计算一个累计概率的“右边界”然后生成一个0到总权重之间的随机数看它落在哪个区间。using System; using System.Collections.Generic; using UnityEngine; public class WeightedRandomSelectorT { [System.Serializable] public struct Item { public T value; public float weight; } private ListItem _items; private float _totalWeight; private bool _isDirty; // 标记权重是否被修改需要重新计算 public WeightedRandomSelector() { _items new ListItem(); _totalWeight 0f; _isDirty true; } public void Add(T value, float weight) { if (weight 0) { Debug.LogWarning($添加的项权重必须大于0当前权重{weight}。该项将被忽略。); return; } _items.Add(new Item { value value, weight weight }); _isDirty true; } public T GetRandom() { if (_items.Count 0) { throw new InvalidOperationException(随机选择器中没有添加任何项。); } if (_isDirty) { CalculateTotalWeight(); } float randomPoint UnityEngine.Random.Range(0f, _totalWeight); float cumulativeWeight 0f; // 注意这里使用for循环比foreach稍快且在Unity中更常见 for (int i 0; i _items.Count; i) { cumulativeWeight _items[i].weight; if (randomPoint cumulativeWeight) { return _items[i].value; } } // 理论上不会走到这里除非浮点数精度问题。兜底返回最后一项。 return _items[_items.Count - 1].value; } private void CalculateTotalWeight() { _totalWeight 0f; for (int i 0; i _items.Count; i) { _totalWeight _items[i].weight; } _isDirty false; } }使用示例与心得// 初始化一个掉落物选择器 var lootSelector new WeightedRandomSelectorstring(); lootSelector.Add(普通药水, 70f); lootSelector.Add(稀有戒指, 25f); lootSelector.Add(史诗武器, 5f); // 模拟10000次掉落验证概率分布 Dictionarystring, int countDict new Dictionarystring, int(); int trials 10000; for (int i 0; i trials; i) { string loot lootSelector.GetRandom(); if (countDict.ContainsKey(loot)) countDict[loot]; else countDict[loot] 1; } foreach (var kvp in countDict) { Debug.Log(${kvp.Key}: 出现次数 {kvp.Value}, 实际概率 {(float)kvp.Value / trials:P2}); } // 输出应接近普通药水: ~70%, 稀有戒指: ~25%, 史诗武器: ~5%注意浮点数精度问题。当权重值非常小或总权重非常大时浮点数的累加可能产生微小误差。上面的兜底逻辑就是为此准备的。对于要求极端精确的场景如涉及真金白银的抽卡可以考虑使用整数权重如 7000 2500 500或者使用高精度的decimal类型进行计算但Random.Range不支持decimal需要转换。实操心得我习惯将这类选择器设计成可序列化的这样就能在Inspector窗口中直观地配置权重对于策划调整数值非常友好。你可以将ListItem _items改为public或者使用[SerializeField] private ListItem items;再配合一个自定义Editor脚本就能在编辑器里拖拽配置了。4. 实战技巧二创建可预测的随机数生成器全局的UnityEngine.Random就像一块公用的黑板谁都可以上去写写画画你无法回溯之前写了什么。要获得可预测的、独立的随机序列我们需要自己的“私人笔记本”——即System.Random类的实例。4.1 使用System.Random获得独立状态System.Random是 .NET 框架中的伪随机数生成器类。你可以创建它的多个实例每个实例都有自己的内部状态互不干扰。using System; // 注意是System不是UnityEngine public class DeterministicMapGenerator { private System.Random _rng; public DeterministicMapGenerator(int seed) { // 用指定的种子初始化随机数生成器 _rng new System.Random(seed); Debug.Log($地图生成器已初始化种子{seed}); } public int NextInt(int minInclusive, int maxExclusive) { // System.Random.Next(min, max) 的max是独占的与Unity的Random.Range(int, int)行为一致 return _rng.Next(minInclusive, maxExclusive); } public float NextFloat() { // System.Random.NextDouble() 返回 [0.0, 1.0) 的双精度浮点数 return (float)_rng.NextDouble(); } public float NextFloat(float minInclusive, float maxInclusive) { // 将其映射到指定范围。注意NextDouble()是[0,1)要模拟Unity的[max] inclusive需要一点技巧。 // 一个简单近似是使用 maxInclusive - minInclusive 的微小容差。 // 更严谨的做法是使用 NextDouble() * (maxInclusive - minInclusive) minInclusive; // 但由于浮点精度maxInclusive可能极难达到。通常游戏逻辑可以接受[max] exclusive。 float range maxInclusive - minInclusive; return (float)_rng.NextDouble() * range minInclusive; } }关键区别与注意事项特性UnityEngine.RandomSystem.Random状态全局静态共享状态实例化每个对象独立状态种子设置Random.InitState(seed)影响全局通过构造函数new Random(seed)设置实例种子整数范围Range(int, int)max exclusiveNext(int, int)max exclusive浮点数范围Range(float, float)max inclusive需手动计算NextDouble()返回 [0.0, 1.0)性能经过优化通常较快通常也很快但实例化有开销线程安全非线程安全非线程安全但实例隔离可在不同线程用不同实例重要提示UnityEngine.Random.Range(float, float)的第二个参数是包含inclusive的而System.Random.NextDouble()返回的是右开区间[0.0, 1.0)。这意味着你无法通过简单的线性变换得到一个严格包含上限的浮点数。在大多数游戏逻辑中比如随机位置、随机伤害[min, max)的区间已经足够因为恰好抽到上限值的概率在数学上是0。如果业务逻辑必须包含上限可以考虑生成一个[0, 1]的整数随机数或者接受一个极其接近上限的值。4.2 应用场景地图生成与网络同步场景一可分享的地图种子public class ProceduralWorld { private System.Random _terrainRng; private System.Random _decorRng; public void GenerateWorld(string seedString) { // 将字符串种子转换为整数。使用GetHashCode相同字符串得到相同整数。 int seed seedString.GetHashCode(); _terrainRng new System.Random(seed); // 可以用派生种子来生成不同的但确定的序列避免干扰。 _decorRng new System.Random(seed 1); GenerateTerrain(_terrainRng); PlaceTreesAndRocks(_decorRng); // ... 玩家输入同一个种子得到的世界一模一样 } }场景二网络游戏的确定性逻辑在锁步同步的RTS或MOBA游戏中所有客户端必须保持完全一致的随机序列以确保公平性。通常由主机或服务器权威生成随机种子或直接广播随机事件结果。客户端可以使用相同的种子初始化自己的System.Random实例来重现非权威的视觉效果如伤害飘字、粒子效果但核心逻辑如暴击判定的随机数必须来自权威源。实操心得我强烈建议在项目早期就规划好随机数的使用策略。将核心的、影响游戏结果的随机逻辑如战斗计算、 loot生成与纯视觉的随机效果如屏幕抖动、粒子颜色分离开。核心逻辑使用System.Random实例并妥善管理种子视觉特效可以放心使用UnityEngine.Random。这样既能保证确定性又能享受Unity全局随机的便利。5. 实战技巧三在特定几何空间内生成随机点直接组合Random.Range来生成随机点在非矩形区域会导致分布不均。下面介绍几种常见几何空间的正确生成方法。5.1 在圆内均匀随机错误的方法是随机一个半径r和角度θ然后转换成坐标(r*cosθ, r*sinθ)。这样生成的点会在圆心处更密集因为面积元dA r dr dθ半径小的环面积小但点密度相同所以圆心处点的实际面密度更高。正确的方法是先随机半径的平方根以保证点在圆内均匀分布。public static Vector2 RandomPointInCircle(float radius) { // 1. 随机角度 float angle UnityEngine.Random.Range(0f, Mathf.PI * 2f); // 2. 随机半径为了均匀分布需要对半径的平方进行均匀随机再开方。 float r Mathf.Sqrt(UnityEngine.Random.Range(0f, 1f)) * radius; float x Mathf.Cos(angle) * r; float y Mathf.Sin(angle) * r; return new Vector2(x, y); } // 三维版本在XZ平面上的圆盘 public static Vector3 RandomPointInCircleXZ(float radius) { float angle UnityEngine.Random.Range(0f, Mathf.PI * 2f); float r Mathf.Sqrt(UnityEngine.Random.Range(0f, 1f)) * radius; float x Mathf.Cos(angle) * r; float z Mathf.Sin(angle) * r; return new Vector3(x, 0f, z); }5.2 在球面上均匀随机随机方向生成一个随机的、均匀分布在球面上的单位向量常用于发射子弹、随机初始速度等。 错误的方法是随机经度和纬度这会在两极产生聚集。正确的方法是使用正态分布或拒绝采样法。最常用的是Marsaglia的“正态分布”方法但Unity提供了一个更简单且正确的方法在一个单位立方体内随机取点然后归一化。public static Vector3 RandomOnUnitSphere() { // 方法一使用Unity内置方法最简单 // return UnityEngine.Random.onUnitSphere; // 方法二手动实现理解原理 Vector3 point; do { // 在[-1,1]的立方体内随机取点 point new Vector3( UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.RandomRange(-1f, 1f), UnityEngine.Random.Range(-1f, 1f) ); } while (point.sqrMagnitude 1f || point.sqrMagnitude 0.0001f); // 拒绝在单位球外的点也避免零向量 return point.normalized; // 归一化得到球面上的点 }5.3 在球体内均匀随机类似于圆内在球体内均匀随机也需要对半径的立方根进行处理。public static Vector3 RandomPointInSphere(float radius) { // 1. 随机一个球面上的方向 Vector3 direction UnityEngine.Random.onUnitSphere; // 2. 随机半径为了均匀分布需要对半径的立方进行均匀随机再开立方。 float r Mathf.Pow(UnityEngine.Random.Range(0f, 1f), 1f / 3f) * radius; return direction * r; }实操心得这些几何随机函数非常常用我建议你将其封装到一个静态工具类中比如RandomGeometry。在需要的地方直接调用避免每次都要重新推导或查找公式。对于性能极其敏感的场景如每帧生成大量粒子可以预先计算好Mathf.PI * 2f等常量并考虑使用Unity.Mathematics中的Random结构体它在Burst编译下性能更高。6. 实战技巧四高效随机采样与洗牌算法从一个集合中随机抽取元素有放回和无放回是两种基本操作。6.1 无放回随机采样Fisher-Yates洗牌算法如果你有一个数组想把它随机打乱顺序比如洗牌或者想从中依次取出若干个不重复的随机元素Fisher-Yates算法是标准且高效的选择。它的时间复杂度是O(n)。public static class RandomUtility { // Fisher-Yates洗牌直接打乱原数组 public static void ShuffleT(this IListT list) { int n list.Count; for (int i n - 1; i 0; i--) { // 在[0, i]区间内随机选择一个索引j int j UnityEngine.Random.Range(0, i 1); // 交换元素list[i]和list[j] T temp list[i]; list[i] list[j]; list[j] temp; } } // 从列表中随机抽取k个不重复的元素不修改原列表 public static ListT SampleWithoutReplacementT(this IListT source, int k) { if (k source.Count) { throw new ArgumentException($采样数量k({k})不能大于源列表长度({source.Count})。); } // 创建一个副本用于内部洗牌操作 ListT copy new ListT(source); ListT result new ListT(k); int n copy.Count; for (int i 0; i k; i) { // 每次在剩余部分随机选一个 int randomIndex UnityEngine.Random.Range(i, n); result.Add(copy[randomIndex]); // 将选中的元素交换到当前位置i确保不会被再次选中 T temp copy[randomIndex]; copy[randomIndex] copy[i]; copy[i] temp; } return result; } }使用示例// 洗牌示例 ListCard deck GetDeck(); deck.Shuffle(); // 牌组现在已被打乱 // 无放回采样示例 ListMonster monsterPool GetAllMonsters(); ListMonster selectedMonsters monsterPool.SampleWithoutReplacement(5); // 随机选5种不同的怪物 foreach (var monster in selectedMonsters) { SpawnMonster(monster); }6.2 有放回随机采样这就是最简单的用Random.Range做索引。但当采样次数非常多时需要注意性能。如果列表长度固定可以预先计算一次list.Count存入局部变量避免在循环中反复查询属性。// 高效的有放回采样 int count myList.Count; // 预先计算 for (int i 0; i 1000; i) { int index UnityEngine.Random.Range(0, count); var item myList[index]; // ... 处理item }实操心得Fisher-Yates洗牌算法的一个关键点是循环方向。上面例子是从后往前i n-1; i 0; i--也可以从前往后。重要的是随机索引j的范围是[0, i]或[i, n-1]确保每个位置都有机会被交换。这个算法是“原地”的会修改原数组。如果你需要保留原列表记得先创建一个副本。7. 实战技巧五管理随机状态避免副作用全局状态是滋生Bug的温床。当你的游戏系统越来越复杂音频系统、特效系统、AI系统、游戏逻辑系统都可能调用UnityEngine.Random它们会相互污染随机数序列。7.1 问题重现随机数序列被意外干扰假设你有一个抽卡系统在抽卡前先播一段随机的闪光特效。特效系统调用Random.Range来随机颜色这改变了全局随机状态。紧接着你的抽卡逻辑也调用Random.Range得到的“随机”结果实际上已经被特效逻辑影响了。在单机游戏中这可能只是让结果变得不可预测但未必是坏事。但在需要确定性重现的场合如录像回放、网络同步这就是灾难。7.2 解决方案隔离与封装策略一为关键系统使用独立的System.Random实例正如技巧二所介绍的将影响游戏核心进程的随机逻辑如伤害计算、 loot判定、AI决策封装到独立的类中并使用System.Random实例。public class CombatSystem { private System.Random _combatRng; private int _seed; public void Initialize(int seed) { _seed seed; _combatRng new System.Random(seed); } public bool TryCriticalHit(float baseRate) { // 使用独立的RNG进行暴击判定 double roll _combatRng.NextDouble(); return roll baseRate; } public void ResetWithSeed(int newSeed) { // 重置随机状态用于录像回放或重新开始 _combatRng new System.Random(newSeed); } }策略二保存与恢复全局随机状态如果某些遗留代码或第三方插件必须使用UnityEngine.Random而你需要在它们执行前后保持你序列的纯净可以保存和恢复状态。UnityEngine.Random.state属性允许你获取和设置完整的内部状态。public class RandomStateScope : IDisposable { private Random.State _previousState; public RandomStateScope(int seed) { // 进入作用域时保存当前状态并初始化新种子 _previousState Random.state; Random.InitState(seed); } public void Dispose() { // 离开作用域时恢复之前的状态 Random.state _previousState; } } // 使用示例 void MyDeterministicFunction() { // 在using块内Random的状态是独立的 using (new RandomStateScope(12345)) { int a Random.Range(0, 100); int b Random.Range(0, 100); // a和b将由种子12345确定性地生成 } // 离开using块后全局Random状态恢复到进入前的样子 int c Random.Range(0, 100); // 不受上面种子影响 }警告频繁地保存和恢复Random.state会有一定的性能开销因为它涉及拷贝一个结构体。不要在每帧或高频循环中使用。它更适合用于隔离一些确定性的、一次性的生成过程。策略三使用Unity.Mathematics.Random面向高性能和ECS如果你在使用DOTS/ECS架构或者对性能有极致要求Unity.Mathematics.Random是一个更好的选择。它是一个值类型struct可以存储在组件中完美契合ECS范式并且可以与Burst编译器协同工作获得极高的运行效率。using Unity.Mathematics; // 在Component或System中 public struct MyRandomComponent : IComponentData { public Random rng; } // 初始化 var myRng new Random(12345); // 使用 int randomInt myRng.NextInt(0, 100); float randomFloat myRng.NextFloat();实操心得在项目初期就建立随机数使用规范。我的团队通常约定所有游戏逻辑Gameplay相关的随机必须使用注入的System.Random实例或Unity.Mathematics.Random。所有视觉效果、音频等不影响核心逻辑的随机可以使用UnityEngine.Random。代码审查时会特别检查对全局Random的滥用。这条规矩帮我们避免了许多难以复现的诡异Bug。8. 常见问题与排查技巧实录即使掌握了上面的技巧在实际开发中还是会遇到各种奇怪的问题。下面是我总结的几个高频“坑点”和解决方法。8.1 问题一随机数看起来“不随机”有规律或聚集现象生成的随机数序列看起来有模式比如连续出现几个相近的数或者奇偶交替。可能原因与排查种子问题如果你在每帧或每次调用时都使用new Random(DateTime.Now.Millisecond)或Random.InitState(Time.frameCount)由于计算机速度很快可能连续几次获取到相同或相近的时间戳导致种子几乎一样生成的序列开头部分自然也相似。解决对于需要唯一性的随机序列使用更可靠的种子源如Guid.NewGuid().GetHashCode()或结合时间与硬件信息。范围太小如果你在很小的范围内比如Random.Range(0, 2)生成大量随机数出现连续多次相同结果在统计学上是正常的但人眼会觉得“不随机”。解决理解这是概率的正常体现。如果需要减少这种“连续感”可以引入一些简单的平滑逻辑比如“如果连续3次出现1则强制下一次为0”但这会破坏真正的随机性慎用。使用了错误的分布如前面提到的在圆内直接随机半径会导致点向中心聚集这不是随机数生成器的问题而是数学方法的问题。解决回顾技巧三确保在特定几何空间内使用了正确的采样方法。8.2 问题二随机结果在编辑器模式和打包后不一致现象在Unity编辑器中运行正常但打包成exe后随机生成的地图、敌人布局等完全不同。可能原因与排查种子来源不一致如果你的种子依赖于System.DateTime.Now或Environment.TickCount在编辑器多线程、刷新率不稳定和独立播放器中的执行环境有差异获取到的时间戳可能不同。解决使用确定的种子进行测试。在发布版本中如果需要随机种子可以使用更稳定的源如玩家输入的字符串哈希或从系统获取的经过处理的时间如((int)DateTime.UtcNow.Ticks)。代码执行顺序差异编辑器和打包后某些初始化的Awake/Start/OnEnable执行顺序可能微妙不同。如果随机数生成依赖于这些初始化过程而生成器本身又在Awake中初始化就可能因顺序问题导致第一个随机数不同进而引发蝴蝶效应。解决确保所有依赖随机数生成器的系统其初始化顺序是明确的。可以使用脚本执行顺序设置或通过一个统一的GameManager在Start中按顺序初始化所有子系统。8.3 问题三多线程环境下随机数生成崩溃或异常现象在使用Job System或普通多线程时调用UnityEngine.Random或System.Random实例导致崩溃或数据竞争。可能原因与排查UnityEngine.Random非线程安全绝对不能在子线程中调用UnityEngine.Random的任何方法。解决在主线程生成好随机数再将结果传递给Job。或者在Job内部使用Unity.Mathematics.Random它是线程安全的因为它是值类型每个Job实例拥有自己的副本。共享System.Random实例多个线程同时调用同一个System.Random实例的Next()方法会导致内部状态损坏可能抛出异常或返回错误结果。解决为每个线程创建独立的System.Random实例。注意如果同时创建不要使用相同的种子如都使用DateTime.Now否则它们会产生相同序列。可以使用线程ID或一个全局的原子计数器来生成不同的种子。// 错误示例 private System.Random _sharedRng new System.Random(); void ParallelJob() { Parallel.For(0, 1000, i { int num _sharedRng.Next(); // 多线程竞争危险 }); } // 正确示例 void ParallelJob() { Parallel.For(0, 1000, i { // 每个线程/task有自己的RNG种子由线程ID和主种子混合生成 var localRng new System.Random(Environment.CurrentManagedThreadId ^ _mainSeed); int num localRng.Next(); }); }8.4 随机数使用自查清单在提交代码前可以快速过一遍这个清单[ ]确定性需求这个功能需要录像回放或网络同步吗如果是必须使用System.Random或Unity.Mathematics.Random并管理好种子。[ ]全局状态污染这个随机调用会影响到其他模块吗考虑将其隔离。[ ]几何分布我需要在圆、球体、球面上生成点吗使用了正确的公式吗[ ]性能需要在循环中生成大量随机数吗考虑使用更高效的库如Unity.Mathematics或预先生成数组。[ ]线程安全这段代码会在子线程或Job中运行吗绝对不能使用UnityEngine.Random。[ ]范围检查Random.Range(int, int)和Random.Range(float, float)的第二个参数含义不同exclusive vs inclusive我用对了吗最后关于随机数我最深的一点体会是“真随机”在计算机中是不存在的我们用的都是“伪随机”。但这恰恰是游戏开发所需要的——可控、可重现、可调试的随机。把UnityEngine.Random当成一把万能钥匙迟早会碰到打不开的锁。花点时间了解并善用System.Random、加权随机、几何随机和洗牌算法就像是给你的工具箱添置了撬锁器、扳手和螺丝刀套装。当需求再次来临时你就能从容地选出最合适的那把工具干净利落地解决问题。