はじめに
ソーシャルアプリのガチャを作ったときの話。
無作為にデータを取り出すプログラムを書いたとき「なんだか偏るなぁ」と感じたので、
自分の中でどのやり方が課金者に平等で無作為(そして簡単に)にデータを取り出せるか調べてみたことを書きます。
使用している言語
PHP7.0.0、データベースはMySQL
そもそもMySQLでランダムにレコードを取得してはいけない?
SELECT * FROM 'gacha' ORDER BY RAND() LIMIT 1;
超簡単に取得できる方法ではありますが、SQLアンチパターンであるので却下しました。
PHPで取得したデータからランダムに取得
rand()
、mt_rand()
どちらも擬似乱数を整数で返してくれるもの。
mt_randの方が数倍高速だということですので使うならmt_randかなぁと思いましたが
暗号学的に安全ではないようなのです。
PHP7以降では暗号学的に安全と言われているrandom_int()
が存在します。
PHP7以降で暗号化に使う場合であればこちら推奨されています。
引数を省略することはできませんが、基本的な使い方自体はmt_rand()
と変わらないので置き換えも簡単にできるかと思います。
ではランダム関数使うならrandom_intを使いましょうという話なのか?
安全面を考えるとrandom_int()
でいいと思います。
ただ、速度はmt_randの方が早そうなので、単純に乱数が欲しいだけならmt_randで十分だと思います。
stackoverflowに速度を検証された方がいたようです。
PHP Rand()vs. random_int()
結論
課金が絡むようなできる限り不具合なく安全で公平なシャッフルを望むのであれば多少速度が遅かろうがrandom_int()
を使った方がいいと判断しましたので、PHP7以降であればrandom_int()
、それ以下であればmt_rand()
を使用するとします。
本題
ただ、プログラムで完璧な無作為を行うことは不可能なので関数を利用しつつ
仕組みでカバーすることにします。
つくるガチャについて(ここからは興味のある人だけお読みください)
ここで、どんなガチャがあるのか覚えている限り書きます(備忘録)
ランダム型
データベースから1件ランダムで排出するものを決定する一番作りやすいガチャです。
以前私が作っていたものもこの形で、データのレコード数を調整することで確率を収束させていました。
問題点は当たらない時はいくらお金をつぎ込んでも当たらない、という点です。
コンプリート型
例えば10種類のアイテムを全てガチャで引けたら今回の目玉のアイテムをもらえるといったものです。
欲しくもないアイテムをひたすら狙い続ける上にランダム型ベースなので確率も上がらない最も闇が深いガチャです。
現在は規制が入り、使えません
ボックス型
コンプリート型が規制されたことによってよく使われるようになったガチャです。
イメージは実物のガチャポンのように袋の中から一つずつ引いていき、いずれ空になるので欲しいものが確実に手に入るガチャです。
基本的に空になるともう課金されないため、売り上げの上限が決まってしまうという問題があったので
目玉のアイテムを複数集めるメリットを用意した上で、目玉のアイテムをユーザーが手に入れた段階でボックスをリセットできる機能をつけた派生系のものが生まれてきました。
いずれ100%欲しいものが欲しいだけ手に入る分コンプリート型よりは良心的になりました。
ステップアップ型
ガチャを引くとステップが上がるとともにオマケなどがもらえて、ステップが上がるほどにオマケやお得なアイテムが豪華になっていくガチャです。
また、頭打ちにならないように10ステップまで進むと1ステップに戻るものもあります。その場合は10ステップ目に目玉アイテムがある場合が多いです。
ボックス型よりも比較的早く目的のアイテムが確実に手に入るのでわかりやすいです。
テーブル型
ランダム性を完全に取り除いて最初からガチャで出る順番をデータに全て登録しておく、というものです。
これはかなり古いやりかたでデータをいい感じに登録する作業が面倒なのでやっているゲームはもうないかもしれません。
今回は単純に確率の話なので「ランダム型」でいきます。
以下のようなデータを用意します。
実際作ったものとは少々異なりますがイメージはこんな感じです。
gacha_item テーブル
id | gacha_id | rarity | item_name | min_ratio | max_ratio |
---|---|---|---|---|---|
1 | 1 | 5 | アイテムSSR | 0 | 10 |
2 | 1 | 4 | アイテムSR | 11 | 100 |
3 | 1 | 4 | アイテムSR | 101 | 500 |
4 | 1 | 4 | アイテムSR | 501 | 1000 |
5 | 2 | 4 | アイテムSR | 0 | 200 |
6 | 2 | 3 | アイテムR | 201 | 500 |
7 | 2 | 3 | アイテムR | 501 | 1000 |
8 | 3 | 2 | アイテムUC | 0 | 250 |
9 | 3 | 2 | アイテムUC | 251 | 500 |
10 | 3 | 2 | アイテムUC | 501 | 750 |
11 | 3 | 2 | アイテムUC | 751 | 1000 |
フロー
やりたいこと。
- ユーザーがガチャを回す
- ガチャIDをランダムで取得
- ガチャIDの中からランダムで1件取得
- データベースから絞り込んだアイテムを取得
- ユーザーがアイテムをGET
$a = random_int(1, 3); // ガチャ&演出決定
$b = random_int(0, 1000); // ratio決定
$Item = getGachaItem($a,$b); // データベースから排出アイテム取得
// sqlはこんな感じ
SELECT * FROM 'gacha_item' WHERE 'gacha_id' = $a AND 'min_ratio' <= $b AND 'max_ratio' >= $b LIMIT 1;
一発でテーブルから1件取得するのではなく、ガチャIDを先にランダムで選ぶことで無作為性を上げています。
これで、簡単な仕組みのガチャができました。
あと必要なのはゲームバランス等を加味した排出率の計算をします。
データの設定ができたら後は10万回ほど回してみて排出率を出します。
データがでたらどれくらいのユーザーが目玉のアイテムを手に入れて欲しいかを考えます。
アクティブユーザー数や平均のガチャ回数を見たり、そのアイテムがゲームプレイに与える影響の大きさ(ゲームバランス)を鑑みて排出率を決定すればOKです。
余談:ガチャ演出について
gacha_idはガチャを入れ物で分けて無作為性を上げていますが「ガチャ演出の切り替え」に使う場合もあります。
レアリティによって演出を変えてもいいんですが、上位レアリティを増やすときでも演出を増やしやすいというメリットがあります。
例:
gacha_idが1の場合はrarityが高いものが入っているあたりガチャ。演出も派手。
gacha_idが2の場合は少しrarityが低いものが入っています。演出は少し派手。
gacha_idが3の場合はrarityが低いものが入っています。演出はノーマル。