Python
ゲーム開発

ソシャゲチート対策 - サーバ編

株式会社gumiのサーバエンジニアとして、ゲームの開発や運営に携わっております。
今回はソーシャルゲームのサーバ側の開発を行う際に行っているチート対策について書きたいと思います。
とは申しましても、これはソシャゲに限ったことではないなぁと、書き終わった後で感じました。
あと、今回は文字ばかりでごめんなさい!(ネタ考えてる時間がありませんでした)

チートダメ!

チートできる穴を用意してしまうのもダメ!
ユーザが不正に手を染めないよう、染めさせないよう、我々はチートがまかり通らないように対策をする必要があると思っております。
その穴があったがため、つい魔がさしてチートしてしまい、好きだったゲームのアカウントが停止になってしまうなんて、ユーザも運営側も悲しいですよね。

基本

サーバ側のチート対策の基本姿勢は、「クライアントから送られてきた情報を信用しない」だと思ってます。
通信はいくらでも改変可能というつもりでサーバ側の処理を実装する必要があります。

今回は(自分にとっても)とっつきやすいと思われる、ゲームの基本機能としてよくある、アイテムの売買やクエスト関係のチート対策について書いていきます。
といっても、ただずらずら書くだけではつまらないので、クイズっぽくしたいと思います。
それぞれクラスを定義しまして、チートをチェックするクラスメソッドを用意しました。
そのメソッドの引数が通信で送られてくるパラメータとして、チート対策を考えてみてください。

問題

アイテム売買

ゲームにはアイテムがつきもので、たいていのゲームではアイテムの売買の機能が備わっていると思います。
それでは下のようなクラスでアイテムの売買の機能を実装する際、どのようなチートがあるか考えてみましょう。

player_item.py
from django.db import models


class PlayerItem(models.Model):
  player_id = models.IntegerField()
  item_id = models.IntegerField()
  quantity = models.IntegerField()

  @classmethod
  def check_item_purchase_parameter(cls, player_id, item_id, quantity):
    """
    アイテムの購入を行う前のチェック処理
    :param int player_id: 購入者の固有ID
    :param int item_id: 購入するアイテムのアイテムID
    :param int quantity: 購入するアイテムの個数
    """
    チートを防ぐ処理を考えましょう

  @classmethod
  def check_item_sell_parameter(cls, player_id, item_id, quantity):
    """
    アイテム売却を行う前のチェック処理
    :param int player_id: 売却者の固有ID
    :param int item_id: 売却するアイテムのアイテムID
    :param int quantity: 売却するアイテムの個数
    """
    チートを防ぐ処理を考えましょう

クエスト

クエストの一般的な流れは
1. クエストの一覧からプレイしたいクエストを選択し、スタミナを消費してクエストを開始する。
2. 敵を倒してクエストの報酬をもらう。
3. 例外的ですが、アプリが落ちてしまった場合、中断したクエストを再開する。

です。
これらの処理について、どのようなチートが考えられ、どのようにそれを防いだらよいでしょうか?
クエストの仕様によるところが大きいので、皆さんが普段遊んでいるゲームのことを思い浮かべながら考えてみてください。

quest.py
from django.db import models


class PlayerQuest(models.Model):
  player_id = models.IntegerField()
  quest_id = models.IntegerField()
  そのほか必要と思われるフィールド

  @classmethod
  def check_quest_start_parameter(cls, player_id, quest_id):
    """
    クエストを開始する際のチェック処理
    :param int player_id: クエストを開始するプレイヤーの固有ID
    :param int quest_id: 開始するクエストのクエストID
    """
    チートを防ぐ処理を考えましょう

  @classmethod
  def check_quest_clear_parameter(cls, player_id, quest_id, reward_list):
    """
    クエストクリア時のチェック処理
    :param int player_id: クエストをクリアしたプレイヤーの固有ID
    :param int quest_id: クリアしたクエストのクエストID
    :param list reward_list: クリアしたことでもらえる報酬のリスト
    """
    チートを防ぐ処理を考えましょう

  @classmethod
  def check_quest_restart_parameter(cls, player_id, quest_id):
    """
    クエスト再開時のチェック処理
    :param int player_id: クエストを再開するプレイヤーの固有ID
    :param int quest_id: 再会するクエストのクエストID
    """
    チートを防ぐ処理を考えましょう

解答編

アイテム売買

購入時チェック

  1. 購入するアイテムの個数が1以上か。 例えば-10個分のお金を支払うことで逆にお金が増えてしまいますね(お金の減算処理でもチェックはしておきましょう)。
  2. item_idがショップに置いてある商品か。 販売していない、貴重なアイテムを買えてしまいます。

売却時チェック

  1. item_idをユーザが所持しているか。 所持していない高額なアイテムを売れないようにします。
  2. 売却するアイテムの個数が1以上か。 レアアイテムを-999個売却することで逆に増やすというチートを防ぎます。

クエスト

クエスト開始時

  1. クエスト開始に必要なスタミナが残っているか。 これはスタミナ消費処理でチェックしても良いですね。
  2. quest_idがプレイ可能か。 クエストが開催している期間もありますが、それ以外にクエストをプレイするために条件が設定されているようであれば(例えばノーマルをクリアしないとハードモードのクエストがプレイできないとか)、その条件を満たしているかもチェックします。

クエストクリア時

  1. quest_idを開始していたか。 ちゃんとスタミナを消費してquest_idのクエストを開始しているかチェックします。クエストクリアの通信だけを繰り返し行ってタダで報酬を何回も貰うことを防ぎます。私が担当しているアプリではクエストのプレイ状態を保存するテーブルを用意して、そこの情報を参照してチェックしています。
  2. reward_listが正しい報酬か。 クライアントから送られてきた報酬を何もチェックしないで配布してしまうと、通信をいじって本来獲得できない、超レアなキャラやアイテムをゲットできてしまいます。私が担当しているアプリではクエストを開始するたびにドロップする報酬を抽選してDBに保存しており、クリア時に比較しております。

クエスト再開時

  1. quest_idを開始していたか。 これもクエストのクリアと同じですね。クエストを開始してないのに再開できてしまうとスタミナを使わずにクエストがプレイできてしまいますのでチェックしましょう。

まとめ

解答はごく一部で、これがすべてではないと思います。
仕様によっては半額セールや買取額アップ、報酬にボーナスが付いたりしますよね。スタミナ半分!っていうのもあります。
また、RPGですと戦闘コマンドを選択するとサーバと通信をしてバトルの結果を返す、という処理もあるかもしれません。
通信ごとに送信されてきたパラメータが正常かどうかをチェックするようにすればチートもかなりしにくくなると思います。
私が担当しているアプリではゲーム本体の操作をBigQueryに流し込んでチートをしていないかチェックする、なんてことも行っております。

サーバサイドの開発をしていれば当然と思われることかもしれませんが、意外とうっかり忘れてしまっているアプリをみかけることがありますので(自戒)、忘れないようにメモしておきました。
私があげた解答例以外に「こんなチェックもしないとダメでしょ」などなどありましたらコメントでご指摘ください。