0

I have a List of Timings (in seconds), at which a function needs to be executed. I tried a very naive way but it doesn't seem to work, most likely because Time.fixedTime isn't always exact. Any better ideas on how to execute something on 1000 determined (but not equally spread out) times?

if (Timings.Contains(Time.fixedTime)){
    //do something
}
dorien
  • 5,265
  • 10
  • 57
  • 116

5 Answers5

2

If you want to continuously do something for a specific amount of time,you have to use Time.deltaTime in a coroutine. Increment a float value from 0 with Time.deltaTime until it reaches the time you want to do that thing.

IEnumerator executeInWithFixedTiming(float time)
{
    float counter = 0;

    while (counter <= time)
    {
        counter += Time.deltaTime;

        //DO YOUR STUFF HERE
        transform.Rotate(Vector3.right * Time.deltaTime);

        //Wait for a frame so that we don't freeze Unity
        yield return null;
    }
}

You can start as many tasks as possible like below. The example run the code for 5 seconds:

StartCoroutine(executeInWithFixedTiming(5));

You can also extend this function and make it take a parameter of what to do in that coroutine as Action. You can then pass in the code to run inside that function too. Not tested but should also work.

IEnumerator executeInWithFixedTiming(Action whatToDo, float time)
{
    float counter = 0;

    while (counter <= time)
    {
        counter += Time.deltaTime;
        whatToDo();
        //Wait for a frame so that we don't freeze Unity
        yield return null;
    }
}

then use it like this:

StartCoroutine(executeInWithFixedTiming(
delegate
{
    //DO YOUR STUFF HERE
    transform.Rotate(Vector3.right * Time.deltaTime);
}, 5));

EDIT:

The thing is, I don't want to continuously do it for X seconds, but only once at each point of Timings

You mentioned that the timer is sorted so a for loop to loop through it and while loop to wait for the timer to finish should do it.

List<float> timer = new List<float>();

IEnumerator executeInWithFixedTiming()
{
    float counter = 0;

    //Loop through the timers
    for (int i = 0; i < timer.Count; i++)
    {
        //Wait until each timer passes
        while (counter <= timer[i])
        {
            counter += Time.deltaTime;
            //Wait for a frame so that we don't freeze Unity
            yield return null;
        }

        //TIMER has matched the current timer loop.
        //Do something below
        Debug.Log("TIMER REACHED! The current timer is " + timer[i] + " in index: " + i);
    }

    //You can now clear timer if you want
    timer.Clear();
}

Just start the coroutine once in the Start function and it should handle the timer. StartCoroutine(executeInWithFixedTiming());. You also also modify it with the second example in this answer to make it take a parameter of each code to execute.

Note:

In Unity, it's better to time something with Time.deltaTime. It's the most accurate way of timing that I know about. Other Unity variables tend to loose their accuracy over time.

Programmer
  • 121,791
  • 22
  • 236
  • 328
  • Thanks for this. The thing is, I don't want to continuously do it for X seconds, but only once at each point of Timings. – dorien Aug 07 '17 at 14:54
  • Oops. I may have gotten this wrong. So you have list of timers(floats) and you want to execute code when time in the game matches the timer code? If so, can you provide me the example of the timers values in the last. I can update my answer to reflect that. Also, are the timers sorted? – Programmer Aug 07 '17 at 14:58
  • The Timings is a List() that contains values such as: 0.98 1.88 2.705 3.213 etc (about every second, but slighty varies. Basically, something happens to the beat of music. – dorien Aug 07 '17 at 15:01
  • Ok. That makes sense. Just to be sure, it looks like it is sorted. Is it in order? – Programmer Aug 07 '17 at 15:01
  • Indeed, it is sorted. – dorien Aug 07 '17 at 15:02
  • 1
    Ok. Check the edit. Hopefully, that's what you are shooting for. – Programmer Aug 07 '17 at 15:13
  • do u calculate this list beforehand? if so this is a terrible way of going through the list i think. – Jeroen De Clercq Aug 07 '17 at 15:17
  • @JeroenDeClercq OP mentioned he already have the timers 0.98, 1.88 2.705, 3.213 in the List. Now, he has to wait for amount of each one to reach then execute something. What's terrible in this answer? – Programmer Aug 07 '17 at 15:19
  • why not make an enumerator that waitforsecondsrealtime. add an integer that u increase by one each enumeration and use that to access the list? If i am wrong tell me i am just observing and learning. – Jeroen De Clercq Aug 07 '17 at 15:22
  • 1.For the sake of accuracy since this is a timer. The values are too small that using `Time.deltaTime` is more appropriate. waitforsecondsrealtime or waitforseconds should be used for bigger numbers from `0.5` and up but I assume OP may have value very tiny in the list. 2.Also, by not using waitforsecondsrealtime or waitforseconds, OP can see the timing. For example, see the remaining of the timing if that is required to do something in the game like playing warning sound. You can't do that with waitforsecondsrealtime or waitforseconds. – Programmer Aug 07 '17 at 15:30
  • Thank you for explaining. i Wasn't aware that the timing were of that much. – Jeroen De Clercq Aug 07 '17 at 15:32
  • @Programmer if accuracy is that important why not run it in a different thread? – Jeroen De Clercq Aug 07 '17 at 15:47
  • Thread has nothing to do this. You only use `Thread` for networking, file stuff, path finding etc.. This can be done without a Thread so there is no need to complicate it. Not to mention that Unity is not Threadsafe so you will need to [implement](https://stackoverflow.com/questions/41330771/use-unity-api-from-another-thread-or-call-a-function-in-the-main-thread/41333540#41333540) a way to call Unity functions when the timer is over. It's not worth it and not needed. – Programmer Aug 07 '17 at 15:53
  • I will give this solution a try. Interesting what you say about Time.deltaTime. I have something else going on simulateously, (a ball bouncing with this changing beat). And can currently only do this with relative times so it seems to be shifting. Any suggestions are welcome at this question https://stackoverflow.com/questions/45318281/dynamically-changing-speed-of-object-in-unity – dorien Aug 07 '17 at 15:59
  • @Programmer I tried this and it almost works. I have another CoRoutine that is started right before which contains Music.GetComponent().Play (); Unfortunately, the actions done at the timings with the coroutine you suggest (a ball flashing) are not in sync with the music as they should be. Could there be something causing an asynchronicity. I tried FixedTime too as I thought maybe the music is captured that way. Basically the timings should relate to the start-time of the music and be executed immediately. – dorien Aug 09 '17 at 06:41
  • Ah, found that it works when, instead of using counter, I check with AudioSource.time. Thanks so much for the help! – dorien Aug 09 '17 at 06:55
0

You can use Time.realtimeSinceStartup to get the absolute time since the game started and use that with a reference point to get the elapsed time since X.

If you need to compare a lot of timings against that I suggest sorting them into order so the earliest is first. That way you only ever need to check if the earliest timing has happened, if it hasn't you can skip any more checks.

float refTime;
Queue<float> timings;
void Update() {
    var elapsed = Time.realtimeSinceStartup - refTime;
    //test against the earliest timing
    if (timings.Count > 0 && refTime > timings.Peek()) {
        //do stuff
        //remove earliest timing from queue
        timings.Dequeue();
    }
}

If your timings happen less often than once a frame you can get away without checking the next item in the queue until next frame.

Weyland Yutani
  • 4,682
  • 1
  • 22
  • 28
0

Instead of checking against the exact time, check whether that time has passed. This will account for the inaccuracy of fixedTime and your code will fire on the correct frame or the one after it. If that's not timely enough for what you're doing, try some of the more accurate timing methods outlined in the other answers.

Keep your collection of timings sorted and remove them as they fire. Then you can keep checking the first (earliest) instead of iterating through them all.

if (Time.fixedTime <= Timings[0])
brehonia
  • 66
  • 2
  • That's an interesting idea. And I suppose it's easy to just remove the first element of a list with pop() or something? – dorien Aug 07 '17 at 14:51
  • Yes it depends what you're using to store them but you can `Dequeue()` from a Queue, `Pop()` from a Stack, `RemoveAt(0)` from a Collection, etc. – brehonia Aug 07 '17 at 15:54
0

At first you need sort your list, then do the rest:

float time = 0;
Int count = 0;
Lisr<float> timeList;
void Update()
{
  time += Time.deltaTime;
  If(timeList.count > count)
{
  If(time >= timeList[count])
  {
    //do something
    ++count;
  }
}
}
BlackMB
  • 230
  • 6
  • 20
0

You may be able to use Invoke in a recursive manner. Like this:

void Start() {
    Timings.sort();
    Timings.reverse(); // use like a stack, removing last elements in order
    InvokeRecursive();
}

void InvokeRecursive() {
    if (Timings.Count <= 0) return;
    float time = Timings[Timings.Count - 1];
    Timings.remove(Timings.Count - 1);
    // do stuff
    Invoke("InvokeRecursive", time - Time.realtimeSinceStartup);
}
joshwilsonvu
  • 2,569
  • 9
  • 20