ソーシャルゲーム開発における「報酬」「消費」「条件」のテーブル設計と機能

本記事は[QualiArts Advent Calendar 2021](https://qiita.com/advent-calendar/2021/qualiarts) 18日目の記事です。
今回は、ソーシャルゲームにおいて、どんなゲームにも必要となる基盤機能のテーブル設計と機能について紹介していきたいと思います。

# はじめに
　2008年にスマートフォンが日本で始動して以来、多種多様のソーシャルゲームが開発運用されてきました。ゲームはその他のドメインのWebアプリケーションと比較して、ロジックが複雑化しやすく、その分必要となるテーブル数も多くなるという特徴を持っており、運用の長いタイトルでは1000テーブルを超えることもあります。当然の話ですが、ゲーム毎に仕様が大きく異なり、仕様に合わせて機能の設計や実装を工夫しています。しかし、どんなゲームでも必要とされる機能もあり、その中の一部が「報酬」「消費」「条件」を担う機能です。

##### 報酬 (Reward)
　「報酬」はユーザーにリソースを付与する機能です。当社では伝統的に「Reward」という名前を使っています。例えば、クエストをクリアした時/ミッションを達成した時/ガチャを引いた時に、カード/アイテム/コインなどゲーム内で使用することのできるリソースを獲得できると思います。このようなユーザーに報酬を付与する仕組みを「Reward」という機能で実現しています。
##### 消費 (Consumption)
　「消費」はユーザーのリソースを削減する機能です。当社では伝統的に「Consumption」という名前を使っています。例えば、クエストに挑戦する時にスタミナが減少する/アイテムを交換するときにチケットを消費する/ガチャを引くときにゲーム内通貨を消費するなど、所持しているリソースが削減される機会があると思います。このようなユーザーのリソースを消費する仕組みを「Consumption」という機能で実現しています。
##### 条件 (Condition)
　「条件」はユーザーの状態に合わせて処理を分岐させるための判定を行う機能です。当社では伝統的に「Condition」という名前を使っています。例えば、クエスト1をクリアするとクエスト2に挑戦できるようになる/n月n日~m月m日の間はイベントが開放される/カードの能力値が一定の値を超えていればストーリーを閲覧できるなど、特定の条件を満たすことで機能が解放される場面を見かけることがあると思います。このような様々な条件について判定を行う仕組みを「Condition」という機能で実現しています。

　本記事ではこれらの機能について、リレーショナルデータベースにおけるテーブル設計や機能を紹介していきたいと思います。なお、これらの機能はQualiArtsや、その前身のAmeba Gamesなどで古くから開発されてきた機能であるため、細かな設計や機能がプロジェクトによって異なっています。従って、本記事での解説は私が携わっているプロジェクトにおける設計・機能になることをご了承ください。

# Rewardのテーブル設計と機能
### テーブル設計
##### reward_set
| カラム名         | 型       | 説明                                                   |
|:----------------|:--------|:-------------------------------------------------------|
| id              | string  | 報酬のID。このテーブルの複合主キー                           |
| number          | integer | 同一ID内での連番。このテーブルの複合主キー                    |
| reward_set_type | enum    | Probability or Ratio (確率による抽選か重みによる抽選か指定) 。|
| rate            | integer | 付与確率。RewardSetTypeの値に応じてふるまいが変わる。         |
| resource_type   | enum    | ResourceIDのリソース種別を指定。(Itemなど)                  |
| resource_id     | string  | 付与リソースのID。(CardIDやItemIDなど)                     |
| quantity_max    | integer | 付与個数の最大値。                                        |
| quantity_min    | integer | 付与個数の最低値。                                        |

　Rewardで使用するテーブルは「reward_set」という名前にして、idとnumberからなる複合主キーのテーブルにしています。ゲームの報酬では大抵の場合、複数の種類のリソースが付与されます。また、複数の種類の報酬の中から一つが確率で付与されるといった設定もしばしば用いられるため、1つのidに対して複数の報酬が紐づけられるようにしています。クエストやガチャなど報酬付与を行う機能のテーブルにreward_setのidを持たせることで報酬内容を柔軟に変更しています。

### 機能解説
　Rewardの基本的な機能はreward_setに設定してあるリソース(resource_type, resource_idから判別)を指定個数分(quantity_max, quantity_minから算出)付与する機能です。また、ゲームの複雑な要件に柔軟に対応していくためにいくつかの機能を実装しているため、それらを紹介していきたいと思います。

##### 確率抽選
　「確率抽選」は指定した確率に応じて、その報酬が付与されるか否か変化させる機能です。reward_set_typeを「Probability」に設定することでrateで指定した確率で報酬が付与されるようになります。確率判定は同一idの報酬に対して1つずつ実行されます。
　下記のデータを例に動作を説明します。下記のデータは2つのItem(item_id=i1,i2)を付与する設定です。rateにはi1とi2のそれぞれの付与確率が設定されています(簡単化のために単位は%にしています)。つまり、下記のデータは60%の確率でi1を1つ付与し、さらに50%の確率でi2を1つ付与する設定になります。
　また、rateに100%を超える値を設定した場合は、100%を1単位として複数回の付与処理が発生するようにしています。例えば、250%を指定した場合は、100%+100%+50%と解釈して付与個数が2倍~3倍になるようにしています。

| id | number | reward_set_type | rate | resource_type | resource_id | quantity_max | quantity_min |
|:---|:-------|:--------------|:-----|:-------------|:-----------|:------------|:------------|
| set1 | 1 | Probability | 60 | Item | i1 | 1 | 1 |
| set1 | 2 | Probability | 50 | Item | i2 | 1 | 1 |

##### 重み抽選
　「重み抽選」は複数の報酬の中からrateに指定した重みに従って１つの報酬を付与する機能です。reward_set_typeを「Ratio」に設定することで重みによる抽選が実行されるようになります。
　下記のデータを例に動作を説明します。下記のデータはi1とi2とi3の中から1つを付与する設定です。rateにはそれぞれのItemの重みが設定されています。このデータに従って報酬付与を実行すると、i1:i2:i3が1:1:2の割合でいずれか1種類が付与されます。つまり、25%の確率でi1が1つ、25%の確率でi2が1つ、残り50%の確率でi3が1つ付与される設定になります。

| id | number | reward_set_type | rate | resource_type | resource_id | quantity_max | quantity_min |
|:---|:-------|:--------------|:-----|:-------------|:-----------|:------------|:------------|
| set1 | 1 | Ratio | 1 | Item | i1 | 1 | 1 |
| set1 | 2 | Ratio | 1 | Item | i2 | 1 | 1 |
| set1 | 3 | Ratio | 2 | Item | i3 | 1 | 1 |

##### 付与個数のランダム化
　reward_setは付与個数をランダムに変化させるためにquantity_maxとquantity_minの2つの付与個数用カラムを持っています。確率抽選や重み抽選の例では両者とも同一の値を設定しているため、付与個数は固定になっています。quantity_maxとquantity_minに別の値を設定することでその値の間でランダムに付与個数を変化させるようにしています。例えば、下記のデータはi1を1~3つ付与する設定となります。

| id | number | reward_set_type | rate | resource_type | resource_id | quantity_max | quantity_min |
|:---|:-------|:--------------|:-----|:-------------|:-----------|:------------|:------------|
| set1 | 1 | Ratio | 1 | Item | i1 | 3 | 1 |

##### reward_setの再帰的参照
　reward_setのresource_idにreward_set内の他のidを指定することで、reward_setを再帰的に展開して報酬を付与できるようにしています。この機能によって、より柔軟かつ効率的な運用を可能にしています。自身のidや循環が発生するような参照を設定してしまうと無限ループになってしまうので、そのような設定はできないように制御しています。
　下記のデータを例に動作を説明します。下記のデータのうちset1はset2とset3をそれぞれ100%の確率で付与する設定になります。set2はi1とi2のうちどちらかを1つ付与、set3はi3とi4のうちどちらかを1つ付与する設定であるため、set1はi1かi2を1つ、さらにi3かi4を1つ付与する設定です。
　ちなみに、reward_setに対しても付与個数をランダムにしたり、確率抽選で100%を超える値が設定できるように実装しています。

| id | number | reward_set_type | rate | resource_type | resource_id | quantity_max | quantity_min |
|:---|:-------|:--------------|:-----|:-------------|:-----------|:------------|:------------|
| set1 | 1 | Probability | 100 | Set | set2 | 1 | 1 |
| set1 | 2 | Probability | 100 | Set | set3 | 1 | 1 |
| set2 | 1 | Ratio | 1 | Item | i1 | 1 | 1 |
| set2 | 2 | Ratio | 1 | Item | i2 | 1 | 1 |
| set3 | 1 | Ratio | 1 | Item | i3 | 1 | 1 |
| set3 | 2 | Ratio | 1 | Item | i4 | 1 | 1 |

# Consumptionのテーブル設計と機能
### テーブル設計
##### consumption_set
| カラム名       | 型       | 説明                                   |
|:--------------|:--------|:--------------------------------------|
| id            | string  | 消費のID。このテーブルの複合主キー          |
| number        | integer | 同一ID内での連番。このテーブルの複合主キー   |
| resource_type | enum    | ResourceIDのリソース種別を指定。(Itemなど) |
| resource_id   | string  | 消費リソースのID。(ItemIDなど)            |
| quantity      | integer | 消費個数。                              |

　ConsumptionもRewardと同様に「consumption_set」という名前のidとnumberの複合主キーからなるテーブルで複数種類のリソースを1度に消費できるようにしています。

### 機能解説
　Consumptionの基本的な機能はconsumption_setに設定してあるリソース(resource_type, resource_idから判別)を指定個数分(quantity)消費する機能です。Rewardとよく似ていますが、Consumptionに関してはRewardほど複雑なユースケースが存在していないため、比較してシンプルな設計・機能になっています。

##### consumption_setの再帰的参照
　reward_setの再帰的参照と同様にconsumption_setも再帰的に消費物を設定できるように、resource_idにconsumption_setのidを指定することができます。例えば、下記のデータのうちset1はset2とset3を参照しているため、i1とi2を消費する設定になります。

| id | number | resource_type | resource_id | quantity |
|:---|:-------|:--------------|:------------|:---------|
| set1 | 1 | Set | set2 | 1 |
| set1 | 2 | Set | set3 | 1 |
| set2 | 1 | Item | i1 | 1 |
| set3 | 1 | Item | i2 | 1 |

# Conditionのテーブル設計と機能
### テーブル設計
##### condition_set
| カラム名       | 型       | 説明                                   |
|:--------------|:--------|:--------------------------------------|
| id            | string  | 条件のID。このテーブルの複合主キー          |
| number        | integer | 同一ID内での連番。このテーブルの複合主キー   |
| operator_type | enum    | AND or OR (AND条件かOR条件か)           |
| condition_type| enum    | 条件種別を指定。(後ほど解説)               |
| resource_id   | string  | 条件に使われるリソースのID。               |
| max           | integer | 条件に使われるリソースの個数の最大値。       |
| min           | integer | 条件に使われるリソースの個数の最小値。       |

　Conditionも複雑な条件を表現するためにidとnumberの複合主キーからなるテーブルとしています。condition_typeによって、設定に必須な情報(resource_id,max,min)が変わります。

### 機能解説
　Conditionの基本的な機能はcondition_setに設定してある条件を満たすかどうか判定する機能です。operator_typeやcondition_typeの組合せによって複雑な条件を表現することができます。

##### condition_type
　Conditionの機能の解説の前にcondition_typeについて紹介しておきます。condition_typeはその条件がどういった判定を行うものか判別するための情報です。例えば、よく使われる条件として下記のような値が考えられます。また、特殊な値としてSetとNegativeSetを用意しており、こちらは後述の「condition_setの再帰的参照」で紹介します。

| 値          | 説明     |
|:------------|:--------|
| CardLevel   | resource_idに指定したカードのレベルがmin~maxの範囲内であればtrue |
| QuestClear  | resource_idに指定したクエストをクリアしていればtrue             |
| Set         | resource_idに指定したcondition_setのidの判定がtrueならtrue    |
| NegativeSet | resource_idに指定したcondition_setのidの判定がfalseならtrue   |

##### 複数条件のANDとOR
　複雑な条件を表現するために論理演算子のANDとORをoperator_typeに指定して切り替えられるようにしています。データの入力難易度を下げるために、連番でソートして先頭のレコードのoperator_typeを参照し、そのsetがAND条件なのか、OR条件なのか判別するような実装を行なっています。
　下記のデータを例に動作を説明します。下記のset1、set2はそれぞれAND、ORを使用した設定になっています。set1はc1とc2のレベルが共に50~100の範囲内であればtrueとなる条件を表現しています。また、set2はq1かq2のどちらかをクリアしていればtrueとなる条件を表現しています。

| id | number | operator_type | condition_type | resource_id | max | min |
|:---|:-------|:--------------|:------------|:---------|:---------|:---------|
| set1 | 1 | AND | CardLevel | c1 | 100 | 50 |
| set1 | 2 | | CardLevel | c2 | 100 | 50 |
| set2 | 1 | OR | QuestClear | q1 | | |
| set2 | 2 | | QuestClear | q2 | | |

##### condition_setの再帰的参照
　ANDとOR条件に加え、さらに複雑な条件を表現可能とするために、condition_setを再帰的に参照できるようにしています。condition_setの再帰的参照ではcondtion_typeにSetかNegativeSetを指定します。NegativeSetを使用することで否定の条件を表現することができるようになっています。
　下記のデータを例に動作を説明します。下記のデータのうちset1はset2とset3をAND条件で参照しています。さらにset2はcondition_type=Set、set3はcondition_type=NegativeSetで参照しており、「set2を満たし、かつ、set3を満たさない」場合にtrueとなる条件を表現しています。つまり、set1はq1とq2をクリアしていてq3をクリアしていない場合にtrueとなる条件になります。

| id | number | operator_type | condition_type | resource_id | max | min |
|:---|:-------|:--------------|:------------|:---------|:---------|:---------|
| set1 | 1 | AND | Set | set2 | | |
| set1 | 2 | | NegativeSet | set3 | | |
| set2 | 1 | AND | QuestClear | q1 | | |
| set2 | 2 | | QuestClear | q2 | | |
| set3 | 1 | AND | QuestClear | q3 | | |

# おわりに
　本記事ではソーシャルゲームの「報酬」「消費」「条件」について、リレーショナルデータベースにおけるテーブル設計と実装している機能について紹介しました。これらの機能はゲームというドメイン以外でも活用機会のある汎用的な機能だと思っているので、開発の参考になれば幸いです。ご一読いただきありがとうございました。