ペット育成ゲームの作り方#5|閉じている間も時間を進めよう(DateTime)

#5の完成画面。開いた直後に「おかえり! 5時間ぶりだね」のパネルが出て、後ろでおなかを空かせたペットがしょんぼりしている

育成ゲームといえば、久しぶりに開いたらペットのおなかがペコペコになっている、あの感じ!でも今のままだと、ゲームを閉じると時間が止まってしまい、開くたびにおなかも元どおり・・・。今回はペット育成ゲームのシリーズの5回目として、閉じている間も時間が進んで、開いたときに「おかえり!〇時間ぶりだね」と迎えてくれる仕組みを作ってみたいと思います!

目次

今回作るもの

  • ゲームを閉じたときに、ペットの状態と時刻を保存する
  • 次に開いたとき、閉じていた時間の分だけ、おなかときげんを減らす
  • 「おかえり!〇時間ぶりだね」のパネルを出す
  • 端末の時計をいじられたときの対策(マイナスと上限)
スクリプト今回やること
PetController時間を進める処理を、外から呼べるメソッド(PassTime)に分ける
OfflineProgress保存と読み込み、閉じていた時間の計算、おかえりパネル(新しく作る)

前回の続きから

  • Unityのバージョンは6.3 LTS
    • 記事作成時点では6000.3.22f1で確認しています
  • テンプレートはUniversal 2D
  • #4の状態:ボタンでお世話が出来て、ゲージが回復し、コインが増える

DateTime(現実の日付と時刻)とTimeSpan(時間の長さ)の基本は、12-7 日付と時刻を扱おう:DateTimeとTimeSpanで紹介しています。PlayerPrefsでの保存は12-6 データを保存しようを見てください。

閉じている間の時間はどうやって進める?

ゲームを閉じている間は、スクリプトは1行も動いていません。ずっと動かしているわけではなく、次のように「あとからまとめて」計算します。

  • 閉じるときに「今の時刻」を保存しておく
  • 次に開いたとき、「今の時刻 − 保存した時刻」で、閉じていた時間を出す
  • その時間の分だけ、まとめておなかときげんを減らす

#3で「1秒あたりに減る量」を計算したので、あとは「閉じていた秒数」を掛けるだけです。

スライム

だから、ずっと閉じっぱなしでもスマホの電池は減りません。開いた瞬間に、一気に計算しているだけなんです

フェアリー

「最後に閉じた時刻」と「今の時刻」の差を計算するんだね。目次のページで言ってたのはこれだったんだ!

時間を進める処理を分けよう(PetController)

今は、Updateの中で直接おなかときげんを減らしています。これを「〇秒ぶん時間を進める」メソッド(PassTime)に分けて、閉じていた時間の計算からも呼べるようにします。

using UnityEngine;

// ペットの状態を時間で減らし、少なくなったらしょんぼりさせる。お世話を受け取る
public class PetController : MonoBehaviour
{
    [SerializeField] private PetStatus status = new PetStatus();

    [Header("バランス")]
    [SerializeField] private float hoursToEmptyHunger = 6f; // おなかが100から0になるまでの時間(時間)
    [SerializeField] private float hoursToEmptyMood = 8f;   // きげんが100から0になるまでの時間(時間)
    [SerializeField] private float sadLine = 30f;           // これより少ないとしょんぼりする

    [Header("お世話")]
    [SerializeField] private float feedAmount = 30f;        // ごはんで回復するおなか
    [SerializeField] private float playAmount = 25f;        // あそぶで回復するきげん
    [SerializeField] private int coinsPerCare = 5;          // お世話1回でもらえるコイン

    [Header("確認用")]
    [SerializeField] private float timeScale = 1f;          // 時間の進む速さ(確認するときだけ大きくする)

    [Header("参照")]
    [SerializeField] private Animator animator;
    [SerializeField] private StatusView statusView;

    private static readonly int SadHash = Animator.StringToHash("Sad");
    private static readonly int HappyHash = Animator.StringToHash("Happy");

    public PetStatus Status => status;

    // ---- ここから変更 ----
    private void Update()
    {
        PassTime(Time.deltaTime * timeScale);
    }

    // 〇秒ぶん時間を進める(閉じていた時間の計算からも呼ぶ)
    public void PassTime(float seconds)
    {
        status.hunger = Mathf.Max(0f, status.hunger - PerSecond(hoursToEmptyHunger) * seconds);
        status.mood = Mathf.Max(0f, status.mood - PerSecond(hoursToEmptyMood) * seconds);

        bool isSad = status.hunger < sadLine || status.mood < sadLine;
        animator.SetBool(SadHash, isSad);

        statusView.Show(status);
    }
    // ---- ここまで変更 ----

    // ごはんをあげる:おなかが回復する
    public void Feed()
    {
        status.hunger = Mathf.Min(PetStatus.Max, status.hunger + feedAmount);
        Care();
    }

    // あそぶ:きげんが回復する
    public void Play()
    {
        status.mood = Mathf.Min(PetStatus.Max, status.mood + playAmount);
        Care();
    }

    // お世話したときに共通でやること
    private void Care()
    {
        status.coins += coinsPerCare;
        animator.SetTrigger(HappyHash);
        statusView.Show(status);
    }

    // 「何時間で空になるか」から、1秒あたりに減る量を計算する
    private static float PerSecond(float hoursToEmpty)
    {
        return PetStatus.Max / (hoursToEmpty * 60f * 60f);
    }
}
  • Updateの中身をPassTimeに移した
    • やっていることは#3と同じ。「何秒進めるか」を引数でもらうようにしただけ
    • Updateでは「前のフレームからの秒数」、閉じていた時間の計算では「閉じていた秒数」を渡す
  • PassTimeはpublic
    • OfflineProgressから呼ぶため

閉じていた時間を計算しよう(OfflineProgress)

保存と読み込み、閉じていた時間の計算、おかえりパネルの表示をまとめたスクリプトです。

  • PetRaising/Scriptsに「OfflineProgress」を作る
using System;
using TMPro;
using UnityEngine;

// 閉じるときにペットの状態と時刻を保存し、次に開いたときに閉じていた時間の分だけ進める
public class OfflineProgress : MonoBehaviour
{
    // PlayerPrefs のキー(#6 で JSON にまとめるまでの仮の保存)
    private const string LastTimeKey = "LastTime";
    private const string HungerKey = "Hunger";
    private const string MoodKey = "Mood";
    private const string CoinsKey = "Coins";

    [SerializeField] private PetController pet;
    [SerializeField] private float maxOfflineHours = 24f; // 閉じていても、進めるのはこの時間まで

    [Header("おかえりパネル")]
    [SerializeField] private GameObject welcomePanel;
    [SerializeField] private TMP_Text welcomeText;
    [SerializeField] private float minMinutesToWelcome = 1f; // これより短いときはパネルを出さない

    private void Start()
    {
        welcomePanel.SetActive(false);

        // 初めて遊ぶときは、まだ何も保存されていない
        if (!PlayerPrefs.HasKey(LastTimeKey))
        {
            return;
        }

        // 閉じたときの状態に戻す
        PetStatus status = pet.Status;
        status.hunger = PlayerPrefs.GetFloat(HungerKey, status.hunger);
        status.mood = PlayerPrefs.GetFloat(MoodKey, status.mood);
        status.coins = PlayerPrefs.GetInt(CoinsKey, status.coins);

        // 閉じていた時間を計算する
        DateTime lastTime = DateTime.FromBinary(long.Parse(PlayerPrefs.GetString(LastTimeKey)));
        TimeSpan away = DateTime.UtcNow - lastTime;

        if (away < TimeSpan.Zero)
        {
            away = TimeSpan.Zero; // 端末の時計が戻されていたら、時間はたっていないことにする
        }
        if (away > TimeSpan.FromHours(maxOfflineHours))
        {
            away = TimeSpan.FromHours(maxOfflineHours); // 進めるのは上限まで
        }

        // 閉じていた時間の分だけ、まとめて進める
        pet.PassTime((float)away.TotalSeconds);

        if (away.TotalMinutes >= minMinutesToWelcome)
        {
            ShowWelcome(away);
        }
    }

    // スマホでホームに戻ったとき
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            Save();
        }
    }

    // ゲームを終了したとき(エディタでは再生を止めたとき)
    private void OnApplicationQuit()
    {
        Save();
    }

    private void Save()
    {
        PetStatus status = pet.Status;
        PlayerPrefs.SetFloat(HungerKey, status.hunger);
        PlayerPrefs.SetFloat(MoodKey, status.mood);
        PlayerPrefs.SetInt(CoinsKey, status.coins);
        PlayerPrefs.SetString(LastTimeKey, DateTime.UtcNow.ToBinary().ToString());
        PlayerPrefs.Save();
    }

    private void ShowWelcome(TimeSpan away)
    {
        // 閉じていた時間を「5時間」「30分」のような文字にする
        string awayText = away.TotalHours >= 1
            ? $"{(int)away.TotalHours}時間"
            : $"{(int)away.TotalMinutes}分";
        welcomeText.text = $"おかえり!\n{awayText}ぶりだね";
        welcomePanel.SetActive(true);
    }

    // おかえりパネルの OK ボタンの On Click に登録する
    public void CloseWelcome()
    {
        welcomePanel.SetActive(false);
    }

    // 確認用:コンポーネントの右クリックメニューから「5時間前に閉じたことにする」
    // 再生していないときに使う。保存した時刻だけを書き換える(ペットの状態は、前回閉じたときのまま)
    [ContextMenu("5時間前に閉じたことにする")]
    private void PretendClosedFiveHoursAgo()
    {
        PlayerPrefs.SetString(LastTimeKey, DateTime.UtcNow.AddHours(-5).ToBinary().ToString());
        PlayerPrefs.Save();
        Debug.Log("5時間前に閉じたことにしました。再生するとおかえりパネルが出ます");
    }
}
  • 保存するもの
    • 時刻だけでなく、おなか・きげん・コインも保存する。時刻だけ覚えていても、元の数字が分からないと減らせないため
    • 今回はPlayerPrefsで1つずつ保存しています。#6でJSONにまとめて、1つのファイルにします
  • DateTime.UtcNow と ToBinary
    • 保存も計算もUTC(世界標準時)で行う。地域の時刻(Now)だと、設定を変えたときにずれてしまう
    • DateTimeはToBinaryで数字(long)にして、文字として保存している
  • away(閉じていた時間)の対策
    • 時計を戻されるとマイナスになるので、0にする
    • 時計を大きく進められても、進めるのは最大24時間まで(maxOfflineHours)
  • pet.PassTime((float)away.TotalSeconds)
    • 閉じていた秒数を、そのままPassTimeに渡す。#3の「1秒あたりに減る量」が、そのまま使える
    • 時間の長さはTotalSeconds(全部を秒にした値)を使う。Secondsだと「秒の部分だけ」になってしまう
  • OnApplicationPauseとOnApplicationQuitの両方で保存
    • スマホでは、ホームに戻ったあとにアプリを消されるとQuitが呼ばれないことがある。Pauseでも保存しておく
  • [ContextMenu]
    • Inspectorでコンポーネントを右クリックしたメニューに、このメソッドが出てくる。5時間待たなくても確認出来るようにするためのもの
    • ここではSave()を呼ばず、時刻だけを書き換えている。再生していないときのペットの状態はInspectorの初期値(おなか80・コイン0)なので、Save()すると前回の状態が消えてしまうため

おかえりパネルを作ろう

閉じていた時間を知らせるパネルを作ります。

  • HierarchyでCanvasを右クリック>UI (Canvas)>Panel で作り、名前を「WelcomePanel」にする
    • 画面全体を覆う、半透明のパネルが出来る
    • Canvasの一番下(SafeAreaより下)に置く。UIは下にあるものほど手前に描かれるので、ボタンやペットの上に出る
    • ImageのColorを黒、A(透明度)を150ぐらいにする
  • WelcomePanelの子に Image を作り、名前を「Window」にする
    • Pos Y:520、Width:800、Height:440
    • 真ん中に置くとペットが隠れてしまうので、ペットの上に出す(パネルの後ろで、ペットが待っているのが見える)
    • Source Image:UISprite、Image Type:Sliced(角が丸いまま大きくなる)
  • Windowの子に Text – TextMeshPro を作り、名前を「WelcomeText」にする
    • アンカー:stretch-stretch、Left・Right:40、Top:40、Bottom:180
    • Font Size:64、Alignment:真ん中、Color:濃いグレー
    • テキストは「おかえり!」(スクリプトが書き換えるので仮でOK)
  • Windowの子に Button – TextMeshPro を作り、名前を「OKButton」にする
    • アンカー:bottom-center、Pos Y:100、Width:360、Height:120
    • 子のテキスト:「OK」、Font Size:56
HierarchyにWelcomePanel>Window>WelcomeText・OKButton、Gameビューに半透明の背景とおかえりのウインドウ
  • WelcomePanelは、編集中は表示されたままでOK
    • 再生するとOfflineProgressのStartで非表示になり、必要なときだけ表示されます

スクリプトを付けよう

OfflineProgressを付ける

  • Hierarchyで右クリック>Create Empty で「GameManager」を作る
  • Add Component>OfflineProgress
  • 次のようにドラッグ&ドロップする
    • Pet:Pet
    • Welcome Panel:WelcomePanel
    • Welcome Text:WelcomeText
GameManagerのOffline Progress。Pet・Max Offline Hours:24・Welcome Panel・Welcome Text・Min Minutes To Welcome:1

OKボタンにつなぐ

  • HierarchyでOKButtonを選ぶ
  • ButtonのOn Click ()の「+」を押し、左下の欄にGameManagerをドラッグ&ドロップ
  • 「No Function」から、OfflineProgress>CloseWelcome ()を選ぶ
OKButtonのOn Click ()に、GameManagerのOfflineProgress.CloseWelcomeが登録されている

動かして確認しよう

保存されるか確認する

  • 再生(▶)して、ごはんを何回か押してコインを増やす
  • 再生を止める(OnApplicationQuitで保存される)
  • もう一度再生して、コインの枚数が止める前と同じになっていればOK

おかえりパネルを確認する

5時間待つわけにはいかないので、確認用のメニューを使います。

  • 一度再生して止めておく(保存されたデータがある状態にする)
  • 再生していない状態で、GameManagerのOffline Progressの右上の「︙」(またはコンポーネント名を右クリック)を押す
  • 「5時間前に閉じたことにする」を選ぶ
  • 再生(▶)する
  • 「おかえり! 5時間ぶりだね」のパネルが出る
  • 後ろのゲージが、5時間分減っている
    • おなか:6時間で空になる速さなので、約83減る(100なら残り17ぐらい)
    • きげん:8時間で空になる速さなので、約62減る(80なら残り17ぐらい)
  • コインの枚数は、前回止めたときのまま
  • OKを押すとパネルが消える
5時間前に閉じたことにして再生した画面。おかえりパネルが出て、ゲージが減り、ペットがしょんぼりしている
OKを押したあと。パネルが消えて、減ったゲージとしょんぼりしたペットが見えている

おなかが減ってしょんぼりしていたら、ごはんをあげて元気にしてあげてください!

スライム

保存したデータを消して最初からやり直したいときは、Edit>Clear All PlayerPrefs を選ぶとPlayerPrefsが全部消えます

端末の時計をいじられたら?

DateTimeは端末の時計なので、プレイヤーが時計を進めたり戻したりできてしまいます。

  • 時計を戻したとき
    • 閉じていた時間がマイナスになる。そのままだとゲージが増えてしまうので、0にしている
  • 時計を進めたとき
    • おなかが減るだけなので、プレイヤーは得をしません
    • ただ、#8で「育った時間」で進化するようにしたとき、時計を進めて一気に進化させられてしまう。今のうちに上限(24時間)を付けておきます

完全に防ぐにはインターネットのサーバーから時刻をもらう必要がありますが、一人で遊ぶゲームなら、ここまでの対策で十分だと思います。

あとがき

今回は閉じている間も時間が進むようにして、おかえりパネルまで作りました。これで「久しぶりに開いたら、おなかペコペコで待っていた」という、育成ゲームらしい体験が出来るようになりました!今は保存にPlayerPrefsを使っていますが、次回はペットの状態をJSONにまとめて、1つのファイルに保存するようにしたいと思います。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次