本系列文章由zhmxy555編寫,轉載請註明出處.
http://qianmo.blog.51cto.com/5127279/875508
作者:毛星雲 郵箱: [email protected] 歡迎郵件交流編程心得
每一款遊戲,或大或小,都是由一段段默默無聞的算法在支撐着他們的運作,我們不能只欣賞絢麗的遊戲成品表現在我們面前的華麗與光鮮,還要看到那些支撐在華麗與光鮮背後的,鮮爲人知的算法。
篇章一 引言
我們知道,在遊戲領域裏,圍繞隨機性與隨機數展開的一系列技術有着非常廣闊的運用空間。
比如所有遊戲都離不開的寶物掉落系統。極品裝備的掉落永遠牽動着玩家們的心,譬如盛大《熱血傳奇》盛行的年代一把霸氣的“屠龍刀”,比如《魔獸世界》70級版本里一把帥氣的“伊利丹的雙刀”,又如《地下城與勇士》60級版本時一把拉風的“流光星隕刀”。
比如幾乎同樣是所有遊戲都離不開的裝備強化系統。閃閃發光的高強化武器永遠是每一個玩家的夢寐以求,但在這高強化武器光鮮的背後,有那麼多的玩家因爲不高的強化成功率而黯然神傷甚至傾家蕩產。不高的強化成功率往往是玩家“燒錢”的罪魁禍首,同樣這恰恰就是遊戲運營商盈利的主要來源之一(商城的強化防爆保護藥)。
比如《地下城與勇士》,《龍之谷》等網遊中的通關後翻牌(翻箱子)獎勵機制,又比如夢幻西遊中變異寶寶的出現等等,以上這些網絡遊戲中最吸引人的地方,表面上是明麗的圖畫與彩色的提示語,其實遊戲程序要實現這一個個可玩性十足的遊戲系統,全都離不開隨機數的產生。
我們來假設一個場景,你很喜歡玩DNF,今天你去凱莉那裏強化,心愛的武器【死亡舞步】直接一路上了15。看着散發出璀璨光芒的【+15死亡舞步】,你肯定會想,哇,今天人品真好~其實這樣的人品好,只不過是計算機的隨機數算法得出了一個個合適的隨機數數值,能滿足強化成功條件設定的臨界值罷了。又假設你剛剛單刷深淵爆出了一把【光炎劍-烈日裁決】,其實也是一樣的道理,如果深淵BOSS掉落【光炎劍-烈日裁決】的概率是五千分之一,需要的數值是386到390之間,也只不過是在你殺死BOSS的瞬間,計算機的隨機數算法算出了一個剛好在386到390之間的隨機數值,剛好滿足掉落這件PK神器的條件罷了。
引言說了這麼多了,無非就是想強調隨機數的產生在遊戲開發中的重要性,下面就進入正題吧,講解計算機中隨機數的產生方式。
篇章二 知識講解
在開始展開講之前,我們必須牢記一個概念,計算機中一般不能產生絕對隨機的隨機數。計算機產生隨機數的過程,是根據一個數(我們可以稱它爲種子)爲基準以某個遞推公式推算出來的一系列數,當這系列數很大的時候,就符合正態公佈,從而相當於產生了隨機數,但這不是真正的隨機數,當計算機正常開機後,這個種子的值是確定的,除非你對系統進行了更改。
即計算機一般情況下只能生成相對的隨機數,即僞隨機數。
當然,也不是說計算機沒有能力產生絕對隨機的真隨機數。前段時間看到過一篇用計算機產生“真隨機數”的論文,這裏先不作考慮,感興趣的朋友可以去看看相關的文章。
僞隨機數並不是假隨機數,這裏的“僞”是有規律的意思,就是計算機產生的僞隨機數既是隨機的又是有規律的。怎樣理解呢?產生的僞隨機數有時遵守一定的規律,有時不遵守任何規律;僞隨機數有一部分遵守一定的規律;另一部分不遵守任何規律。比如“世上沒有兩片形狀完全相同的樹葉”,這正是點到了事物的特性,即隨機性,但是每種樹的葉子都有近似的形狀,這正是事物的共性,即規律性。
在很多時候,我們會使用rand()函數與srand()配合來達到產生隨機數的效果,srand初始化隨機種子,rand產生隨機數,下面進行展開的分析(當然我們在這裏先不考慮某些遊戲引擎會另外設計自己的隨機數產生機制。):
一、隨機數發生器rand()函數的用法
函數名: rand
功 能: 隨機數發生器
用 法: int rand(void);
所在頭文件: stdlib.h
函數說明 :
▲rand()的內部實現是用的線性同餘法,它不是真的隨機數,因其週期特別長,故在一定的範圍裏可看成是隨機的。
▲這種僞隨機數是由小M多項式序列生成的,其中產生每個小序列都有一個初始值,即隨機種子。(注意: 小M多項式序列的週期是65535,即每次利用一個隨機種子生成的隨機數的週期是65535,當你取得65535個隨機數後它們又重複出現了。)
▲目前,計算機中用來產生隨機數的算法基本上都是“線性同餘”法。rand()返回一隨機數值的範圍在0至RAND_MAX 間。RAND_MAX的範圍最少是在32767之間(int)。
▲用unsigned int 雙字節是65535,四字節是4294967295的整數範圍。0~RAND_MAX每個數字被選中的機率是相同的。
▲用戶未設定隨機數種子時,系統默認的隨機數種子爲1。
▲rand( )產生的是僞隨機數字,每次執行時是相同的;若要不同,用函數srand()初始化它。
下面我們給出第一個小例子
- //MyRand01.cpp
- #include <iostream>
- using namespace std;
- #include <stdlib.h>
- #include <time.h>
- #define MIN 1 //隨機數產生的範圍
- #define MAX 10
- int main()
- {
- int i;
- srand((unsigned)time(0));
- cout<<"10個隨機數從 "<<MIN<<
- " 到 "<<MAX<<" :\n"<<endl;
- for(i=0; i<10; i++) //產生隨機數
- {
- cout<<MIN + (int)MAX * rand() / (RAND_MAX + 1)<<"\t";
- }
- cout<<endl;
- return 0;
- }
二、初始化隨機數發生器srand( )函數的用法
函數名: srand
功 能: 初始化隨機數發生器
用 法: void srand(unsigned int seed);
所在頭文件: stdlib.h
函數說明:
▲srand()用來設置rand()產生隨機數時的隨機數種子。
▲參數seed必須是個整數,通常可以利用time(0)的返回值或NULL來當做seed。
▲如果每次seed都設相同值,rand()所產生的隨機數值每次就會一樣。
下面我們給出第二個小例子
- //MyRand02.cpp
- #include <iostream>
- using namespace std;
- #include <stdlib.h>
- #include <time.h>
- #define MIN 0 //隨機數產生的範圍
- #define MAX 99
- int main()
- {
- int i;
- srand((unsigned)time(NULL));
- cout<<"10個隨機數從"<<MIN<<
- " 到"<<MAX<<" :\n"<<endl;
- for(i=0; i<10; i++) //產生隨機數
- {
- cout<<MIN + rand() % (MAX + MIN + 1)<<"\t";
- }
- cout<<endl;
- return 0;
- }
三、rand( )和srand( )的聯繫
rand( )和srand( )要一起使用,其中srand( )用來初始化隨機數種子,rand( )用來產生隨機數。
因爲默認情況下隨機數種子爲1,而相同的隨機數種子產生的隨機數是一樣的,失去了隨機性的意義,所以爲使每次得到的隨機數不一樣,用函數srand()初始化隨機數種子。srand()的參數,用time函數值(即當前時間),因爲兩次調用rand()函數的時間通常是不同的,這樣就可以保證隨機性了。
四、產生相同的隨機數的原因
計算機的隨機數都是由僞隨機數,即是由小M多項式序列生成的,其中產生每個小序列都有一個初始值,即隨機種子。(注意: 小M多項式序列的週期是65535,即每次利用一個隨機種子生成的隨機數的週期是65535,當你取得65535個隨機數後它們又重複出現了。)
我們知道rand()函數可以用來產生隨機數,這裏我再囉嗦一遍。計算機中一般不能產生絕對隨機的隨機數。計算機產生隨機數的過程,是根據一個數(我們可以稱它爲種子)爲基準以某個遞推公式推算出來的一系列數,當這系列數很大的時候,就符合正態公佈,從而相當於產生了隨機數,但這不是真正的隨機數,當計算機正常開機後,這個種子的值是確定的,除非你對系統進行了更改。
下面我們給出第三個小例子
- //MyRand03.cpp
- #include <iostream>
- using namespace std;
- #include <stdlib.h>
- #include <time.h>
- int main()
- {
- int i;
- for (i=0; i<10; i++) //產生10個隨機數
- {
- cout<<rand()<<"\t";
- }
- cout<<endl;
- return 0;
- }
每次運行得到相同的隨機序列:
41 18467 6334 26500 19169 15724 11478 29358 26962 24464
41 18467 6334 26500 19169 15724 11478 29358 26962 24464
爲得到不同的隨機數序列,則需改變這個種子的值。方法:在開始產生隨機數前,調用一次srand(time(NULL))(注意:srand()一定要放在循環外面或者是循環調用的外面,否則的話得到的是相同的隨機數)。
下面我們給出第四個小例子
- //MyRand04.cpp
- #include <iostream>
- using namespace std;
- #include <stdlib.h>
- #include <time.h>
- int main( )
- {
- int i;
- srand((unsigned)time(NULL)); //初始化隨機數種子
- for (i=0; i<10; i++) //產生10個隨機數
- {
- cout<<rand()<<"\t";
- }
- cout<<endl;
- return 0;
- }
每次運行得到不同的隨機序列:
1294 18562 14141 18165 11910 29784 11070 13225 131 24405
1774 25714 18734 16528 20825 17189 9848 8899 2503 5375
五、幾種隨機數的簡單算法
1.產生一個範圍內的隨機數
一般地,我們可用j=1+(int)(n*rand()/(RAND_MAX+1.0))來生成一個0到n之間的隨機數。
若用int x = rand() % 101;來生成 0 到 100 之間的隨機數這種方法是不可取的,比較好的做法是:
j=(int)(100.0*rand()/(RAND_MAX+1.0))
當然,如果是在gcc,vc之外的編譯器,我們也可以使用random(100)。下面的例子都是用了random(n)(VC無法識別random這個函數,VC下我們還是採用 j=(int)(100.0*rand()/(RAND_MAX+1.0)).
2、篩選型隨機數 如希望取0-99的隨機數,但不能是6。
解決方法:
x = random(100);
while (x==6) {
x = random(100);
}
又如希望取0-99的隨機數,但不要5的倍數 解決方法:
x = random(100);
while ((x % 5)==0) {
x = random(100);
}
3、從連續的一段範圍內取隨機數。
如從40--50的範圍內取隨機數。 解決方法: x=random(11)+40
4、從一組亂數中取隨機數。 如:從 67, 87, 34, 78, 12, 5, 9, 108, 999, 378十個數中隨機取數。 解決方法:可以用數組將些十個數存貯,然後把0--9中取出的隨機數作爲序號,實現隨機取數。
a = new Array(67, 87, 34, 78, 12, 5, 9, 108, 999, 378);
j = random(10);
x = a[j];
六、產生一定範圍隨機數的通用算法公式
▲要取得[a,b)的隨機整數,使用(rand() % (b-a))+ a (結果值含a不含b)。
▲要取得[a,b]的隨機整數,使用(rand() % (b-a+1))+ a (結果值含a和b)。
▲要取得(a,b]的隨機整數,使用(rand() % (b-a))+ a + 1 (結果值不含a含b)。
▲即(通用公式:a + rand() % n;取得[a,a+n) 的隨機整數,其中的a是起始值,n是整數的範圍。)
▲要取得[a,b) 的隨機整數,另一種表示:a + (int)(b-a) * rand() / (RAND_MAX + 1)。
▲要取得[a,b] 的隨機整數 另一種表示:a + (int)(b-a) * rand() / (RAND_MAX )。
▲要取得[0,1] 之間的浮點數 ,可以使用rand() / double(RAND_MAX)。
瞭解了隨機數產生的基礎知識和一些產生隨機數的算法,相信大家心裏應該有底了,比如如何設置各階段裝備強化的成功率,副本里裝備的掉落率,通關獎勵翻牌的掉落率,***暴擊的概率,***MISS的機率,夢幻西遊裏碰到變異寶寶的概率等等。
最後,我們提出兩個要點,就算這篇文章你看過後不能留下深刻印象,只要記住以下兩個要點,就算是我的這篇文章沒白寫,
因爲它讓你有收穫了:
1.計算機的僞隨機數是由隨機種子根據一定的計算方法計算出來的數值。所以,只要計算方法一定,隨機種子一定,那麼產生的隨機數就是固定的。
2.只要用戶或第三方不設置隨機種子,那麼在默認情況下隨機種子值爲1,來自系統時鐘。
本節筆記基本上就講解完了。
每一款遊戲,或大或小,都是由一段段默默無聞的算法在支撐着他們的運作,我們不能只欣賞絢麗的遊戲成品表現在我們面前的華麗與光鮮,還要看到那些支撐在華麗與光鮮背後的,鮮爲人知的算法。
接下來的這幾節淺墨準備講解遊戲相關的算法研究,注重算法的思想,配起漂亮的圖來不是那麼方便,看過我之前筆記的喜歡圖文並茂的朋友們請體諒一下淺墨。
關於這節筆記的實例(本來淺墨想寫一個C++版武器強化的demo的或者一個DNF通關後翻牌裝備獎勵的demo的,但是最近事情實在是太多了- -,等過一段時間閒下來了淺墨就開始寫,寫完了依然是貼出來供大家下載學習),所以這節筆記的實例就是上面的4個cpp源文件,非常輕量級,爲了方便大家研究,我依舊將他們打包起來。
本節文章源代碼請點擊這裏下載: 【Visual C++】Code_Note_17
感謝一直支持【Visual C++】遊戲開發筆記系列專欄的朋友們,也請大家繼續關注我的專欄,我一有時間就會把自己的學習心得,覺得比較好的知識點寫出來和大家一起分享。
精通遊戲開發的路還很長很長,非常希望能和大家一起交流,共同學習,共同進步。
大家看過後覺得值得一看的話,可以頂一下這篇文章,你們的支持是我繼續寫下去的動力~
如果文章中有什麼疏漏的地方,也請大家指正。也希望大家可以多留言來和我探討編程相關的問題。
最後,謝謝你們一直的支持~~~
——————————淺墨於2012年4月17日