0
\$\begingroup\$

I have a problem. In my GetTime.cs script, I am getting local time from an Android/iOS device, and simply assigning it to a Text component.

private void Update()
{
    time = System.DateTime.Now.ToLocalTime().ToString("HH:mm:ss");
    timeText.text = time;      
}

I am making a travel system, that takes X amount of seconds to get from point A to point B. While in travel, you cannot do any actions.

How can I make this into a countdown timer based on local time? For example, I want to do travel from point A to point B like this:

  • I am in point A

  • I choose an option to travel to point B

  • I wait 30 seconds

  • I am at point B

But it has to be based on local time, and it has to work in the background, even when the app is not running.

\$\endgroup\$
  • 1
    \$\begingroup\$ If you want to measure the time elapsed between now and a saved DateTime, that's a general programming question you can find answered on StackOverflow. Can you show us how you've tried saving your start time, subtracting it from your current time, and displaying the time remaining based on your research so far? \$\endgroup\$ – DMGregory Jul 8 at 18:57
  • \$\begingroup\$ @DMGregory firstly, I am getting actuall time by System.DataTime.Now.ToLocalTime().ToString("HH:mm:ss"); and I am assigning it to a ActuallTime variable. Now, when I want to travel to point B (let's say that travel takes 1 hour), then I am getting actuall time +1 hour by System.DateTime.Now.Hour.ToString() + 1;. I simply substract that, the question is, how do I display, second after second how much time is left to travel to that point B? EDIT: And how do I make this run in background, when the app is OFF? \$\endgroup\$ – Seweryn Pokutycki Jul 8 at 21:23
1
\$\begingroup\$

It looks like you want to do something like this, using PlayerPrefs to store the state between runs of the game. (This is insecure, but so is using the local system time - if a player wants to cheat, they can just change their local clock anyway, so we might as well keep things simple)

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using DateTime = System.DateTime;
using TimeSpan = System.TimeSpan;


public class CountdownAction : MonoBehaviour
{
    // Assign a unique name here so two different countdowns
    // don't fight for the same save location.
    public string actionKey = "travelStart";
    public int durationSeconds = 60;
    public Text display;

    public UnityEvent OnComplete;

    DateTime? _startTime;    

    // On load, try to resume our countdown from where we left off.
    void Start() {
        // If we don't have a saved start time, then there's no action in progress.
        // So we'll abort, and wait patiently for the player to start the action.
        if (!PlayerPrefs.HasKey(actionKey))
            return;

        // Otherwise, load our saved start time.
        // PlayerPrefs doesn't support DateTime natively, so we'll decode it from a string.
        var savedString = PlayerPrefs.GetString(actionKey);        
        if (long.TryParse(savedString, out long ticks)) {
            _startTime = new DateTime(ticks);
        } else {
            Debug.LogWarningFormat("Indecipherable countdown start time for {0}, restarting countdown.", actionKey);
            _startTime = DateTime.Now;
        }

        // Wait for the action to complete, updating our countdown timer in the meantime.
        StartCoroutine(WaitForCompletion());
    }

    // Start a new countdown.
    public void StartCountdown() {
        if (_startTime.HasValue) {
            // Abort the countdown in progress and warn the developer - this might be a sign of a bug.
            Debug.LogWarningFormat("Restarted the countdown for {0} when one was already running!", actionKey);
            StopAllCoroutines();
        }

        // This moment is when we will measure the duration from.
        _startTime = DateTime.Now;

        // Save this in our player prefs, so we can recover it even if the app is closed & restarted.
        PlayerPrefs.SetString(actionKey, _startTime.Value.Ticks.ToString());
        // To be safe, let's save this data to disc right away, just in case our app crashes or we lose power.
        PlayerPrefs.Save();

        // Wait for the action to complete, updating our countdown timer in the meantime.
        StartCoroutine(WaitForCompletion());
    }

    // This can be a win compared to Update, since it runs only when we have a countdown in progress.
    IEnumerator WaitForCompletion() {
        int remainingSeconds;

        do {
            // Compute how much time has passed since the countdown started.
            var elapsed = DateTime.Now - _startTime.Value;

            // Subtract the time we've waited from the total duration of the action.
            remainingSeconds = Mathf.Max(durationSeconds - (int)elapsed.TotalSeconds, 0);

            // Display the difference in our text field.
            display.text = (new TimeSpan(0, 0, remainingSeconds)).ToString(@"hh\:mm\:ss");

            // Wait a frame.
            yield return null;
        // Loop until the countdown is over.
        } while (remainingSeconds > 0);

        // We're done!
        Debug.LogFormat("Action {0} has completed!", actionKey);
                
        // Clear the countdown, since there's no longer any action in progress.
        _startTime = null;
        PlayerPrefs.DeleteKey(actionKey);        

        // Fire off any actions that were wired-up to happen once this timer completes.
        // We'll assume if this countdown has any persistent effects, these handlers
        // will call PlayerPrefs.Save so we don't have to double-up on it here.
        if (OnComplete != null)
            OnComplete.Invoke();
    }
}
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ PlayerPrefs is designed to store player preferences, not important game data. IIRC on Android, the user can accidentally clear PlayerPrefs by clearing the cache for the app. Better to use a save file. \$\endgroup\$ – Kevin Jul 9 at 17:21
  • 1
    \$\begingroup\$ You're quite right, but here I wanted to focus on the time computations which were the focus of the question, without going down a rabbit hole about saving to a custom file. ;) \$\endgroup\$ – DMGregory Jul 9 at 17:24
  • 1
    \$\begingroup\$ I have been a freelance developer for years and worked on numerous projects with different teams. I have found that most bad habits that most developers have are things they picked up tangentially from tutorials where the tutorial author used a shortcut because that wasn't the point of the tutorial. The three most common mistakes are using public fields instead of serialized private fields, using GameObject.Find() for everything every frame, and saving everything in PlayerPrefs. \$\endgroup\$ – Kevin Jul 9 at 17:33
  • 1
    \$\begingroup\$ You'll hear no objections from me, and you can easily find other answers where I've warned against the use of PlayerPrefs exactly as you say. You're welcome to post another answer showing how to save the time in a more robust fashion. \$\endgroup\$ – DMGregory Jul 9 at 17:37
  • \$\begingroup\$ I don't really understand this code. Isn't there a shorter answer and some simplier way to do this? \$\endgroup\$ – Seweryn Pokutycki Jul 11 at 21:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.