iT邦幫忙

2022 iThome 鐵人賽

DAY 17
1
Modern Web

30個遊戲程設的錦囊妙計系列 第 17

Trick 16: 用MD5亂數產生器當個造物主

  • 分享至 

  • xImage
  •  

前天提到亂數產生器,以一個看似單純卻聚集了數學精要的演算法自製亂數產生器。

昨天講了哈希雜湊,列舉了雜湊在遊戲裏能夠施展的各種能力。

那麼今天我們來試著讓MD5和亂數產生器合體,看看可以蹦出什麼樣的火花。

MD5亂數產生器

我們想想MD5做的是什麼?就是把我們看得懂的資料變成一串我們看不懂亂七八糟的字串。咦!?那不就是亂數的意思嗎!

我們給定一個基因整數,搭配一個名字,然後把這兩個參數用MD5.hash()一下,再從結果中取出一段數字當成亂數種子,這樣就可以得到一個專屬於這個名字的亂數產生器。這個亂數產生器只要給的基因整數與名字不變,那產出來的亂數數列就會一模一樣。

上面我講得很含糊,大家可能也聽得很迷濛,因為這個方法是我早年製作一個養寵物的遊戲時想出來的小把戲,一直很不好意思拿出來獻醜。不過現在臉皮厚了,下面我把當時實際使用的方法介紹給大家,希望能啟發同學們生出更多的好點子。
怪朋友
玩家在遊戲的世界中找到一隻寵物的實體時,這隻寵物就會有身高體重、技能力量、飲食偏好等各種屬性,事實上,這隻寵物連升級後的各項屬性也是在一出生的時候就已經決定好了。這樣的設計在遊戲中是怎麼做到的呢?
火龍 牛寶寶
方法說穿了很簡單,就是在寵物出生時,先用亂數取一個數值當成這隻寵物的基因,儲存在伺服器上的同樣也只有這組基因數字。在玩家查看寵物的力量屬性,或是寵物對戰要取出力量屬性去計算傷害的時候,就將基因加上字串"力量"去作Hash,再從這個Hash裏取一段數字出來當亂數種子,最後再把亂數種子放進亂數產生器去得到這隻寵物的力量屬性。

// 決定寵物的基因
let gene = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
// 取得力量的Hash(將屬性名稱加上基因去取MD5)
let strengthHash = MD5.hash("力量" + gene);
// 從Hash裏取出最前面的八個字串(16進位的字串)
let strengthHashCut = strengthHash.substring(0, 8);
// 將切下來的字串轉換成10進位的數字
let strengthSeed = parseInt(strengthHashCut, 16);
// 建立亂數產生器
let rng = new RandomGenerator(strengthSeed);
/** 計算力量屬性(平均力量: 100,最大差異: ±10)
 * 這裏的rng.next()會給出一個介於0到1的數字
 * 所以(rng.next()-0.5)*2就會得到-1到1之間的亂數
 */
let strength = Math.round(100 + (rng.next() - 0.5) * 2 * 10);
console.log(`這隻寵物的力量=${strength}`);

利用這個方法,不但在伺服器上只要為每隻寵物儲存一組作為基因的整數,而且往後要擴充什麼新的屬性,比如寵物對辣椒的過敏程度或冬天待在家的進食頻率,也完全不是問題。

正式程式碼

上面寫的程式只是先用簡單幾行解釋給同學看,下面我們正式把MD5亂數產生器寫成一個類別,這個類別會繼承前兩天寫的亂數產生器,忘記的同學可以再去回味一下。

/** 將一組數字與字串的組合,變成一個亂數種子的函式 */
function getMD5Seed(num: number, phrase: string): number {
    let hash = MD5.hash(phrase + num);
    let cut = hash.substring(0, 8);
    return parseInt(cut, 16);
}
/** 實作MD5亂數產生器(繼承先前寫好的RandomGenerator) */
class MD5RandomGenerator extends RandomGenerator {
    // 建構子
    constructor(gene: number, phrase: string) {
        /** 呼叫從RandomGenerator繼承來的建構子
         * super()會呼叫繼承類別的建構子
         * RandomGenerator的建構子需要一個亂數種子作為參數
         */
        super(getMD5Seed(gene, phrase));
    }
}

常態分配的亂數

剛剛我們用亂數產生器去得到力量屬性時,用的是平均分配的亂數。平均力量100,最大差異±10,這樣所有寵物的的力量屬性會很平均地分布在90到110之間。

不過我們要的不是這種亂數,這樣的屬性分配太不自然。我們真正想要的是一個呈常態分布的亂數產生器,也就是說,如果黑皮熊的平均力量是100,變異數是10,那麼把遊戲中所有黑皮熊的力量放在一起看的話,絕大部分應該都要出現在100左右,只有約2%的黑皮熊力量大於120,也只有2%的力量比80還小。

以下寫一個取常態分配亂數的函式給同學們參考。

function normalDistributionRandom(
    rng: RandomGenerator, // 使用這個亂數產生器
    mean: number, // 平均值
    dev: number,  // 變異數
) {
    // 平均從全圓取一個角度
    let theta = rng.next() * 2 * Math.PI;
    // 用指數分布取離原點距離
    let dist = -Math.log(1 - rng.next());
    // 用極座標取點
    let point = Point.polar(dist, theta);
    // 取完的點,裏面的x和y都是屬於標準常態分布的亂數
    let value = point.x;
    // 把標準常態分布依平均值與變異數去平移和放大
    return value * dev + mean;
}

如果想深入理解如何DIY常態分布的亂數,可以參考拙作
(寫程式玩數學#3)人工製造常態分布的亂數...不會了吧!

我們再寫個示範程式碼來用上面寫的工具取得寵物的力量。

// 寫一個造物函式
function createPet(id: number) {
    /** 定義寵物力量平均值和變異數 */
    let strMean = 100;
    let strDev = 10;
    /** 定義寵物的基因 */
    let gene = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
    /** 建立MD5亂數產生器 */
    let rng = new MD5RandomGenerator(gene, "力量");
    /** 計算這隻寵物的力量 */
    let strength = normalDistributionRandom(rng, strMean, strDev);
    /** 取整數 */
    strength = Math.round(strength);
    console.log(`寵物(${id})的力量 = ${strength}`);
}
// 造出五隻寵物
for (let i = 1; i <= 5; i++) {
    createPet(i);
}

按以上的方法,就能造出五隻寵物,他們的力量大部分會集中在100左右,但也有機會生出極少數力量達到120的精英,或甚至超越140天生神力的怪力寵。

MD5亂數產生器和常態分配的結合,很有趣吧!

CG示範專案


上一篇
Trick 15: 把Hash函數帶進遊戲玩
下一篇
Trick 17: 綿延不絕的隨機地形是咋做出來的?
系列文
30個遊戲程設的錦囊妙計32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言