sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Unity 放置ゲームにおける「日付と時刻変更」基本的な対策

f:id:shikaku_sh:20210205155735p:plain

スマートフォンの放置ゲームを作ろうと思ったら、開発者が必ず注意することになる問題のひとつに、端末の時刻設定を故意に変更するチート行為が挙がると思います。

対応策として、NTP サーバーのようなサーバー時間を取得してローカル時間を比較する方法が最も安全ですが、サーバーを用意するものコストが掛かりますし、大学などの公開 NTP サーバーを(ゲームのような)私的に利用し続ける設計も、第一からしてモラルに欠ける感じです。

そんなわけで、スマートフォンの環境設定「日付と時刻」が自動的に設定されているかどうかを確認する、というのがインディーズのゲームだとよくある対策かと思います。Android 端末においてのやり方をメモします。

f:id:shikaku_sh:20210205154244p:plain:w600
これがゴール(左:ゲーム画面、右:Android 設定画面)

対策不足の一例を挙げると、放置系ハクスラモンスターズもリリース直後は、このあたりの対策が不十分だったため、端末内の時間を進ませるだけで、大きな放置時間を作りだすチート行為が横行し、Twitter の雰囲気は淀みました。また、対策を加えていく中で、チーターも周囲のチート行為に便乗する形でチートに手を染めだしたプレイヤーもまとめてアカウント BAN(プレイ制限)になり、スタート時点の混乱は見苦しいものだった記憶があります。
ゲームの本質的な部分でないといえば、そのとおりなのですが、ゲームを楽しめる環境を維持するために必要な開発コストなのかな、とそのとき思いました。

日付と時刻の設定値を取得するネイティブプラグイン

そんなわけなので、Unity というよりも Android ネイティブプラグインを作って、Android 本体の設定値を取得します。

Android ネイティブプラグインの作り方は過去の記事「Unity アプリケーションのメモリー使用量を可視化する」などを参照してください。

今回は Android の設定「日付と時刻」から次の2つの設定値を取得するようにします。

  • 日付と時刻の自動設定
  • タイムゾーンの自動設定
  • (ここでは省略しますが、ネットワーク接続の有無も必要かと)

とりあえず、これが自動設定になっていれば、端末の放置時間を直接編集するようなチートを防ぐことができそうです。

package com.sh1.androidnativeutil;

import com.unity3d.player.UnityPlayer;
import android.os.Build;
import android.os.Debug;
import android.os.Process;
import android.app.ActivityManager;
import android.content.Context;
import android.provider.Settings;

public class SystemSettings
{
    /**
     * ユーザーが日付、時刻、タイムゾーンの自動取得設定をしているかどうかを示す値を取得します。
     * @return true = ON, false = OFF
     */
    public static boolean IsAutoTime()
    {
        final Context context = UnityPlayer.currentActivity.getApplication().getApplicationContext();
        boolean isResult = Settings.Global.getInt(context.getContentResolver(), Settings.Global.AUTO_TIME, 0) == 1;
        /*
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
        {
            isResult = Settings.Global.getInt(context.getContentResolver(), Settings.Global.AUTO_TIME, 0) == 1;
        }
        else
        {
            // API level 17
            isResult = Settings.System.getInt(context.getContentResolver(), Settings.System.AUTO_TIME, 0) == 1;
        }
         */
        return isResult;
    }

    /**
     * ユーザーがタイムゾーンを自動取得設定をしているかどうかを示す値を取得します。
     * @return true = ON, false = OFF
     */
    public static boolean IsAutoTimeZone()
    {
        final Context context = UnityPlayer.currentActivity.getApplication().getApplicationContext();
        boolean isResult = Settings.Global.getInt(context.getContentResolver(), Settings.Global.AUTO_TIME_ZONE, 0) == 1;

        return isResult;
    }
}

用意したプラグインを Unity に設定します。「Android」専用のプラグインを使うときは、Android というフォルダー名にします。詳細は「AAR plug-ins and Android Libraries」を確認します。

Assets\Plugins\Android\プラグインファイル

f:id:shikaku_sh:20210205153738p:plain
直下に置いても動きます

ネイティブプラグインを利用する Unity スプリプト

Unity の UI Text コンポーネントに結果を単純に表示するだけのサンプルです。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;

public class TimeNetworkChecker : MonoBehaviour
{
    private AndroidJavaClass _NativeClass = null;

    private const string PACKAGE_NAME = "com.sh1.androidnativeutil";
    private const string CLASS_NAME = "SystemSettings";
    private const string METHOD_AUTO_TIME = "IsAutoTime";
    private const string METHOD_AUTO_TIME_ZONE = "IsAutoTimeZone";

    [SerializeField]
    private Text _Text = null;

    private void Start()
    {
        _NativeClass = new AndroidJavaClass($"{PACKAGE_NAME}.{CLASS_NAME}");
    }

    private void OnDestroy()
    {
        _NativeClass?.Dispose();
        _NativeClass = null;
    }

    private void Update()
    {
        var text = "";

        try
        {
            var isAutoTime = false; 
            var isAutoTimeZone = false;
#if UNITY_ANDROID
            isAutoTime = _NativeClass.CallStatic<bool>(METHOD_AUTO_TIME);
            isAutoTimeZone = _NativeClass.CallStatic<bool>(METHOD_AUTO_TIME_ZONE);
#endif
            text = $"Time:{isAutoTime}, TimeZone:{isAutoTimeZone}";
        }
        catch(Exception ex)
        {
            text = ex.Message;
        }

        if (_Text != null)
        {
            _Text.text = text;
        }
    }
}

Android の APK をうまく出力できないとき

f:id:shikaku_sh:20210205153427p:plain:w600
エラーウィンドウ

Native プラグインは、指定する APK のバージョンでないと出力の際にエラーになります。Unity かプラグインのバージョン指定を変更します。

f:id:shikaku_sh:20210205153642p:plain:w600
プラグインの最小バージョンを変更する例

動作のテスト

こんな感じです。各画像の左がアプリの動作画面で右が設定。

f:id:shikaku_sh:20210205154234p:plain:w600f:id:shikaku_sh:20210205154239p:plain:w600f:id:shikaku_sh:20210205154244p:plain:w600
動作テスト

環境設定に応じて、表示の true, false が切り替わっています。わりとよさそうですね。

対策の発展形

日付と時刻の設定が自動であること+ネットワーク接続が出来ているなら時刻はおよそ正常です。しかし、ゲームを起動するときに必ずネットワーク接続が必要になるのも困りものです。地下鉄に乗る合間にプレイしたいといったニーズに不都合です。

なので、オフライン状態でも特定条件なら可とするアイデアを考えてみます:

  • オンライン状態の確認を取得している時刻がある
  • その時刻から 24 時間以内であること(またはオフライン状態になって 24 時間以内)

フローチャートにするとこんな感じでどうでしょうか。仮のログイン期間を 24 時間以内としたのは、日本を基準にした世界のタイムゾーン)の差ぐらいまで、というアイデアから。

f:id:shikaku_sh:20210205155515p:plain

タイムゾーンの変化で時間が進んだときは、それだけ放置したと認めて、また日本に戻ってきたときに戻った時間だけ停止時間が発生する。 時間が戻ったときは、その時間だけ停止時間が発生するけど、また日本に戻ってきたときに停止時間分は放置した時間として戻ってくるはずです。およそ問題なさそう。

国外に退出して戻ってこない、またはその逆みたいなケースを例外としています。

サンプル

GitHub に「unity-network-time-check」というサンプルを公開しています。

参考