從Dota2的偽隨機談開

起因是在知乎上看到木七七工作室轉發的談隨機處理的一個內部視頻,視頻里面聊到Dota2的技能概率處理方式,比如大魚人(Sorry好久沒玩Dota了已經想不起來這位的真名)的被動技能升滿后:

  • 每次攻擊有25%的幾率讓敵人眩暈。

一般做法用純隨機 True random distribution的方式,每次攻擊時計算概率,獨立判斷是否觸發眩暈。不過可能會出現一些體驗問題:

  1. 因為每次獨立計算概率,極限情況會導致一直觸發眩暈和一直不觸發,間接造成歐皇和非洲酋長之間的戰爭。
  2. 從玩家體驗上來說,感官上25%的幾率,超過5,6次不觸發,他就會開始懷疑幾率是否被策劃運營篡改,幕后是否有骯臟的PY交易,而不是回想下初中的數學課。

純隨機在數學上是無罪的,機器底層的隨機函數是清白的(其實也不是那么清白,畢竟純隨機是不存在的,不過這個就扯深了,先默認一般的random接口函數就是純隨機),但是有些時候并不是最佳解決方案。

用偽隨機分布Pseudo-random distribution處理概率

Dota2的偽隨機分布采用概率補償的方式,每次觸發概率從一個值開始遞增,第N次的觸發概率P(N) = C * N,比如25%的幾率,C值大概為8.5%,運算流程如下:

  1. 第一次觸發眩暈概率為8.5%
  2. 第二次為17%,以此類推遞增
  3. 如果觸發眩暈成功,則概率重新從8.5%開始遞增計算。

這種方式使得連續觸發或連續不觸發的幾率降低,避免了運氣成分過于影響戰斗結果(特別是競技游戲)。

一般幾率對應的C值可以參考下面這張圖。P(T)代表預期值,就是游戲中顯示的幾率值。P(A)是用了偽隨機后的實際概率。MaxN表示最壞情況下觸發概率的次數。

rpic1.png

計算C值的方式和程序實現可以參考這個鏈接下的回答,有C#的實現代碼:

//CfromP是主函數,傳入理論概率P就可以求得遞增的C值
public decimal CfromP( decimal p )
{
    decimal Cupper = p;
    decimal Clower = 0m;
    decimal Cmid;
    decimal p1;
    decimal p2 = 1m;
    while(true)
    {
        Cmid = ( Cupper + Clower ) / 2m;
        p1 = PfromC( Cmid );
        if ( Math.Abs( p1 - p2 ) <= 0m ) break;

        if ( p1 > p )
        {
            Cupper = Cmid;
        }
        else
        {
            Clower = Cmid;
        }

        p2 = p1;
    }

    return Cmid;
}

private decimal PfromC( decimal C )
{
    decimal pProcOnN = 0m;
    decimal pProcByN = 0m;
    decimal sumNpProcOnN = 0m;

    int maxFails = (int)Math.Ceiling( 1m / C );
    for (int N = 1; N <= maxFails; ++N)
    {
        pProcOnN = Math.Min( 1m, N * C ) * (1m - pProcByN);
        pProcByN += pProcOnN;
        sumNpProcOnN += N * pProcOnN;
    }

    return ( 1m / sumNpProcOnN );
}

上面的偽隨機分布算是用概率補償的方式控制概率來改善玩家的體驗,詳細的可以參考Dota2的Wiki(打Dota2,向冰蛙學數學)。

當然也有其他方式控制隨機數和概率,正好前一陣子看了一個從D&D擲骰角度談控制隨機分布的文章,下面也算一個翻譯和整理。

我這把可是1d2有毒的飛刀

D&D里面NdS表示投擲S面的骰子N次,累加結果。比如1d12表示投擲一個12面骰子一次,3d4表示投擲一個4面骰子3次。

假設我們要獲取[0,24]之間的隨機值,可以先設置一個函數rollDice(N, S)來模擬骰子投擲:

public static int rollDice(int N, int S) {
    int value = 0;
    for (int i = 0; i < N; i++) {
        //每次隨機結果為[0, S]
        value += Random.Range(0, S + 1);
    }

    return value;
}

我們可以rollDice(1,24),也可以拆分成2次,變成rollDice(2,12),變成兩次[0,12]的和,以此類推rollDice(3,8)、rollDice(4,6),下面這張圖可以看到最終結果的分布變化:

rpic5.jpg

可以看到投擲的次數越多,最終結果分布就越集中在[0,24]的平均值附近,所以4d6的武器比3d8的武器輸出更平穩,但3d8的武器造成高傷害的幾率也更高。

除了控制隨機取值的集中區域,我們還可以用簡單的方式控制隨機取值是大部分分散在平均值以下還是大部分分散在平均值以上。

兩次隨機取較大/較小值

還是以取[0,24]之間隨機值為例,每次rollDice(2,12)兩次,取較大值:

int roll1 = rollDice(2, 12);
int roll2 = rollDice(2, 12);
int result = Math.Max(roll1,  roll2);

分布圖如下:

rpic2.png

反過來,取較小值,可以獲得集中在平均值以下的分布:

int roll1 = rollDice(2, 12)
int roll2 = rollDice(2, 12)
int result = Math.Min(roll1, roll2);
rpic4.png

取較小值在計算傷害值比較常見,比如一個角色的攻擊力在20到40之間,利用這種方法可以使得最后結果集中在較低的范圍,高傷害出現的幾率較低。

三次隨機取較大的兩個值

rollDice(1,12)三次,取較大的兩個值:

int roll1 = rollDice(1, 12);
int roll2 = rollDice(1, 12);
int roll3 = rollDice(1, 12);

int result = roll1 + roll2 + roll3;
result = result - Math.Min(roll1, roll2, roll3);

分布圖如下:

rpic3.png

可以看出比兩次取較大/較小值分布更為平滑。

總結一下,可以看到在控制某個范圍內隨機數時,可以從下面幾個角度進行自定義以滿足需求:

  1. 范圍。確定隨機范圍的最大值和最小值,如果需要可以做一些偏移,比如[20, 30]可以分解為20 + rollDice(1, 10)。
  2. 方差。將一次隨機分解為多次隨機,可以使結果更靠近中間值。相反,次數越少,結果分布范圍越廣。
  3. 不對稱性。可以通過上面介紹的兩種方法,使隨機結果更多分布在平均值之前或者之后。

自定義概率分布

很多情況下,策劃過來找你的時候,情景有可能是:我這里有10種掉落物品,每種的掉率我都想單獨配置,比如A掉率10%,B掉率20%等等和一個Excel文件。

最終的配置文件可能是像這樣一個數組,前面是掉率(以100算100%),后面跟著物品ID。

local dropRate = {
    {10, 100001},
    {20, 100002},
    {30, 100003},
    {40, 100004},
}

掉率的總和不一定正好是100,畢竟要考慮些對配置文件的容錯性,所以先算出概率和sumRate,取random(sumRate)的值value,依次遍歷dropDate表,累加概率和weight,如果value小于等于weight,則算是落在當前區間,返回對應的物品ID。我用lua寫了一段測試代碼,畢竟lua的table實在是太方便了。

local dropRate = {
    {10, 100001},
    {20, 100002},
    {30, 100003},
    {40, 100004},
}

local distribute = {
    [100001] = 0,
    [100002] = 0,
    [100003] = 0,
    [100004] = 0,
}

local checkRate = function(t, value)
    local weight = 0
    for i=1,#t do
        weight = weight + t[i][1]
        if value <= weight then
            return t[i][2]
        end
    end

    return nil
end

local getDropItem = function(t)
    local weightTotal = 0
    for k,v in pairs(t) do
        weightTotal = weightTotal + v[1]
    end

    local value = math.random(weightTotal)

    return checkRate(t, value)
end

local main = function()
    --用倒序時間設置random的seed,確保seed隨時間顯著變化
    math.randomseed(tostring(os.time()):reverse():sub(1, 6))

    for i=1,10000 do
        local id = getDropItem(dropRate)
        if id and distribute[id] then
            distribute[id] = distribute[id] + 1
        end
    end

    for index,dis in pairs(distribute) do
        print("index:",index)
        print("dis:",dis)
        print("percent:",dis / 10000)
        print("=================")
    end
end

main()

測試結果和配置概率很接近,這樣就可以讓策劃盡情發揮他的奇怪掉率了。

總結

上面的部分只是最近看到的一些有意思的隨機數討論整理,真正在實際項目中,隨機數的處理是跟隨不同的需求做變化的,隨機可以增加游戲過程的樂趣,可以給游戲增加賣點,也可以變成各種“坑”,對于開發來說,只要這個坑是可控制的,不要坑到自己就行了~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容