読者です 読者をやめる 読者になる 読者になる

(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

確率判定【C#】【Unity】

C# Unity

このエントリーをはてなブックマークに追加

この記事でのバージョン
Unity 5.3.0f4 Personal

はじめに

ゲームを作ってると30%の確率でアイテム取得するだとか、

ガチャのようにそれぞれ出現確率が違うものから一つ抽選するとか、

確率を使って何かを判定するという事がよくあります。


今回はそんな確率判定の話。


ProbabilityCalclator

先ほど例にあげた、真偽を確率で判定する処理と、

複数の中から確率で一つを抽選 する処理が行えるクラス

ProbabilityCalclatorを作ってみました。



使い方は以下のような感じ。

//真偽を確率で判定
//ProbabilityCalclator.DetectFromPercent

//perが確率でint or float
if (ProbabilityCalclator.DetectFromPercent (per)) {
	//判定が真
}
//複数の中から確率で一つを選択
//ProbabilityCalclator.DetermineFromDict


//Dictionary<対象, 確率>でDictを作成し、入力する。確率はint or float
Dictionary<Color, int> colorPerDict = new Dictionary<Color, int> () {
  {Color.red, 25}, {Color.green, 15}, {Color.blue, 60}
};

//RGBから一色選ぶ、確率はそれぞれ25%,15%,60%
Color color = ProbabilityCalclator.DetermineFromDict<Color> (colorPerDict); 


対象となっているColorの部分はstringでもGameObjectでもenumでもなんでも大丈夫です。

また、例だと分かり易いように確率の合計が100%ですが、

これは確率ではなく重みなので、100%超えても下回っても問題ありません。


精度

実際にはどの程度の精度になるか、

真偽判定と抽選の双方について100万回試行し、平均した結果が以下の通りです。

真偽判定の精度確認
試行回数 : 1000000回, 目標 : 0.2%, 実際 : 0.1974%

抽選の精度確認
試行回数 : 1000000回
RGBA(1.000, 0.000, 0.000, 1.000) 目標 : 25%, 実際 : 24.9291%
RGBA(0.000, 1.000, 0.000, 1.000) 目標 : 15%, 実際 : 15.0331%
RGBA(0.000, 0.000, 1.000, 1.000) 目標 : 60%, 実際 : 60.0378%


特に検証はしませんが、まぁ十分な数値だと思われます。


ちなみに試行に使ったコードは以下の通りです。





処理内容

ではコードの説明です。

なお、入力がintのものはfloatに変換してるだけの同じ処理になります。


真偽を判定

DetectFromPercentの処理内容ですが、

まず小数点以下の桁数を求めます。

int digitNum = 0;
if(percent.ToString().IndexOf(".") > 0){
  digitNum = percent.ToString ().Split(".")[1].Length;
}


その桁数から、少数を消すための倍率を求め、

int rate = (int)Mathf.Pow (10, digitNum);


判定用の乱数の上限と、真と判定するボーダーを設定します。

int randomValueLimit = 100 * rate;
int border = (int)(rate * percent);


最後に乱数を生成し、ボーダーを越えたか否かで判定を行います。

return Random.Range (0, randomValueLimit) < border;


ランダムで選ばれるパターンはrandomValueLimit個

その中で真を返す数はborder個なので、border / randomValueLimitの確率になるという感じです。


複数の中から一つを選択

DetermineFromDictの方は、まずそれぞれの確率を累計し、

//累計確率
float totalPer = 0;
foreach (float per in targetDict.Values) {
  totalPer += per;
}  


その累計を上限とした乱数を生成

//0〜累計確率の間で乱数を作成
float rand = Random.Range (0, totalPer);


その乱数から各対象の確率を引いていき、0未満になった時の対象が抽選されると言った具合です。

//乱数から各確率を引いていき、0未満になったら終了
foreach (KeyValuePair<T, float> pair in targetDict) {
  rand -= pair.Value;

  if(rand < 0){
    return pair.Key;
  }
}


確率が大きいほど、引く値も大きくなるので抽選される確率が上がるという事ですな。


おわりに

確率判定自体は今回紹介した方法で可能ですが、そもそも

実装する確率がそれで大丈夫か?確率で判定するのが正しいか?という事が重要です。


よくある誤解で「100回に1回ぐらい当てたいから1%」ではないってやつです。

つい最近も話題になりましたね。



100回に1回ぐらい当てたいのであれば、単純に確率で判定するのではなく、

クジ引きのような処理をした方が近い体感を得られると思います。

100本のうち1本だけ当たりを用意して、抽選する度にクジを無くしていくみたいな感じ。


そういえば、そういう形式のガチャもありましたね〜(:3っ)∋〜