大家好,這篇要來聊一聊unity的亂數產生器(random number generator,簡稱 RNG )
會寫這篇原因是在開發我的棒球遊戲時,會發現我的球員守備失誤率似乎不如我預期來得好(或差)
而我的球員守備失誤是由球員本身的數值比對亂數產生器出來的結果
所以我做了幾項實驗,並得出一些心得跟大家分享:
 
小弟認為 ,RNG 是人類偉大的發明,現在這個年代我們生活中已經離不開它的影子了。舉凡影像處理,通訊加密都會應用上。在遊戲開發上,也會用到RNG,像攻擊敵人產生會心一擊的機率,或者是手遊看到抽卡牌的功能,都是用到RNG。
正式討論unity rng 之前,我們先了解一下RNG的作法原理,可參考wiki的說明。

 
簡言之,RNG可再分成 屬於TRNG或PRNG,TRNG (True Random Number Generator) 使用到自然界的亂數分佈行為的因子來產生(如 自然界中的分子熱擾動行為是亂數的)。它的優點是他是真實的亂數,無法預測,無法回推,但缺點就是軟體無法實現,硬體實現成本也高(註 )。另一種則是 PRNG (Pseudo Random Number Generator),它用一個方程式算出數列(random sequence),這個數列符合亂數產生行為, 也方便於軟體實現,不過PRNG需要亂數種子(random seed)來當初始值, 亂數序列會因不同的random seed 產生不同的亂數序列. 但不要誤會一件事, PRNG 的亂數指的是單一random seed所產生的亂數序列是呈現亂數行為,不同的random seed產生的亂數序列混合用在同一事件上並不是能保證它表現是亂數行為的, 在網路文章中有提到這樣的說明與實驗. PRNG雖然得使用random seed來初始化,這在某些應用是方便的,比如說我們在每次執行遊戲後,遇到因亂數產生的corner case出現程式bug, 我們可以使用random seed來重現問題,因為同一個random seed出來的亂數序列都會是一樣的。但這在通訊加密領域可是一個潛在漏洞了,像之前wifi wpa2加密漏洞事件就是駭客可以從強迫斷線重連來嘗試重置random seed ,來達到破解的目的。
 
 
了解RNG特性後,
我們來討論 Unity 的RNG , Unity 叫用 RNG 即是 Random.range(min_vale, max_vale) 
他有一個使用者要小心的地方是 min_value/max_vale 是float的話, 亂數出來的值是min_value/max_value兩者都包含.
但如果是 int 型別的話, max_value 不不會包含於其中,比如說,我寫成
 
ran = Random.range(0,100) 
 
ran出來的值分佈會是在 0~99 之間,並不會產生出100 這個int值
 
它有一個api 叫 random.initState(int random_see)
於是我們可以推測unity 的RNG應是使用PRNG
在unity的script API 也有描述
而unity 裡也有一個 Random.state ,它記錄了PRNG 目前的序列狀態
你可以在 unity scripting API 找到它的reference code

 

好,再來回到我最初的疑問,再詳細描述一次:
我遇到的問題是假設我的球員有98%的守備力,我把它數值設定為98
接著我每次到該球員需要守備時,就call一次 Random.range(0,101), 
於是它會亂數產生0~100的亂數值
用此值跟球員的守備數值比對,比球員數值大者就是失誤
有時平安無事,有時你會發現球員會著魔似的狂失誤
於是我開始懷疑我使用的亂數方法是否有問題
 
首先,遊戲中我都是使用同一亂數序列貫穿所有該需要random的事件
也就是說,遇到A球員也一樣call Random.range(0,101),遇到B球員一樣call
Random.range(0,101),等於是一套亂數序列等分給每一次的random事件 

根據對PRNG的理解,這樣對於每一位單一球員各自遇到的亂數序列未必是真的亂數
於是,我做了一些實驗想證明這件事 :
 
首先,我對單一的事件作10000次的 Random.range(0,101) 亂數, 並統計亂數所選出的數字次數
用unity 畫成長條圖顯示出來, 為了更精確看出變化,再統計計算其標準差 

單一亂數10000次

其標準差落在 0.000967  (數字多少不重要,主要要再跟下面實驗數值做比較)

接著, 我分成九群統計群, 來代表球員各自遇到的亂數序列, 一樣作10000次的 Random.range(0,101) 亂數
不過這次會依序平分給九群統計群
 
每一群長這樣,  

而九群的標準差是如此
 
 
你會看到似乎不太平均,有些異常有高有低
Bingo! 你也許覺得這就是答案了!但事情沒那麼簡單! 

因為 10000 次分成了九群,對每一群其實只random 10000/9 ,約1000多次而已
所以我在做一個實驗, 九群亂數9倍,90000次 

九群亂數90000次

九群各自的標準差

你會發現跟單一事件10000次而言,看來差不多! 標準差雖然稍微增加一些
不過也不能說很嚴重,實際上,它可以說它不是dominate factor!
 
倒是一件事引起我的在意 - 就是random的次數影響標準差甚鉅!
接著又開始另一項實驗 - 單一事件random的次數跟標準差比較 

Random.range(0,101) random 100次結果
標準差 = 0.00916515

Random.range(0,101) random 200次結果
標準差 = 0.006770831

Random.range(0,101) random 500次結果
標準差 = 0.004400001

Random.range(0,101) random 1000次結果
標準差 = 0.003052868

Random.range(0,101) random 10000次結果
標準差 = 0.000967

於是你發現問題了!  亂數次數(母數)越少, 出來的序列不足夠代表亂數
這也就是球員守備失誤率無法顯現他該有的守備能力表現了
一場比賽,單一球員遇到的守備次數最多也不會超過30次
實際上,一場比賽一位球員應該有5~10次守備機會就很了不起了
 
那麼我該如何在5~10次的數列中表現出球員守備率98%和守備率87%的不同呢? 

最後,我採用了另一種做法 : 
對98%守備率的球員如果有100次守備機會, 會有兩次失誤
這兩次失誤會隨機亂數出現在100次機會的任一地方 
 
乍聽之下不是廢話嗎?
我把Pseudo code 寫出來吧
 
int s[100]
//initial array
for i = 0..100
    s[i] = i
 
j=0
for i = 0..random_time 
    j = j + s[i] + Random.range(0,100)
   swap (s[i], s[j])
    
 
等於是說我把亂數用在於array的亂數置換上
在array 裡的值因為只有置換,裡面得值永遠只會落在 0..100 之間
之後每次球員遇到守備機會就會依序去抓取array的值來比大小
對球員而言, 100次機會只有兩次機會失誤,只是不知何時會發生
不會因為母數不足而產生三次以上的失誤
 
 
註 : 這個方法的靈感來自一種古老的加密演算法-
          RC4 的隨機亂數初始化.
          RC4雖然在加解密時代的洪流中被淘汰了
          但是它的演算精神我把它帶來在遊戲設計中 

希望這分享對大家有用
最後偷渡一下我的想法, 隨機亂數在遊戲設計中是個很常被應用的功能
角色數值企劃會應用上,AI設計也會應用上, 
利用隨機亂數些許的不確定性會讓遊戲更有深度, 更趣味
對小弟我這個做數位IC的工程師來說,它就好像讓數位的邏輯轉換成類比的訊號的DAC
(讓有限的狀態,轉成了無限多種狀態)
讓數位類比混合讓功能更加強大!
不過,小弟我仍希望隨機亂數的功能大部分是設計在遊戲設計上的,不論是遊戲操作,遊戲攻擊數值...等
但如果一款遊戲隨機亂數應用注重在商品的販售上,如抽卡牌
小弟我認為反而是減弱了遊戲的趣味性
並偏重於博弈味的樂趣了.