
育成ゲームといえば、久しぶりに開いたらペットのおなかがペコペコになっている、あの感じ!でも今のままだと、ゲームを閉じると時間が止まってしまい、開くたびにおなかも元どおり・・・。今回はペット育成ゲームのシリーズの5回目として、閉じている間も時間が進んで、開いたときに「おかえり!〇時間ぶりだね」と迎えてくれる仕組みを作ってみたいと思います!
- シリーズの目次:ペット育成ゲームの作り方
- 前回:#4 ごはんとあそぶのボタンでお世話しよう
今回作るもの
- ゲームを閉じたときに、ペットの状態と時刻を保存する
- 次に開いたとき、閉じていた時間の分だけ、おなかときげんを減らす
- 「おかえり!〇時間ぶりだね」のパネルを出す
- 端末の時計をいじられたときの対策(マイナスと上限)
| スクリプト | 今回やること |
|---|---|
| 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


- WelcomePanelは、編集中は表示されたままでOK
- 再生するとOfflineProgressのStartで非表示になり、必要なときだけ表示されます
スクリプトを付けよう
OfflineProgressを付ける
- Hierarchyで右クリック>Create Empty で「GameManager」を作る
- Add Component>OfflineProgress
- 次のようにドラッグ&ドロップする
- Pet:Pet
- Welcome Panel:WelcomePanel
- Welcome Text:WelcomeText


OKボタンにつなぐ
- HierarchyでOKButtonを選ぶ
- ButtonのOn Click ()の「+」を押し、左下の欄にGameManagerをドラッグ&ドロップ
- 「No Function」から、OfflineProgress>CloseWelcome ()を選ぶ


動かして確認しよう
保存されるか確認する
- 再生(▶)して、ごはんを何回か押してコインを増やす
- 再生を止める(OnApplicationQuitで保存される)
- もう一度再生して、コインの枚数が止める前と同じになっていればOK
おかえりパネルを確認する
5時間待つわけにはいかないので、確認用のメニューを使います。
- 一度再生して止めておく(保存されたデータがある状態にする)
- 再生していない状態で、GameManagerのOffline Progressの右上の「︙」(またはコンポーネント名を右クリック)を押す
- 「5時間前に閉じたことにする」を選ぶ
- 再生(▶)する
- 「おかえり! 5時間ぶりだね」のパネルが出る
- 後ろのゲージが、5時間分減っている
- おなか:6時間で空になる速さなので、約83減る(100なら残り17ぐらい)
- きげん:8時間で空になる速さなので、約62減る(80なら残り17ぐらい)
- コインの枚数は、前回止めたときのまま
- OKを押すとパネルが消える




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



保存したデータを消して最初からやり直したいときは、Edit>Clear All PlayerPrefs を選ぶとPlayerPrefsが全部消えます
端末の時計をいじられたら?
DateTimeは端末の時計なので、プレイヤーが時計を進めたり戻したりできてしまいます。
- 時計を戻したとき
- 閉じていた時間がマイナスになる。そのままだとゲージが増えてしまうので、0にしている
- 時計を進めたとき
- おなかが減るだけなので、プレイヤーは得をしません
- ただ、#8で「育った時間」で進化するようにしたとき、時計を進めて一気に進化させられてしまう。今のうちに上限(24時間)を付けておきます
完全に防ぐにはインターネットのサーバーから時刻をもらう必要がありますが、一人で遊ぶゲームなら、ここまでの対策で十分だと思います。
あとがき
今回は閉じている間も時間が進むようにして、おかえりパネルまで作りました。これで「久しぶりに開いたら、おなかペコペコで待っていた」という、育成ゲームらしい体験が出来るようになりました!今は保存にPlayerPrefsを使っていますが、次回はペットの状態をJSONにまとめて、1つのファイルに保存するようにしたいと思います。
コメント
コメント一覧 (1件)
[…] 前回:#5 閉じている間も時間を進めよう […]