日付と時刻を扱おう:DateTimeとTimeSpan【Unityスタートガイド12-7】

ログインボーナス、スタミナの回復、放っておくと育つ畑・・・。スマホのゲームには「現実の時間」で進む仕組みがたくさんあります。でも、UnityのTime.timeやTime.deltaTimeは、ゲームを閉じると止まってしまいます。今回は、現実の日付と時刻を扱うDateTimeと、時間の長さを扱うTimeSpanをご紹介します!

目次

この記事で学べること

  • Time.timeとDateTimeの違い(ゲームの中の時間と、現実の時刻)
  • 今の日付と時刻を取る方法(DateTime.NowとDateTime.UtcNow)
  • 日付や時刻を文字にして表示する方法
  • 2つの時刻の差(TimeSpan)を計算する方法
  • 時刻を保存して、次に開いたときに「何時間たったか」を計算する方法
  • 時刻を保存するタイミング(OnApplicationPauseとOnApplicationQuit)
  • 日付が変わったかどうかの判定(ログインボーナス)

前提知識

2つの「時間」

Unityで「時間」を扱うものは、大きく分けて2つあります。

Time.time / Time.deltaTimeDateTime
何の時間?ゲームが始まってからの時間現実の日付と時刻(パソコンやスマホの時計)
ゲームを閉じると?止まる(次に開くと0から)閉じている間も進む
一時停止(timeScale:0)すると?止まる止まらない
使いどころ移動、アニメーション、タイマーログインボーナス、スタミナの回復、放置ゲーム

「ゲームを閉じている間に何時間たったか」は、Time.timeでは分かりません。そこでDateTimeを使います。

DateTime:今の日付と時刻

今の時刻を取る

まずは、今の時刻をConsoleに出してみましょう。

using System;
using UnityEngine;

// 今の時刻を Console に出す
public class NowSample : MonoBehaviour
{
    private void Start()
    {
        DateTime now = DateTime.Now;
        Debug.Log($"今の時刻:{now}");
        Debug.Log($"年:{now.Year} 月:{now.Month} 日:{now.Day} 時:{now.Hour} 分:{now.Minute}");
        Debug.Log($"世界標準時(UTC):{DateTime.UtcNow}");
    }
}
  • using System;
    • DateTimeはUnityではなくC#の機能なので、using System;が必要
  • DateTime.Now
    • パソコンやスマホに設定されている地域の、今の時刻(日本なら日本時間)
  • DateTime.UtcNow
    • 世界標準時(UTC)の、今の時刻。日本時間より9時間遅れている
  • Year・Month・Day・Hour・Minute・Second
    • 年・月・日・時・分・秒を、それぞれ数字で取り出せる

NowとUtcNow、どっちを使う?

  • 保存や計算にはUtcNow
    • Nowは地域の設定で変わる。スマホを持って海外に行ったり、地域の設定を変えたりすると、時刻がずれてしまう
    • 夏時間がある国では、1年に1回、時計が1時間戻る日もある
    • UTCは世界中で同じなので、「何時間たったか」の計算がずれない
  • プレイヤーに見せるときはNow
    • 表示は、その人の地域の時刻の方が分かりやすい
    • UTCで保存した時刻は、ToLocalTime()で地域の時刻に直せる
スライム

とりあえず「保存と計算はUtcNow、表示はNow(またはToLocalTime)」と覚えておけばOKです

文字にして表示する

ToStringに書式を渡すと、好きな形の文字に出来ます。

using System;
using TMPro;
using UnityEngine;

// 今の日付と時刻を、テキストに表示する時計
public class ClockText : MonoBehaviour
{
    [SerializeField] private TMP_Text clockText;

    private void Update()
    {
        clockText.text = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
    }
}
書式意味例
yyyy年(4けた)2026
MM月(2けた)09
dd日(2けた)22
HH時(24時間、2けた)18
mm分(2けた)30
ss秒(2けた)15
  • MM(月)とmm(分)は、大文字と小文字で意味が違うので注意
  • 「9月22日」のように日本語を混ぜたいときは、"M月d日"のように書ける(1けたなら1文字で書く)

TimeSpan:時間の長さ

2つの時刻の差を計算する

DateTimeどうしを引き算すると、TimeSpan(時間の長さ)になります。

DateTime start = DateTime.UtcNow;
// ……何かをする……
TimeSpan passed = DateTime.UtcNow - start;
Debug.Log($"{passed.TotalSeconds}秒たった");

TimeSpanからは、次のように長さを取り出せます。

書き方意味3時間12分30秒のとき
TotalHours全部を「時間」にした値(小数あり)3.208…
TotalMinutes全部を「分」にした値192.5
TotalSeconds全部を「秒」にした値11550
Hours「時間」の部分だけ3
Minutes「分」の部分だけ12
Seconds「秒」の部分だけ30
  • 計算にはTotal〜を使う
    • Hoursは「時間の部分だけ」なので、2日と3時間なら3になる(日の部分は入らない)
    • 「何時間たったか」の計算には、TotalHoursを使う
  • 表示にはHours・Minutesが便利
    • 「3時間12分」と表示したいときは、(int)passed.TotalHoursとpassed.Minutesを組み合わせる

TimeSpanを作る

「8時間」「30分」のような長さは、TimeSpanを作って表せます。

TimeSpan eightHours = TimeSpan.FromHours(8);
TimeSpan thirtyMinutes = TimeSpan.FromMinutes(30);
DateTime nextBonus = DateTime.UtcNow + eightHours; // 8時間後の時刻

時刻を保存して、たった時間を計算する

前回遊んだ時刻を覚えておく

ゲームを閉じたときに時刻を保存して、次に開いたときに「何時間ぶりか」を計算してみましょう。

using System;
using TMPro;
using UnityEngine;

// 前回遊んだ時刻を保存して、次に開いたときに「何時間ぶりか」を表示する
public class LastPlayed : MonoBehaviour
{
    private const string LastPlayedKey = "LastPlayed";

    [SerializeField] private TMP_Text messageText;

    private void Start()
    {
        if (!PlayerPrefs.HasKey(LastPlayedKey))
        {
            messageText.text = "はじめまして!";
            return;
        }

        // 保存しておいた文字を数字(long)に戻し、それを DateTime に戻す
        long savedTimeBinary = long.Parse(PlayerPrefs.GetString(LastPlayedKey));
        DateTime lastPlayed = DateTime.FromBinary(savedTimeBinary);
        TimeSpan away = DateTime.UtcNow - lastPlayed;

        string lastText = lastPlayed.ToLocalTime().ToString("yyyy/MM/dd HH:mm");
        messageText.text = $"前回:{lastText}\n{(int)away.TotalHours}時間{away.Minutes}分ぶりです";
    }

    // スマホでホームに戻ったとき(アプリがバックグラウンドに入ったとき)
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            SaveNow();
        }
    }

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

    private void SaveNow()
    {
        PlayerPrefs.SetString(LastPlayedKey, DateTime.UtcNow.ToBinary().ToString());
        PlayerPrefs.Save();
    }
}
  • ToBinary と FromBinary
    • DateTimeを、1つの大きな整数(long)にしたり、元に戻したりする
    • PlayerPrefsにはlongを保存するメソッドがないので、文字(string)にして保存している
  • PlayerPrefs.HasKey
    • 初めて遊ぶときは、まだ保存されていない。先に確認してから読み込む
  • UTCで保存して、ToLocalTimeで表示
    • 計算はUTCどうしで行い、プレイヤーに見せるときだけ地域の時刻に直す
スライム

floatで保存したくなりますが、floatは細かい数字を覚えきれず、時刻がずれてしまいます・・・時刻は文字かlongで保存しましょう

いつ保存する?

保存するタイミングは、PCとスマホで違います。

メソッド呼ばれるとき注意
OnApplicationQuitゲームを終了したとき。エディタでは再生を止めたときスマホでは、ホームに戻ったあとにアプリを消されると呼ばれないことがある
OnApplicationPause(true)スマホでホームに戻ったとき、電話がかかってきたときPCではほとんど呼ばれない
OnApplicationFocus(false)ほかのウインドウやアプリに切り替えたとき頻繁に呼ばれる
  • スマホで遊ぶゲームは、OnApplicationPauseとOnApplicationQuitの両方で保存しておくと安心
  • スマホでは、ホームに戻ったアプリをOSが止めることがある。そのときOnApplicationQuitは呼ばれないので、OnApplicationPauseで保存しておくのが大事
フェアリー

「閉じるときに保存」だけだと、スマホでは保存されないことがあるんだね。PauseとQuitの両方で保存、覚えておいてね!

日付が変わったかを調べる(ログインボーナス)

「1日1回もらえるボーナス」は、前回もらった日と今日の日付を比べます。時刻まで比べると「24時間たったか」になってしまうので、Dateで日付だけを取り出します。

using System;
using UnityEngine;

// 日付が変わっていたら、1日1回のボーナスをあげる
public class DailyBonus : MonoBehaviour
{
    private const string LastBonusKey = "LastBonus";

    private void Start()
    {
        DateTime today = DateTime.Now.Date; // 今日の0時0分(地域の時刻)

        if (PlayerPrefs.HasKey(LastBonusKey))
        {
            DateTime lastDay = DateTime.FromBinary(long.Parse(PlayerPrefs.GetString(LastBonusKey)));
            if (lastDay >= today)
            {
                Debug.Log("今日のボーナスは、もうもらいました");
                return;
            }
        }

        Debug.Log("ログインボーナス!");
        PlayerPrefs.SetString(LastBonusKey, today.ToBinary().ToString());
        PlayerPrefs.Save();
    }
}
  • DateTime.Now.Date
    • 時刻を0時0分にした、日付だけのDateTime
    • 「日付が変わる」はプレイヤーの地域の0時なので、ここではNowを使う
  • lastDay >= today
    • DateTimeどうしは、>や<で比べられる(あとの方が大きい)

端末の時計を変えられたら?

DateTimeはパソコンやスマホの時計なので、プレイヤーが時計を進めたり戻したりすると、その分だけ時間がたったことになってしまいます。

  • マイナスになったら0にする
    • 時計を戻されると、たった時間がマイナスになる。そのまま使うと、ゲージが増えたりしておかしくなる
  • 上限を決める
    • 「何時間放っておいても、進むのは最大24時間まで」のようにしておくと、時計を大きく進められても被害が小さい
TimeSpan away = DateTime.UtcNow - lastTime; // lastTime:保存しておいた前回の時刻(DateTime)
if (away < TimeSpan.Zero)
{
    away = TimeSpan.Zero; // 時計を戻されたら、時間はたっていないことにする
}
if (away > TimeSpan.FromHours(24))
{
    away = TimeSpan.FromHours(24); // 最大24時間まで
}

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

よくあるエラーと対処法

エラー:The name ‘DateTime’ does not exist in the current context

  • 原因:using System;を書いていない
  • 対処法:スクリプトの一番上にusing System;を足す

エラー:FormatException: Input string was not in a correct format.

  • 原因:long.Parseに、数字ではない文字(空の文字など)を渡している。保存する前に読み込んだ、キーの名前が違う、など
  • 対処法:PlayerPrefs.HasKeyで確認してから読み込む。保存と読み込みで、キーの名前をそろえる(定数にしておくと安心)

エラー:たった時間がおかしい(9時間ずれる)

  • 原因:保存はNow、計算はUtcNow(またはその逆)のように、NowとUtcNowが混ざっている
  • 対処法:保存も計算もUtcNowにそろえる

エラー:2日たったのに「3時間」と表示される

  • 原因:TotalHoursではなくHoursを使っている(Hoursは「時間」の部分だけ)
  • 対処法:たった時間の計算にはTotalHours・TotalMinutes・TotalSecondsを使う

まとめ

  • Time.timeはゲームの中の時間(閉じると止まる)、DateTimeは現実の時刻(閉じている間も進む)
  • 保存と計算はDateTime.UtcNow、表示はDateTime.Now(またはToLocalTime)
  • DateTimeどうしの引き算でTimeSpan。計算にはTotalHoursなどのTotal〜を使う
  • 保存はToBinary()を文字にして、読み込みはFromBinary()
  • スマホではOnApplicationPauseとOnApplicationQuitの両方で保存する
  • 時計を戻された(マイナス)ときと、進められたときの上限を決めておく

次のステップ

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

コメント

コメントする

目次