ホーム 職種別 エンジニア ソーシャルゲームにおけるミッション機能のサーバーサイド実装の考察:思考編
 

ソーシャルゲームにおけるミッション機能のサーバーサイド実装の考察:思考編

アピリッツ コンテンツデザイン部の金井と申します。前回に引き続き、ソーシャルゲームにおけるミッション機能に対する考察をしていきます。

関連:ソーシャルゲームにおけるミッション機能のサーバーサイド実装の考察:問題編

前回のあらすじ

 ミッション機能というものは構造的に、

  • 全てのAPIに影響が及ぶが故に改修へのQAコストが非常に高く
  • 処理に時間が掛かりやすく
  • ユーザーデータが膨大になるが故にそれらの問題が更に面倒な事になりやすい

 ものである。
 また、実際に実装/改修するとしても、仕様面の都合で

  • ミッション受注処理
  • ミッション進捗処理
  • ミッション報酬受け取り処理

 の3つが複雑に絡み合う事が多く、所謂スパゲッティコードというものに、しかも見るからに不味いそれになりやすい。

平和だった日常回を思い出しながら仕事に手を付ける覚悟をしたエンジニア

どのように実装/改修すべきか

注:

 ここから記述する内容は、”これから実装する“もしくは”既存のソースを流用して新しいゲームを開発する“と言った、運用中ではない事を前提としたものとなります。
 既存のミッション機能が如何にクソコード汚いものでも改修する事によって得られるメリットは、精々、

  • レスポンスが早くなる
  • コードが見やすくなる
  • 改修等に伴うバグの発生率を減らす事が出来る

等と言ったユーザーからは見えないものが多く、更に

  • 影響が膨大な以上、QAを入念に介してもバグを除き切れずに本番障害を引き起こす可能性が高い
  • バグを引き起こした場合の長時間メンテ、修正、補填などで大きな損害も被る
  • マスタの改修なども必要となった場合、マスタデータを作るExcel…これまで使用していたマクロなども改修しなければいけない事となり、プランナー側、そしてクライアント側にも大きな負担が掛かる

 と言ったそれ以上のデメリットが膨大です。
 如何にエンジンが錆び付いてガションガションうるさい音を立てていようとも、熱効率が悪かろうとも、動いている状態のまま幾つかの部品を取り替える事なんて早々出来ないですからね。
 それでもやらなければいけないとなる場合は、レスポンスの重さでユーザー離脱が激しいとか、原因不明のバグが頻発して、更に手をつけようにも秘伝のソース過ぎて壊さないとどうしようも出来ないとかそんな、それを使い続ける事で極度の不利益が出る場合に限るでしょう。
 そうでない場合は、その汚いコードと付き合っていくのがベストです。
 ベストです。
 ベストなんですよ。悲しい事に。とても、とても悲しい事に。

出社直後、イベントにて大量の期間限定ミッションが追加された事により、特定のAPIの平均レスポンスが5秒以上掛かっている事を発見したエンジニア

普遍的に良く見られるであろう悪くなっていくコード

 まず前回でも書いたような、カードの強化で例えてみましょう。
 カードを強化させた回数のミッション指定アイテムを手に入れた回数のミッションが存在するとします。
 そして、カードを最大レベルまで強化するとカードマスタに指定されたアイテムが貰えるとします。
 すると、さっくり疑似コードとして書いてみると以下のようになるでしょう。

class User < ActiveRecord::Base
  def カード強化(カードID, 使用素材群, 時刻)
    カードが存在するか確認
    カードが既に最大レベルでないか確認
    使用素材群それぞれに対し do
     消費数分持っているか、またそれらがレベルアップ用アイテムか確認
    消費数分の減算、獲得経験値を計算
    end

    カードをレベルアップ(獲得経験値)
    カードを強化したミッションを進捗させる

    if カードが最大レベルならば
     アイテム付与(カードマスタに定義されている報酬アイテムマスタコード, カードマスタに定義されている報酬アイテム量, 時刻)
    end
  end

  def アイテム付与(アイテムマスタコード, 付与量, 時刻)
    指定されたアイテムマスタが存在するか、また受け取れる時間であるか確認
    ユーザーにアイテムを付与
    アイテムを獲得したミッションを進捗させる
  end
end

 こんな感じですね。
 確かにミッションがシンプルな仕様ならば、そんなに時間も掛からないでしょう。
 しかし例えばカードの強化を促進させる施策の一環で、カードを指定レベルまで成長させたカードのレベルを最大まで上げたというミッションが追加されたとすると、最大、合計で4つのミッションの進捗確認を1個1個する事になります。
 段々雲行きが怪しくなってきましたね!

 そして今度はミッションを進捗させる方を詳しく見ていきましょう。分かりやすさの為に、ミッション進捗は別のクラスに纏めましょうか。

class ミッション進捗
  def 進捗させる(ユーザー, 進捗させるミッション種, 進捗させるミッションカウント, ...)
    進捗させるミッション種に属する、現在有効なミッション群を確認する
    そのミッション群に対して do
      ユーザーの進捗を加算する
    end
  end
end

 簡単に書くとこうなりますね。
 さて。ここで例えば前回でも書いたような、ミッションの仕様に良くある、ミッションをクリアしたら次のミッションが解放されるという仕様が入ってくるとこうなりますね。

class ミッション進捗
  class << self
    def 進捗させる(ユーザー, 進捗させるミッション種, 進捗させるミッションカウント, ...)
      進捗させるミッション種に属する、現在有効なミッション群を確認する
      そのミッション群に対して do
        ユーザーの進捗を加算する
        if クリアしたならば
          それのクリアを前提条件とするミッション群を解放させる
        end
      end
    end
  end
end

 シンプルにこのような感じになるでしょうね。
 さて。ミッション進捗そのものも重くなりましたね。それがカードを強化するだけで最大4回走りますね。
 雨が降ってきたようです。
 そしてトドメに、ある特定のミッション種別だったらクリアまでではなく、報酬付与までするとの仕様が入ってきたとしましょうか。

class ミッション進捗
  class << self
    def 進捗させる(ユーザー, 進捗させるミッション種, 進捗させるミッションカウント, ...)
      進捗させるミッション種に属する、現在有効なミッション群を確認する
      そのミッション群に対して do
        ユーザーの進捗を加算する
        if クリアしたならば
          それのクリアを前提条件とするミッション群を解放させる
          そのミッションが特定の種別だった場合、報酬付与まで行う
        end
      end
    end
  end
end

 はい。報酬付与の中ではこのミッション進捗が呼ばれていますね。雷まで落ちてきたようです。

イベント中にバグが発生し、しかもそれが実装した人ももう居ない、誰も見たがらない秘伝のソースの中にあると分かった時のサーバーエンジニア達

こうならない為にはどうするべきか

 自分なりの考えとなりますが、結論としては単純です。

 同じようなコードを何度も通さない仕組みにすれば良い。

 ただ、それだけです。
 上のコードを書き換えるとこんな感じでしょうか。

class User < ActiveRecord::Base
  def カード強化(カードID, 使用素材群, 時刻)
    カードが存在するか確認
    カードが既に最大レベルでないか確認
    使用素材群それぞれに対し do
     消費数分持っているか、またそれらがレベルアップ用アイテムか確認
    消費数分の減算、獲得経験値を計算
    end

    カードをレベルアップ(獲得経験値)
    ミッション用にカードを強化したというデータを保持

    if カードが最大レベルならば
     アイテム付与(カードマスタに定義されている報酬アイテムマスタコード, カードマスタに定義されている報酬アイテム量, 時刻)
    end
  end

  def アイテム付与(アイテムマスタコード, 付与量, 時刻)
    指定されたアイテムマスタが存在するか、また受け取れる時間であるか確認
    ユーザーにアイテムを付与
    ミッション用にアイテム付与したというデータを保持
  end
end

 そしてカード強化の後、他に何もやらなくなったタイミングで保持したデータに対して一括でミッションを進捗させる。

class ミッション進捗
  class << self
    # 進捗させるミッションデータ群 = {ミッション種別: 進捗カウント}
    def 進捗させる(ユーザー, 進捗させるミッションデータ群, ...)
      進捗させるミッション種別群に属する、現在有効なミッション群を確認する
      そのミッション群に対して do
        ユーザーの進捗を加算する
      end
      クリアしたミッション群それぞれに対して do
        それのクリアを前提条件とするミッション群を解放させる
        そのミッションが特定の種別だった場合、報酬付与まで行う
      end

      if 報酬付与まで行なった場合
        ミッション進捗をまた呼ぶ
      end
    end
  end
end

 ざっくりこのような形ですね。
 工夫が足りず、このミッション進捗はこれでも複数回呼ばれる可能性はありますが、少なくとも元々の形よりは呼ばれる頻度は落ちるでしょう。
 カードを強化させた回数のミッション指定アイテムを手に入れた回数のミッションに加えてカードを指定レベルまで成長させたカードのレベルを最大まで上げたというミッションがあったとしても、このミッション進捗を通る回数は1回で変わらないのですから。

結論

 自分ではそれしか思いつきませんでした。
 ミッションという機能は性質上、1APIで様々なミッションの進捗が進むという事が絶対にあります。
 幾らそのミッションの進捗1回分を軽量化しようとしても、無理な軽量化はどこかに歪みを生みます。コードがより複雑になったり、新たな仕様によって軽量化の前提が上等な料理にハチミツをぶちまけるが如くに崩れる事があったり。
 1個1個に対して愚直に進捗を進ませるという事柄自体をやめなければ、ミッションという機能は健全に運営に組み込めないという結論です。
 しかし、それはやはり運営フェーズに入ってからは絶対に出来ない事です。
 全てのAPIに浸透し、隆盛しているようなそんな文化を一気にひっくり返すなど、濃口のラーメンが流行りの極みの時に薄口のラーメンのみを売り出して儲けを出そうとするようなものでしょう。

 次回は、そんなコードを実際に改修した時に起きた事などを書こうと思います。

原因が見つからないエンジニア

第三回はこちら:ソーシャルゲームにおけるミッション機能のサーバーサイド実装の考察:実践編

記事を共有
モバイルバージョンを終了