Unity 可重復隨機數
出處
https://blogs.unity3d.com/cn/2015/01/07/a-primer-on-repeatable-random-numbers/ (英文原版)
http://www.manew.com/thread-37144-1-1.html
不管創建什麽樣的程序,幾乎都離不開隨機數.如果您想多次生成同樣的結果,這就需要隨機數是可重復的。
在本片文章中我們將介紹使用關卡或世界的生成作為示例,但其中的原理也適用於許多其它內容,例如程序紋理、模型、音樂等等。然而,這並不適用於一些具有嚴格要求的應用程序,比如加密。
為什麽想要多次產生同樣的結果呢?
- 為了能夠再次訪問同樣的關卡或世界。例如:通過一個特定的種子來創建一個確定的level/world。如果重復使用相同的種子,就可以重復創建相同的level/world。比如“我的世界”Minecraft就是用此原理。
- 為了動態生成持久的世界。如果要隨著玩家四處移動來動態生成世界,您可能希望玩家再次訪問時的坐標與原始狀態保持一致(就像《Minecraft(我的世界)》、《No Man‘s Sky(無人深空)》等遊戲中一樣),而不是毫無邏輯每次都不同。
- 所有玩家都是同一個世界。可能您希望遊戲中的世界對所有玩家來說都是一樣的,就好像它不是程序生成的。《No Man‘s Sky(無人深空)》中 就有這樣的例子 。這與上述提到的重復訪問相同的關卡或場景基本相同,不同的是重復訪問始終使用同一個種子。
我們多次提到了“種子”這個詞。種子可以是數值、文本字符串或其它數據類型,它用來作為輸入參數從而得到一個隨機的輸出結果。種子的特點就是相同的種子總是產生相同的輸出結果,而一點細微的變化就能導致結果千差萬別。
在本文中,我們將細述兩種生成隨機數的方法——隨機數生成器和隨機哈希(Hash)函數,以及選擇使用它們的原因。據我所知這些內容並不常見,而且其它地方也沒有類似資源,所以我在這裏寫下來與大家分享。
隨機數生成器
最常見的生成隨機數的方法就是通過隨機數生成器(簡稱RNG)。許多編程語言都包含RNG類或函數,並且名字中帶有“random”,所以顯而易見這是開始使用隨機數的首選方法。
隨機數生成器按照初始種子來生成一組隨機數。在面向對象語言中,隨機數生成器通常是一個使用種子初始化的對象。然後重復調用該對象中的某個方法來生成隨機數。
在C#中生成隨機數的代碼如下:
1 2 3 4 5 6 |
Random randomSequence = new Random ( 12345 ) ;
int randomNumber 1 = randomSequence.Next ( ) ;
int randomNumber 2 = randomSequence.Next ( ) ;
int randomNumber 3 = randomSequence.Next ( ) ;
|
這種情況下我們會得到一個0~2147483647(int類型最大值)之間的隨機整數,我們也可以指定隨機整數的範圍,或者指定生成0~1之間的浮點數等等都是不費吹灰之力的。實現該功能的常見方法見下文。
下圖是C#中的Random類經種子0初始化後首次生成的65535個隨機數。每個隨機數都以一個像素來表示,亮度在0(黑)與1(白)之間。
這裏很重要的一點要理解,在沒有獲取第一個和第二個隨機數之前是無法獲取第三個隨機數的。這不僅僅是它實現機制的一種表現。本質上,RNG生成的每個隨機數都用作下一次生成計算的一部分。下面我們來說說隨機序列。
這意味著,如果您想要的是一串逐個生成的隨機數,RNG完全可以滿足您,但是如果想獲取某個特定的隨機數(比如說,隨機數序列中的第26個數),那就歇菜了。不過呢,您還是可以調用Next()函數26次然後取最後一次的結果,當然這只是說笑。
為什麽要獲取序列中某個特定的隨機數呢?
如果同時生成所有內容,您可能不需要獲取序列中某個特定的隨機數,至少我認為沒有必要。然而,如果是一點點動態生成,那就有必要了。
例如,假設您的世界中有三個區域:A、B和C。玩家一開始在A區,所以使用100個隨機數來生成區域A。然後玩家繼續使用另外100個不同的數來生成區域B。與此同時之前生成的A區域會被銷毀並釋放內存。同樣還是使用另外100個隨機數來生成C並釋放B。
然而,如果玩家現在要回到區域B,那應該按照首次生成的100個隨機數來生成區域B,從而使得該區域與原來一致。
可使用隨機數生成器指定不同的種子來實現?
答案是不行!這是一個關於RNG非常常見的誤解。事實上,事實上,盡管同一序列中的不同數字間相關性是完全隨機的,但不同序列中相同索引的數字間卻是相關的
所以如果從100個序列中分別取出第一個數,它們之間並不是隨機的,第10個、第100個或是第1000個也同樣相幹。
關於這點有人會表示懷疑,沒有關系。您可以看看 Stack Overflow上關於RNG生成內容的討論,如果您覺得更可靠。為了讓本文更有趣且實用,我們還是來做些實驗看看結果。
我們以相同序列中生成的隨機數作為參考,然後與在種子取0~65535所生成的65536個序列中,分別取各序列的第一個數得到的序列數進行比較。
盡管圖像更像是均勻分布,但它並不是隨機的。實際上,我已經通過一個純線性函數來比較並展示了輸出,顯而易見的是,使用種子序列生成的隨機數並沒有比直接使用線性函數更好。
這樣就夠隨機了嗎?夠好了嗎?
關於這點,通過更好的方法來衡量隨機性是個不錯的選擇,因為肉眼並不是太可靠。為何?難道這個結果看起來還不夠隨機嗎?
沒錯,我們最終的目標是讓結果充分隨機。但是根據使用方式不同生成的隨機數結果也各不相同。您的生成算法可能會以各種各樣的方式來生成隨機值,將最終的值放在一個簡單的序列中查看時就會發現其中隱藏的模式。
另外一種查看隨機輸出值的方法就是創建2D坐標系,並將隨機數成對繪制在坐標系中最終生成圖像。位於像素點的隨機值越多,則該點的亮度越高。
下面我們來看看,同一序列中的隨機數分布以及不同序列中各取一個的隨機數分布的坐標圖。還附上了線性函數圖來比較。
您可能感到驚訝,使用不同種子生成的不同序列分別取一個值創建的坐標圖,這些坐標都被描繪在細線上而不是任何接近均勻的分布。正如上述說的,與線性函數非常類似。
如果您要用隨機數來創建坐標,用來在地形上布置樹木。現在您所有的樹木都被布置在一條直線上而留出了大量空地。
我們可以得出結論,就是隨機數生成器只在您不需要以特定順序來訪問隨機值時有用。如果您需要,那您可能想仔細了解下面的隨機哈希函數。
隨機哈希函數
I一般說來,哈希函數可以是任意函數,只要它可以用來將任意範圍的數據映射為固定範圍,並且輸入參數一點微小的改變就能導致輸出結果千差萬別。
對於程序生成,典型的用例就是提供一個或多個整型數據作為輸入,然後得到一個隨機數作為輸出。例如,比較大的世界可以一次只生成一部分,典型的需求就是得到一個與輸入向量相關的隨機數(例如世界中的坐標),如果輸入相同則該隨機數保持不變。與隨機數生成器(RNG)不同,它是沒有順序的——您可以以任意您喜歡的順序來獲取隨機數。
在C#中的示例代碼如下(註意您可以按任意順序來獲取隨機數):
1 2 3 4 5 6 |
RandomHash randomHashObject = new RandomHash ( 12345 ) ;
int randomNumber 2 = randomHashObject.GetHash ( 2 ) ;
int randomNumber 3 = randomHashObject.GetHash ( 3 ) ;
int randomNumber 1 = randomHashObject.GetHash ( 1 ) ;
|
The hash function may also take multiple inputs, which mean you can get a random number for a given 2D or 3D coordinate:
哈希函數也可以接收多個輸入,也就是說您可以按照給定的2D或3D坐標來獲取隨機數:
1 2 3 4 5 6 7 8 |
RandomHash randomHashObject = new RandomHash ( 12345 ) ;
randomNumberGrid[ 20 , 40 ] = randomHashObject.GetHash ( 20 , 40 ) ;
randomNumberGrid[ 21 , 40 ] = randomHashObject.GetHash ( 21 , 40 ) ;
randomNumberGrid[ 20 , 41 ] = randomHashObject.GetHash ( 20 , 41 ) ;
randomNumberGrid[ 21 , 41 ] = randomHashObject.GetHash ( 21 , 41 ) ;
|
程序生成隨機數並非哈希函數的典型用法,也並不是所有的哈希函數都適用於程序生成隨機數,因為它們可能不會充分隨機分布,又或者性能開銷過大。
哈希函數的應用之一就是作為數據結構實現的一部分,例如哈希表和字典。這些通常高效但不會充分隨機,因為它們不是為隨機而生而只是使算法更高效。理論上這種方式應該也是隨機的,但實際上,我還沒找到比較它們隨機性的資源,而我測試的結果證明其隨機性非常差(詳情請看附錄C)。
哈希函數另一個應用就是加密。這通常是非常隨機的,但效率很低,因為加密需要的隨機並非只是看上去的隨機。
我們使用程序生成的目標就是創建一個隨機並且高效的哈希函數,也就是說其效率不應低於本來的水平。編程語言中並未內置合適的函數供選擇,而您又需要找到一個用於您的項目,這就是機會。
我已經按照網上的推薦和大量相關知識測試過幾個不同的哈希函數。我從中選擇了如下三者來進行比較。
- PcgHash: 我在Google Groups的論壇上 關於程序內容生成的討論中看到了Adam Smith提供的這個函數。他提供了一些技能建議,自己創建隨機哈希函數不難,他還提供了自己的代碼片段PcgHash作為示例。
- MD5:這可能是大家最熟知的哈希函數。它同樣用於加密也比我們的目標開銷更大(它同樣是密碼級的算法強度,對我們的目標來說,算是殺雞用牛刀了)。首先,我們通常只需返回一個32位的整型值,但MD5返回的是更大的哈希值,大多情況下我們會丟棄多余的位數。不過還是要拿它來作比較。
- xxHash:這是一個高性能非加密形式的哈希函數,正好滿足我們隨機性好且性能好的需求。
- 除了生成噪聲點序列的圖片和坐標圖之外,我還利用隨機性測試網站
ENT –偽隨機數序列測試程序測試過。我在圖像中包含了選擇ENT的統計數據,還有一個我自己想的叫做Diagonals Deviation
的統計數據。後者主要展現坐標圖中對角線上的像素之和,並測量這些和的標準誤差。
下面是以上3種哈希函數的比較結果:
最後PcgHash比較突出,盡管從上面的圖片中看序列中的隨機數噪點非常隨機,坐標圖卻顯示出清晰的模式,也就是說它經不住一些簡單的變換。我據此總結,想實現自己的隨機哈希函數挺難的,還是留給專家來解決吧。
MD5和xxHash似乎可以在隨機性上相互媲美,而其中xxHash要快上約50倍。XxHash還有一個優點就是盡管它不是RNG,但還是有種子的概念,這並非所有哈希函數都具有。可以設置種子對程序生成來說如虎添翼,因為您可以使用不同實體、網格或類似對象的不同屬性來作為不同的種子,然後只用該實體的索引(或網格)坐標作為哈希函數的輸入。關鍵是,使用xxHash,不同種子生成的序列之間也是隨機相關的(不同種子生成的序列之間也是隨機的)(詳情請查看附錄2)。
哈希對程序生成的優化實現
從我對哈希函數的研究中顯而易見的是,雖然與一般哈希函數性能基準相當,這是一個很好的選擇,但至關重要的是,要對它進行優化來滿足程序生成的需求而不是原樣使用哈希函數。
下面是兩點非常重要的優化
- 避免int(整型)和byte(字節型)間的類型轉換。最常用的哈希函數都是以一個byte數組作為輸入然後返回一個整型或一些字節的哈希值。然而,一些高性能的函數會將輸入的byte轉換為int,因為它們內部是操作int。由於最常見的程序生成就是根據一個int輸入返回一個哈希值,所以完全沒有必要轉換到byte。去除對byte的依賴可以增加兩倍性能同時保證輸出完全一致。
- 實現不使用循環只有一個或幾個輸入的方法。最常用的哈希函數都是接收不同長度的數據作為輸入,以數組的形式。這對程序生成也非常有用,但最常用的可能是只有1個、2個或是3個整數作為輸入生成哈希值。以固定長度的整數而不是數組作為輸入來優化函數,就不需再使用循環,這能顯著提高性能(我測試大概是快4~5倍)。我不是底層優化的專家,但這顯著的區別可能是由for循環的隱式分支或需要分配數組導致的。
目前我所推薦的哈希函數就是針對程序生成優化過的xxHash,更多詳情請查看附錄C。(目前我所推薦的哈希函數就是針對程序生成隨機數優化過的xxHash)
您可以在 BitBucket上獲取我寫的xxHash及其它哈希函數. 這是我利用空閑時間寫的屬於自己的東西,非屬於Unity Technologies.
另外我也添加了額外的方法來優化生成指定範圍內的整數或是浮點數,這對程序生成來說也是至關重要的。
註意:在寫這篇文章的時候我只添加了單個整數為輸入的優化到xxHash和MurmurHash3。後續有空我會添加重載函數來優化兩個及三個整數輸入。
哈希函數和RNG相結合
隨機哈希函數和隨機數生成器是可以結合使用的。明智的做法是,使用不同種子的隨機數生成器,但這些種子都是經由哈希函數轉換過的而不是直接使用。
假設有一個很大的迷宮,接近無窮大。其中有個大型網格且每個網格單元也是一個迷宮。隨著玩家在世界中移動,網格單元中的迷宮也要在玩家周圍動態生成。
這種情況下您可能希望每個迷宮每次被訪問時其生成方式都是一樣的,所以就需要隨機數的生成與之前生成的隨機數毫不相幹。
然而,迷宮是一次就全部生成的,所以對一個迷宮來說就不用控制各個隨機數的順序。
這裏提供了理想的方法,就是用隨機散列(哈希)函數根據迷宮網格單元的坐標來創建種子,然後將其作為隨機數生成器的種子來生成隨機數序列從而創建迷宮。
在C#中的代碼示例如下:
01 02 03 04 05 06 07 08 09 10 |
RandomHash randomHashObject = new RandomHash ( 12345 ) ;
int mazeSeed = randomHashObject.GetHash ( cellCoord.x , cellCoord.y ) ; 26
Random randomSequence = new Random ( mazeSeed ) ;
int randomNumber 1 = randomSequence.Next ( ) ;
int randomNumber 2 = randomSequence.Next ( ) ;
int randomNumber 3 = randomSequence.Next ( ) ;
|
結論
如果您要控制查詢隨機數的順序,就使用合適的為程序生成優化過的隨機散列函數(例如xxHash)。
如果您只是想要一串隨機數而不在乎順序,最簡單的辦法就是使用隨機數生成器,例如C#中的System.Random類。為了所有隨機數之間dous
隨機相關的(隨機的),就只生成一個序列(只用一個種子來初始化),或者使用隨機散列函數(如xxHash)處理過的多個種子初始化。
本文提到的隨機數測試的源碼,以及大量的RNG和散列函數的源碼,都可以在 BitBucket上獲取。這是我在空閑時間自己寫的,與Unity Technologies無關。
本文最初是發布在runevision blog ,博客專註於遊戲開發及我空閑時的一些研究。
附錄A:關於連續噪聲的註解
對於某些情況您可能希望查詢連續噪聲值,就是說輸入值相近且輸出值也相近。典型應用就是地形或紋理。
這些需求與本文的討論截然不同。對於連續噪聲,要研究Perlin Noise——或更高級的Simplex Noise.
然而,要知道這些只適用於連續噪聲。查詢連續噪聲函數只是為了得到與其它隨機數無關的隨機數,它生成的結果差強人意,因為這並不是這些算法優化的方向。例如,我發現在整數位置查詢Simplex Noise函數,每隔3個輸入就會返回結果0。
另外,連續噪聲函數通常用浮點數進行計算,它的穩定性及精度都不如原來的高。
附錄B:更多關於種子及輸入的測試結果
多年來我聽到過各種各樣的誤解,我會試著在這裏列出一些。
難道用一個很大的數作為種子不是最好的嗎?
不是,沒有任何證據證明這點。從本文中的測試圖片ke以看出,種子值的大小對輸出結果沒有影響。
隨機數生成器不需要幾個數來“開展工作”嗎?
不用。再次說明,從本文中的測試圖片可以看出,從始至終隨機數序列都是相同的模式(左上角開始一行接著一行)
下面是我測試的65535個隨機數序列中分別取第0個以及第100個生成的圖像。可以看到,兩者的隨機性並沒有多大差別。
就沒有一些RNG,比如Java中的,使用不同種子的隨機數序列隨機性更好嗎?
也許會有一點點優勢,但這遠遠不夠。與C#中的Random類不同,Java中的Random類不適用原來的種子,而是在存儲種子之前打亂了種子的位順序。
不同序列中的隨機數看起來可能有一點隨機,我們可以從測試圖中看到Serial Correlation要更好。然而,從坐標圖中可以很明顯看出使用坐標時數字還是會排列成單行。
這就是說,我們有足夠的理由,讓RNG使用經由隨機散列函數處理過的種子。實際上這樣做似乎是個不錯的主意,在我看來沒有不妥。只是我所了解的目前流行的RNG都沒有這樣做,所以您得按照之前描述的內容自己實現。
為何使用不同種子適合隨機Hash函數
這沒有什麽特別原因,只是xxHash和MurmurHash3之類的函數對種子及輸入的處理類似,也就是說它本質上就是一個應用於種子的高質量隨機Hash函數。因為它的實現方式如此,所以從不同種子生成的序列中分別取第N個隨機數結果也是隨機的。
附錄C:更多Hash函數的對比
本文最初版我對比了PcgHash, MD5和MurmurHash3,並推薦使用MurmurHash3。
MurmurHash3的隨機性和速度都非常優秀。作者同時提供了名為SMHasher 的框架來測試哈希函數,該工具已經被廣泛使用。
這裏還有一個很好的方法Stack Overflow question about good hash functions for uniqueness and speed ,其中對比了很多哈希函數,並且生成的圖像與MurmurHash同樣良好。
在發布本文後我看到了Aras Pranckevi?ius的推薦從而得知了xxHash,並且從Nathan Reed那裏了解到了Wang Hash。
xxHash在自己的地盤擊敗了MurmurHash,因為其在SMHasher測試中的質量分很高,同時性能也更好。詳情請查閱xxHash on its Google Code page.
在我最初的實現中,移除了字節轉換之後,它比MurmurHash3更輕量更快速,盡管在SMHasher中看到的結果並非如此。
我還實現了WangHash。結果證明其質量不高,因為它在坐標圖中展示出明顯的模式,但它比xxHash要快5倍以上。我試著實現了“WangDoubleHash”,並將結果反饋,測試得出其質量不錯而且依然比xxHash快3倍以上。
然而,由於WangHash(以及WangDoubleHash)只接收一個整數為輸入,我決定實現同樣只接收單個輸入的xxHash和MurmurHash3
來看看性能會有什麽變化。結果發現性能顯著提高(大概4~5倍)。所有實際上xxHash是比WangDoubleHash更快的。
說到質量,我自己的測試框架 有著很明顯的缺點,但它沒有SMHasher
測試框架那麽復雜,所以某個哈希函數測試分高可以假定為其隨機性好而不僅僅是在我的測試中看起來不錯。一般我會說,經由我的測試框架測試過的函數都足以用於程序生成,但由於xxHash(優化過的版本)是最快的哈希函數並通過了我的測試,所以不用考慮了就用它吧。
還有非常多的哈希函數可以拿來做更多的比較。然而,我主要關註一些公認且被廣泛使用的,隨機性及性能都最優秀的方法,然後將它們優化後用於程序生成。
我覺得使用此版本的xxHash其性能是最佳的,並且想尋找或使用更好方法的可能性微乎其微。這就是說,隨意擴展 測試框架來實現更多吧。
Unity 可重復隨機數