【Unity】サーバーなどを用意せずにオフライン報酬を実装する方法

#はじめに
はじめまして，Nわかです。
初めての投稿でもあるので、軽く自己紹介。
現在起業した友人のもとでUnityエンジニアとして，大学に通いつつ働かせてもらっています。
現在1つのスマホ向けカジュアルゲーム制作を任されています。リモートでOKとのことで，ほぼ家や授業の合間などに作業させてもらっています。ホンマありがたいです。

その担当したゲームの機能の一つに**「オフライン報酬」**というものがあります。これはゲームをプレイしていなくても，ゲーム内コインがゲームをやっていなかった時間分貰えるもらえるというものです。
ソシャゲの時間経過で回復する体力（ゲームプレイ時に消費）などが近しい機能でしょう。

この機能を実装するにあたって，結構ググりましたが，直接的な記事は見当たりませんでした。
自分用の備忘録として，そして似たような機能を実装する方の助けに少しでもなれればと思います。
#仕様
今回実装するにあたり，決められていた仕様をざっくりと。

1. サーバーとのやり取りなどはしない
1. 1分当たりに獲得できる報酬の量は決まっており，その量は成長させられる
1. アプリをキルして再開もしくはキルせず再開（いったん別アプリをアクティブにしていた）のどちらの場合においても，報酬が発生していればポップアップ表示を行い報酬が獲得できる

本当にざっくりと、ですが。
#実装
それでは実際に実装していくわけですが，やることとして大まかに2つ項目があります。

###報酬の計算
サーバーとのやり取りをしないとのことなので，ローカルで時間を取得して前回の時間との差分を出して報酬を計算してあげる必要があります。

```cs
    private void SetOfflineEarning()
    {
        double minutes = CalcSecond(DateTime.UtcNow, _endTime) / 60;
        _endTime = DateTime.UtcNow;
        _earnOffline = (int) minutes * _offlineEarning;
        Save();
    }
```
`DateTime.UtcNow`で現在のUTC時刻を取得可能。`Save();`では，今回の機能に関係あるものでは`_endTime`のみ保存しています。また，記載していませんが，ゲーム開始時に`_endTime`を読み込んでいます。
1分あたりの報酬なので，今回は60で割っています。

ちなみに`CalcSecond()`は以下の通り。`DateTime`でのUTC時刻の差分を出し，トータル秒数を返却するメソッドです。

```cs
    private double CalcSecond(DateTime now, DateTime prevTime)
    {
        return (now - prevTime).TotalSeconds;
    }
```
これで`_earnOffline`にオフライン報酬が代入されている状態になったので，これを全体の稼ぎを示すメンバにでも加算してあげればよいでしょう。

###アプリから離れたことの検知
アプリ起動時だけでなく，他のアプリから戻ってきた際にもポップアップ表示をしてあげないといけないので，アプリから離れたことを検知してあげる必要があります。

UnityのMonoBehaviourには以下のようなメソッドが用意されています。
`OnApplicationPause(bool pauseStatus)`
これはアプリが他のアプリに移ったりホーム画面に戻ったりすることによって一時中断されたときや，逆にアプリケーションに戻ってきた際に呼ばれるものです。
つまり，`pauseStatus`が`false`の際は戻ってきたということになるので，ここにオフライン報酬の処理を記述してあげれば良さそうです。
ここで1つ注意すべき点としては，GoogleのAdMobを導入している際です。リワード広告を表示するとこれもアプリケーションを離れた判定を受けます。つまりリワード広告を閉じて戻ってきた際にも上記メソッドが呼ばれてしまうのです。
よって，もしリワード広告を導入しているのであれば，リワード広告を表示していた場合は無視するようにするべきでしょう。

```cs
    private void OnApplicationPause(bool pauseStatus)
    {
        //ゲーム中は無視
        if (GameSceneManager.IsGameStart) return;
        //リワード広告後は無視
        if (_isRewarding) return;
        if (!pauseStatus) _isOpenOfflineEarning = true;
        OpenOfflineEarning();
    }
```

`OpenOfflineEarning()`は以下のようになっています。

```cs
    public void OpenOfflineEarning()
    {
        //起動時にはオフライン報酬を表示
        if (_isOpenOfflineEarning)
        {
            _isOpenOfflineEarning = false;
            //もし稼ぎが0ならなし
            if (CalcOfflineEarning() <= 0) return;
            SceneManager.LoadSceneAsync(SceneName.OfflineEarningScene, LoadSceneMode.Additive);
        }
    }
```

わざわざメソッドにする意味あったか？とか今見返すと色々思うところがありますが，置いておきます笑
先ほどの`SetOfflineEarning`は読み込んだ先のシーン内にあるシーン管理スクリプトの`Start()`内で実行しています。
`CalcOfflineEarning()`は`SetOfflineEarning`で行っている計算の部分のみを行い値だけ返却するメソッドです。報酬が0の場合はポップアップ表示をする必要がなく計算結果を代入されては困るので，if文で判定を行っています。
一応コードも載せておきます。（同じようなことを記述していてばかばかしいですね，もっと簡潔に書けそうではあります。）

```cs
    public int CalcOfflineEarning()
    {
        double minutes = _timer.CalcSecond(DateTime.UtcNow, _endTime) / 60;
        return (int) minutes * _offlineEarning;
    }
```
追加ロードするシーンは以下のようにポップアップ表示用UIを用意しておき，稼げる金額等をスクリプト側で書き換えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271700/32a255f4-7b2e-8a3c-3fe9-926b5b411331.png)
#まとめ
初投稿で文章が読みづらいところもあるかとは思いますが，ご容赦ください。
今回基本的には時間差分で報酬を計算，アプリケーションに戻ってきたことを判定し，その際にポップアップ表示用のシーンを追加ロードという形で実装しました。
もっと良い方法もあるかとは思いますが，少しでも参考になれば幸いです。

#追記(2019-05-13)
今回の実装では特段チート対策（端末の時刻をいじるなどに対する対策）はしていません。
企画の方に確認したところ，対策しなくてもいいとのことだったので，とりあえず現状は放置です。そのうち対策したものも載せるかもしれません。
#参考
- [UnityでのOnApplicationFocus, OnApplicationPause, OnApplicationQuit関数における各端末(iOS/Android)動作まとめ](https://qiita.com/wapa5pow/items/2cbad14267d2ad3433f5)