サイト内を検索

ソーシャルゲーム開発のベストプラクティス

はじめに

こんにちは。 サーバーエンジニアのUです。

 

ココネに入社してからおよそ半年が経ちました。
前職では10年近くソーシャルゲーム開発に携わっていたので、ココネ入社後は同様のシステムを構築するのにもさまざまなアプローチがあるものだなと感心する一方、その中でも変わらない部分があるという点について興味深く感じていました。このような設計や実装の普遍性は実際的ゆえにベストプラクティスたりうるのではと思い、ソーシャルゲームによくある機能の設計と実装をいくつかピックアップしてベストプラクティスとしてまとめてみました。

アイテム管理

ソーシャルゲームには様々な種類のアイテムが登場します。アイテムの管理、特にその種類の管理やその所持数の増減は主要なゲームパートにとって、もっとも重要な要素といえます。とくにソーシャルゲームのような継続的な開発が前提となるサービスにおいてアイテムの追加を前提とする管理の仕組みの重要度は高いでしょう。

 

アイテム管理は慎重に設計する必要はあるものの、要点さえ理解してさえいればさほど難しいものではありません。実装に際して特に以下の二点を重視します。

 

  • 一意に同定できること
  • 拡張性があること

 

それぞれについて説明します。

一意に同定できること

アイテムはゲーム中のすべての文脈から一意に同定できるようにします。これは実装のみならず運用においても重要です。というのもソーシャルゲームではアイテムを指定するシーンがとにかく多いのです。都度実装や設定などを行わずすべてのアイテムを一つの仕組みの中で扱うために、どの文脈からも一意に同定できるようにすると良いです。

 

方法はシンプルで、アイテム種類ごとに固有のIDを割り振るだけです。ただし、すべてのアイテムに対して一律にIDを割り振ると管理は困難なものになるため、管理しやすい採番方法を選択することが重要になります。

 

アイテム種類を機能ごとに分類してIDを割り振るのが良いでしょう。身近なソーシャルゲームを想像してみてください。「装備」「スタミナ回復アイテム」「ガチャチケット」「通貨」など機能ごとに分類できることがわかると思います。

 

IDはマスターデータとの連携も踏まえて、複数の値でユニークになるよう設計すると扱いやすくなります。複数の値は連結して文字列として扱っても良いでしょう。

 

ただし、ゼロパディングしたり、10000台はA、20000台はBのように一つの数値として連結する方法は推奨しません。これらの方法は拡張性が低いためです。

 

手法 Good/Bad
二値以上の数値 1, 2 Good
区切り文字付きで文字列として連結 1-2 Good
ゼロパディングを使う 01, 0002

01-0002

Bad
方針転換や見積りの甘さなどによって想定した桁数を越える可能性がある。数値と文字列の変換に際して桁数やゼロから始まるという追加情報があることによりパースや入力が複雑になる。

文字数が多くなるので単純にデータ量が増える。文字列にしたときに並べやすいという利点もある。

桁を増やして結合する 10002 Bad
上記と同様。
一値の数値 1 Bad
アイテムの種類が豊富なゲームにおいてIDでカテゴライズできないと単純に不便。カテゴリ情報を別途持たせるようにすると、運用と実装のどちらのシーンにおいても複雑になる。

拡張性があること

アイテム種類の増加というのは案外予想できないもので、実装当時の計画で月に一回のイベントでのみ新アイテムを追加するといった想定でアイテム種類の最大値を三桁に設定したものの、別の機能によってアイテムを大量に追加することになり、桁が足りなくなるというようなことはよく起こります。

 

ソーシャルゲームはお客様を喜ばせるだけでなく、ときに驚かせたりしながら、継続して開発を行うという性質上、こういった予想しにくい仕様の拡張がよく起こります。よほど長期的な計画を持っていない限り、予期できないことも多いでしょう。

 

それゆえに桁をあらかじめ決めてしまうことには相応のリスクが伴います。

 

また、ソーシャルゲームではアイテムは垂直方向だけでなく水平方向にも増加します。これらを念頭に、拡張可能な仕組みを構築すると良いでしょう。

スタミナ管理

時間経過により自然回復するいわゆるスタミナと呼ばれる仕組みもソーシャルゲームにはつきものです。

 

クライアント・サーバー間のスタミナを同期しようと、変更ごとに通信するのはよくある誤りです。

 

スタミナ管理において、クライアント・サーバー間でのやりとりが必要なのは(オーソドックスな仕様であれば)『消費時』と『自然回復以外の手段による回復時』の二つのタイミングのみで、その際にやりとりするデータは『自然回復によりスタミナが最大値に到達する予定の時刻』と『最大値を超えて追加されたスタミナ』の二つだけで良いです。

 

前述の『自然回復によりスタミナが最大値に到達する予定の時刻』と『最大値を超えて追加されたスタミナ』に加え、通常マスターデータで設定される『スタミナの最大値』と『スタミナを1回復するのにかかる時間』の合計四つの値からスタミナの現在値を求めることができます。

 

以下は実装の例です。

class StaminaCalculator
  attr_reader :completion_time, :overflowed_stamina
 
  # 最大値
  MAX_STAMINA = 100
 
  # 1回復するのにかかる時間(秒)
  SECONDS_PER_STAMINA = 60
 
  def initialize(completion_time, overflowed_stamina, now)
    @completion_time = completion_time
    @overflowed_stamina = overflowed_stamina
    @now = now
 
    fix!
  end
 
  def full?
    @now >= @completion_time
  end
 
  def value
    t = full? ? MAX_STAMINA : [MAX_STAMINA - ((@completion_time - @now) / SECONDS_PER_STAMINA.to_f).ceil, 0].max
    t + @overflowed_stamina
  end
 
  def seconds_for_max
    return 0 if @now >= @completion_time
 
    @completion_time - @now
  end
 
  def seconds_for_next
    sfm = seconds_for_max
    return sfm if sfm.zero?
 
    ret = seconds_for_max % SECONDS_PER_STAMINA
    ret.zero? ? SECONDS_PER_STAMINA : ret
  end
 
  def to_empty!
    @overflowed_stamina = 0
    @completion_time = @now + (SECONDS_PER_STAMINA * MAX_STAMINA)
 
    self
  end
 
  def to_fill!
    return self if full?
 
    @completion_time = @now
 
    self
  end
 
  def increase!(v)
    if @overflowed_stamina > 0
      @overflowed_stamina += v
      return self
    end
 
    diff = MAX_STAMINA - value
    if diff >= v
      @completion_time += SECONDS_PER_STAMINA * v
    else
      @completion_time = @now
      @overflowed_stamina = v - diff
    end
 
    self
  end
 
  def decrease!(v)
    raise if value < v if @overflowed_stamina >= v
      @overflowed_stamina -= v
      return self
    end
 
    v -= @overflowed_stamina
    @overflowed_stamina = 0
 
    @completion_time += (v * SECONDS_PER_STAMINA)
 
    self
  end
 
  private
 
  def fix!
    @completion_time = [@completion_time, @now].max
  end
end

どの言語でもさほど多くないコード量でスタミナ関連の実装が完結することが想像できるかと思います。

 

回復量を非線形にしたり、端数の扱い方を変えたりという程度であれば、保存するデータとそのタイミングは変えずに(つまりリクエスト数を増やすことなく)対応が可能です。また、満腹度のような自然に減少していくような仕組みも同様に対応が可能でしょう。

ユーザーアクセスごとの遅延評価

ソーシャルゲームのデータはユーザーごとに紐付くものが大部分を占めます。この性質はユーザーアクセスごとの遅延評価と相性がよく、これを積極的に活用することで運用上発生しうるいくつかのリスクを低減することができます。

 

ソーシャルゲームをプレイしていると全ユーザーに対して付与されたであろうアイテムを受け取ることがあります。このような場面においては通常バッチ処理ではなく遅延評価によってアイテム付与を行います。具体的な手順は次のとおりです。

 

  1. 『全ユーザー宛のアイテム付与用テーブル』にレコードを1件登録する
  2. ユーザーがホーム画面(あるいはアイテムボックスのバッジを表示するようなAPIなど)にアクセスする
    1. 『全ユーザー宛のアイテム付与用テーブル』から登録済みか確認
    2. 登録済みでない場合そのユーザーの『アイテムボックス用テーブル』にレコードを登録する

 

このような遅延評価の手法は全ユーザー宛のアイテム付与に限らず、さまざまな場面で採用できます。対象も全ユーザー宛だけでなくイベント報酬の付与など特定多数のユーザーに対しても、事前にユーザーが特定要件を満たすか検証を行うことで同様に可能です。

 

ただし、どのAPIでこれを行うかについては注意が必要で、たとえばアイテムを付与するのであれば、アイテムボックスにバッジを表示するタイミングではすでにアイテムは付与されているべきですし、必要なユーザーがプレイ中に必ずそのAPIに到達するようになっていないといけません。

 

バッチ処理をこのような遅延評価に置き換えることで、トラフィックのスパイクを無くしたり、必要なユーザーデータのみ更新することができるので、最終的なデータ更新量を減らすことができます。遅延評価はソーシャルゲームにとっては特に有効な手段となります。

最後に

いかがだったでしょうか? 今回はソーシャルゲーム開発における頻出機能である「アイテム管理」「スタミナ管理」「ユーザーアクセスごとの遅延評価」の設計と実装についてまとめてみました。これからソーシャルゲーム開発に携わろうとしている方にとって、より身近でイメージしやすいものになっていれば幸いです。

ココネエンジニアリングでは一緒に働く仲間を募集中です。

ご興味のある方は、ぜひこちらのエンジニア採用サイトをご覧ください。

→ココネエンジニアリング株式会社エンジニアの求人一覧

Tag

Category

Tag