寫這篇隨筆的動機,在於最近看了很多對於遊戲中機率事件的提問
在這些相關討論裏,老是能頻繁看到「真隨機」和「僞隨機」這兩個詞彙。
其中最多見的句子則莫過於寶典通常的——「程序裏沒有真隨機」。
這句話自己固然是沒有問題的。
可是大多數時候,用這句話去回覆別人的疑問就有點風馬牛不相及了。
致使這種狀況的緣由就在於,在不一樣的範疇,「真隨機」和「僞隨機」其實是有着不一樣的定義。
對機率提出疑問的遊戲玩家問的基本上都是應用層面的隨機問題,而回復者提到的倒是程序原理上的真僞隨機。
這樣一來一回非但不能解釋清楚遊戲裏的隨機問題,反而會讓看的人愈來愈迷茫。
因此,要想講清楚這件事情,就必須把程序原理上的真僞隨機和應用領域的真僞隨機都詳細說明一番。
程序原理上的真僞隨機
對計算機有點了解的朋友都知道,在程序裏,0就是0,1就是1,程序中是不存在可能爲0也可能爲1的數據的。
因此,程序也就不能本身生成「隨機」的東西。
在程序原理上,真隨機的定義是指,經過外置的觀測設備,觀測某個真正隨機的事物的狀態。
在須要產生隨機數的時候,記錄該事物的狀態值,再以此值通過必定的算法,獲得一個真正的隨機數。
(也有部分人認爲宇宙中不存在真正隨機的事物,因此宇宙中也沒有真隨機……這個爭論太玄學,我們就不攙和了。)
因此,採用真隨機對於程序來講,成本極高效率極低,在製做遊戲的時候,沒有人會蛋疼的買設備去作真隨機。
而相對於真隨機的僞隨機,就是指在系統內部抓取一個程序員自身沒法預料準確值的值,把該值做爲種子,放進隨機數生成器,由此獲得一連串隨機數的方法。
這句話聽起來很拗口,實際上過程很是簡單。在此我舉一個最簡單例子來講明:
隨機數生成器的核心部分是一個函數,函數就能夠寫成f(x)的形式
這個x,就是一個隨機種子
x的肯定標準就是要沒法預測,好比說能夠選取系統開始運行以後的時間(單位毫秒)
而後把x放進f()
根據函數的特效,x的值肯定,f(x)的值就惟一且肯定
這個f(x)就是該隨機數生成器生成的第一個隨機數,咱們記做R_1
(注:R1只是一個胚體,在實際調用的時候,還會用一個不可逆的處理方法使其變成咱們須要的隨機數——好比50到300之間的隨機數,這個過程在此就略過不寫了)
而後若是還須要第二個隨機數,就把R_1放進f(),獲得第二個隨機數R_2 = f(R_1)
而後若是還須要第三個隨機數,就把R_2放進f(),獲得第三個隨機數R_3 = f(R_2)
以此類推
R_1 = f(x)
R_2 = f(R_1)
R_3 = f(R_2)
…
R_n = f(R_ n-1)
由此能夠看出,一旦某個隨機數生成器的種子肯定,他以後所產生的每個隨機數就都肯定了
隨機數生成器就比如一副空白撲克,放入種子的過程就比如給每一張撲克都寫上一個數字
而後等着程序在須要的時候去一張張抽取調用
進而能夠獲得一個推論:若是兩個隨機數生成器的種子是同樣的,那麼他們這兩副撲克的牌序也就都是同樣的了。
html
<ignore_js_op>程序員
在同一時間建立三個隨機數生成器,讓他們的種子一致算法
而後讓他們輪流生成4個100之內的隨機整數
數組
<ignore_js_op>網絡
能夠看到,這三個隨機數生成器在每一次生成的值都是同樣的
這個推論在遊戲中最多見的運用場景就是replay回放
好比war3的錄像回放,一個幾十分鐘的錄像,大小隻有幾十K
這個錄像文件中存放的實際上只有每個玩家的有效操做,以及每個隨機數生成器的種子值
而後根據這些內容,建立一場遊戲,模擬重現整場戰鬥
錄像文件是不會去記錄每個野怪的掉落,劍聖的每一刀是否暴擊,牛頭人是否能打出粉碎等等信息的,不然容量就會大大超標。這些隨機的內容所有都是經過set種子值來重現。
看到這裏可能就會有很多玩家以爲很沒勁
若是每一次隨機的結果在遊戲開始時就已經肯定了,那隨機還有什麼意義
這,就是一個很哲學的問題了。
若是有一副空白撲克,上帝在每一張的上面都已經隨便寫上了一個數字。
若是你沒法查看也沒法修改這些數字,那麼對於須要一張張摸牌的你來講,這些數字究竟算隨機的仍是肯定的?
每一個人的見解或許都不同。
可是,不管你的見解如何,至少在表現出來的效果上,這副牌就是隨機的。
同理對於程序中的種子隨機:
若是你沒法查看也沒法修改隨機種子,那麼程序用僞隨機方法所產生的隨機數在表現出來的效果上就等同於真隨機。
用僞隨機進行大規模的模擬,其統計結果也會與數學計算出來的指望相符。
若是你暴擊率35%,每次攻擊時程序產生一個1到10000之間的數,處於1到3500之間時就暴擊
連續攻擊一萬次,你暴擊的次數就會在3500附近。
至於第一萬零一次會不會暴擊?仍然是35%的概率暴,65%的概率不暴。
dom
<ignore_js_op>編輯器
模擬攻擊一萬次,暴擊時計數+1函數
<ignore_js_op>spa
暴擊的次數設計
應用層面的真僞隨機
在應用層面上,真隨機就是指每一次概率判斷都是獨立的。
好比說一個遊戲角色的暴擊率是20%,那麼在真隨機的機制下,他的每一次攻擊都會是20%的概率暴擊。
前一刀暴擊了,下一刀是20%暴率,前一刀沒爆,下一刀也仍然是20%暴率。
僞隨機就是指同一類的機率事件,彼此之間存在關聯性。
好比說一個遊戲角色的暴擊率是20%,那麼在僞隨機的機制下,這個角色每一次攻擊的暴擊率都是動態變化的。
前一刀暴擊了,後一刀的爆率就會下降;前一刀沒爆,後一刀的爆率就會提高。可是,當這個玩家進行足夠屢次攻擊以後,統計上的暴擊率仍是會等於20%。
能夠說,真隨機是一種天然的隨機機制,用代碼來實現也很是容易,只須要用一個隨機數與一個常量進行比較,根據大於小於等於分別觸發不一樣的結果就好了。
而僞隨機則是人爲創造出來的一種機制,他須要程序員寫下更多的代碼,也須要數值設計者作更多的計算。
那麼,既然僞隨機費時費力,還反天然,爲何在應用領域還要引入各類僞隨機的算法呢?
其目的就在於——讓用戶獲得更好的體驗。
我以抽獎爲例,好比說某個遊戲內置抽獎系統:
抽獎每次消耗1塊錢,有1%的概率獲得一個價值90塊的東西。
有至關一部分參與者就會以爲,我先抽一下碰下運氣,萬一抽不到,我連抽100次,總歸會拿到的吧,小虧一點點而已。
可是實際上,連續抽獎100次而不中的機率高達36.6%——超過1/3的比例。
甚至於即便連續抽300次,也仍然有4.9%的概率不中。
也就是說,若是這個遊戲有10萬玩家,就有4900我的連續抽獎300次都中不了。
而這部分玩家一般都不會心甘情願的接受本身運氣很差這個事實——他們之中一部分可能會心理受挫,刪除遊戲成爲流失玩家;
還有一部分則極可能會在網絡上聯合起來,產生必定的輿論壓力。
不管那一種狀況,都是遊戲設計者所不肯意見到的。而設計者爲了不這樣的問題,就不得不考慮引入僞隨機。
從用戶體驗上來講,僞隨機就是介於「真隨機」和「不隨機」之間的一種感受。
對於1%概率的抽獎,真隨機就是上面我描述的狀況。
不隨機就是固定的每隔99次以後中獎1次。
僞隨機就是中獎事件會分佈得比真隨機更加均勻,但仍是具備必定的隨機性。
因此,僞隨機並非一個負面詞彙。它存在的意義是爲了讓概率事件分佈得更加均勻,避免讓用戶遇到極端走運或極端倒黴的狀況。
在討論遊戲機率的時候,讓僞隨機來爲某些負面情緒背鍋顯然是不對的。
最後,我大體說明一下最爲常見的幾種僞隨機算法
這是僞隨機在遊戲中最多見的用法,所以直接就被玩家用Pseudo Random Distribution的縮寫PRD來指代了
其中最爲典型的案例就是WAR3,以及用WAR3編輯器製做的DOTA。
在WAR3中,一個暴擊率20%的英雄,並非每一刀都20%暴擊率的。
而是以5.57%做爲初始暴率,若是第一刀不暴,則第二刀的暴率增長到初始值的2倍:11.14%;
若是仍是不暴,就繼續增長到初始值的3倍:16.71%,以此類推。
而若是在這個過程當中任何一次攻擊打出了暴擊,就會把暴擊率重置到5.57%。
<ignore_js_op>
<ignore_js_op>
經過驗算能夠看到,暴雪以這種方式實現的暴擊,最終表現出來的暴率仍然是20%。
不過一般來講,PRD並不會在遊戲中作實時的機率推算——作這樣的逆運算會消耗太多的計算資源。
據我推測,暴雪應該也是創建了一張lookup對照表,在遊戲中根據理論暴率查表而後得到動態暴率的基礎值。
洗牌算法最典型的應用莫過於音樂播放器的隨機播放。
在最先期的時候,播放器的隨機播放就是採用的真隨機。
可是用戶很快就發現,常常會遇到接連播放同一首歌,或者連續屢次在幾首歌之間來回切換,而另外某些歌曲幾百次也放不到。
爲了解決這個問題,播放器就把真隨機改成了洗牌算法。
所謂的洗牌算法就是:若是你的歌單有20首歌,就創建一個1到20的數組,再把這20個數字像洗牌同樣洗成亂序。
在洗完以後,若是第一個數字是n,第一次就播放歌單裏的第n首歌。以此類推。
<ignore_js_op>
<ignore_js_op>
說實話,其實我不肯定這個能不能算做是一種僞隨機。
可是這種作法在如今的遊戲界太過廣泛,不得不拿出來講明一下。
所謂的組合隨機,典型的應用就是在抽獎的時候進行兩次判斷:
一次不隨機:根據預設好的肯定數組,給予玩家對應的chest。
這一次主要是用於肯定獎品品質。
一次真隨機:從選中的chest中隨機抽取一件物品給玩家。
這一次就是從對應品質的獎品堆中隨機獲取一件物品。
最典型的例子就是《我叫MT》的手遊。
在這個遊戲裏,你第幾回抽獎能中紫卡是徹底肯定的,可是你具體抽到哪一張紫卡則是隨機的。
相關閱讀:一個關於遊戲掉落的機率設置問題的討論——容斥原理