Unity 随机数与随机种子
随机数几乎应用于游戏开发的方方面面,例如,随机生成的地图,迷宫,怪物属性等,在Unity中,使用随机数非常方便:
1 // 2 // 摘要: 3 // Return a random integer number between min [inclusive] and max [exclusive] (Read 4 // Only). 5 // 6 // 参数: 7 // min: 8 // 9 // max: 10 public static int Range(int min, int max);
1 // 2 // 摘要: 3 // Return a random float number between min [inclusive] and max [inclusive] (Read 4 // Only). 5 // 6 // 参数: 7 // min: 8 // 9 // max: 10 [FreeFunction] 11 public static float Range(float min, float max);
1 // 2 // 摘要: 3 // Returns a random number between 0.0 [inclusive] and 1.0 [inclusive] (Read Only). 4 public static float value { get; }
正常情况下使用以上三种完全够用了,注意整型的随机是左开右闭的。当然了,你也可以使用System.Random中的方法来随机,可以构造出类似于Unity中的扩展方法:
1 static public int Range(this System.Random random, int min, int max) 2 { 3 return random.Next(min, max); 4 } 5 6 static public float Range(this System.Random random, float min, float max) 7 { 8 var r = random.NextDouble(); 9 return (float)(r * (max - min) + min); 10 }
值得注意的是,System.Random需要实例化才能随机,而UnityEngine.Random是直接使用。
但很多时候,我们除了需要随机数之外,可能会有保留上次随机结果的需求,换句话说,从某一时刻起,我们希望每次都能随机出和上次相同的结果,这个时候就该随机种子出场了。
举例来说,当玩家需要重新进入一次他以前随机出来过的一个迷宫地图进行二次创作,又比如,我们在开发过程中,某个随机单位出现了Bug,但如果下次又没法产生之前随机结果的话,那么就会出现十分头疼的状况了,这样很可能永远有个难以排查的潜在Bug一直在开发过程中而又难以再次复现。
所以,强烈建议,只要是做相对比较复杂的随机行为,我们最好利用随机种子来执行随机。
当然了,你说我将所有随机的数据结果序列化保存到本地,那也没问题,但相比随机种子只需要保存一个整型数据来说,哪种方式更可取显而易见。这样也可以大大减少游戏保存的数据容量。
说了这么半天,什么是随机种子呢?
顾名思义,一个种子对应着一个结果,随机种子对应的就是一个唯一的随机结果。
1 // 2 // 摘要: 3 // Initializes the random number generator state with a seed. 4 // 5 // 参数: 6 // seed: 7 // Seed used to initialize the random number generator. 8 [NativeMethod("SetSeed")] 9 [StaticAccessor("GetScriptingRand()", StaticAccessorType.Dot)] 10 public static void InitState(int seed);
上面的方法中,参数seed就是传入的随机种子,如果在脚本的一开始执行调用了此方法,那么只有当此次随机种子与上次的种子不相同时,才能随机出不同的随机结果,否则随机的结果总是一样的。
注意,这里指的随机结果是指的所有的随机结果,是一个随机数表,它从本质上改变的是整个UnityEngine.Random类的所有随机方法执行的结果,包括最开始列举的三种中的任意一种。
下面做一个测试就很容易理解了:
1 using UnityEngine; 2 3 public class RanTest : MonoBehaviour 4 { 5 public bool bDebug; 6 //System.Random random; 7 void Start() 8 { 9 //random = new System.Random((int)System.DateTime.Now.Ticks); 10 //string s = ""; 11 //for(int i = 0; i < 233; i++) 12 //{ 13 // s += random.Range(0, 10) + ","; 14 //} 15 //Debug.Log(s); 16 17 int seed = (int)System.DateTime.Now.Ticks; 18 if (bDebug) 19 { 20 seed = PlayerPrefs.GetInt("Seed"); 21 } 22 else 23 { 24 PlayerPrefs.SetInt("Seed", (int)System.DateTime.Now.Ticks); 25 } 26 Random.InitState(seed); 27 string s = ""; 28 for (int i = 0; i < 32; i++) 29 { 30 s += Random.Range(0, 10) + ","; 31 } 32 Debug.Log(s); 33 } 34 }
比如我开了一个Debug模式,如果勾选,则随机种子是从上次保存的数据中读取,随机出来的结果永远是一样的,因为我并没有对保存的数据种子进行任何的更改。
结果如下:
我们发现每次的随机数都一样,因为它们都源于同一个随机种子,无论之后再随机多少次,结果都是这个随机数序列,这个种子对应的结果已经被计算机固定了,除非种子更改,不然随机结果不会变。
当我关闭Debug模式时,正常的随机种子时刻都不会一样,这里用到了System.DateTime.Now.Ticks来保证得到和上次的种子绝不相同的整型,也可以使用guid等。
每次在本地备份一次上一次随机种子的记录,以便随时可以再现上一次随机的结果,只需要轻松勾选Debug即可:
例如,我在第三次时发现了随机产生的其他Bug,这样我只用启动Debug模式反复分析几遍后把复现的隐藏Bug修改结束后再回到正常模式产生新的随机数就好。
另外,我们也可以利用System.Random类的构造方法来实现同样的随机效果,一个构造方法带有随机种子的参数,一个则没有,原理和上面是一样的:
1 public Random(); 2 public Random(int Seed);
这个时候改变的就是System.Random类的随机方法,而非UnityEngine.Random的随机方法。
所以一开始就决定好整个开发过程中用的随机类也不容忽视,建议要么就全部用Unity中的,要么就全部用System中的,这样调整起来自然更得心应手事半功倍。
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=12ri51jwydxyj