Delay, Invoke, Repeat!

This asset provides you with a versatile API to invoke callbacks in various ways. It is meant as an alternative to simple coroutines.

Features

Quickstart

Configuration is optional and should usually not be required.

If you want to use DIR in one of your scripts you should add this using declaration so you don’t have to access all methods via Rarebyte.DelayInvokeRepeat.[...]:

using Rarebyte.DelayInvokeRepeat;

Afterwards you can directly start using the Invoker, which is the main access point to the asset’s functionality.

Invoker.New(DoStuff)
  .DelayByGameTime(10)
  .Run();

For more elaborate examples see Examples.

Jobs

Jobs are the central building blocks of the asset. They are wrappers around Actions that are used to delay their execution or to repeatedly execute them.

Creating Jobs

You can create a new Job by calling the New() method.

var job = Invoker.New();

After you’ve created the Job you can set the action that shall be executed with the SetAction(Action action) method.

job.SetAction(() => Debug.Log("Hello"));

All of the methods can also be chained together which makes the code less verbose.

var job = Invoker.New()
  .SetAction(() => Debug.Log("Hello"));

Because a job without an Action doesn’t make sense, we provide a short form alternative to the code above.

var job = Invoker.New(() => Debug.Log("Hello"));

Delays

The execution of the Job’s Action can be delayed by…

Delays can be combined but the combination cannot include both a time and a frame delay.

Delay By Time

Action execution can be delayed by time in either scaled or unscaled mode. Unscaled mode means that the time delay will not be affected by changes to the global time scale (Time.timeScale).

job.DelayByGameTime(5); // Delay by 5s (scaled)
job.DelayByUnscaledGameTime(5); // Delay by 5s (unscaled)

Delay By Frames

Action execution can also be delayed by a certain number of frames.

job.DelayByFrames(30); // Delay by 30 frames
job.DelayByOneFrame(); // Delay by 1 frame

Delay By Condition

Delay action execution until a certain condition evaluates to true.

job.DelayUntilTrue(() => IsConditionTrue());

Controls

A Job’s handling can be controlled with the following controls:

Run

This method adds the Job to the Invoker and it will start ticking. If the Job has no delay it will execute immediately.

Triggers the Listen On Run callback.

job.Run();

Pause

Pauses ticking of the Job. This means that all delays will be paused and will only resume when Resume will be called.

Triggers the Listen On Pause callback.

job.Pause();

Resume

Resumes ticking of the Job. Will only have an effect if the job has been paused before.

Triggers the Listen On Resume callback.

job.Resume();

Cancel

Cancels the Job. This means that all delays and repetitions will be cancelled and the action won’t be executed.

Triggers the Listen On Cancel callback (doesn’t trigger Listen On Done).

job.Cancel();

Reset

Resets all delays of the Job. If the Job is repeating it only resets the delay of the current repetition but not the number of repetitions.

If the job has an automatic cancel then the automatic cancel will be reset too.

job.Reset();

Fast-Forward

Fast-Forwards all delays of the Job and executes the Action immediately. If the Job is repeating only the the delays of the current repetition will be fast-forwarded.

job.FastForward();

Repeats

A repeating Job will reset it’s delays and start again after it’s Action has been executed.

job.Repeat(3); // Repeat 3 times 
job.RepeatForever(); // Repeat until cancelled

Automatic Cancels

In some cases you might want to automatically cancel a job after a delay. One prominent example would be a job that repeats forever and that shall be cancelled after some time or when a condition is met.

Automatic cancels are not affected by resets and fast-forwards but they do get affected by pauses and resumes.

job.CancelAfterGameTime(1);
job.CancelAfterUnscaledGameTime(1);
job.CancelAfterFrames(5);
job.CancelWhenTrue(() => ShouldCancel());

Game Object Bindings

There are many cases where a Job’s controls should be bound to game object events.

These two pieces of code are equivalent:

void OnDestroy() { job.Cancel(); }
void OnDisable() { job.Pause(); }
void OnEnable() { job.Resume(); }
job.CancelOnDestroy(gameObject);
job.PauseOnDisable(gameObject);
job.ResumeOnEnable(gameObject);

Callbacks

There are various callbacks to which you can add listeners to get notified when something of interest happens to the Job.

The existing callbacks are:

Listen On Run

This callback gets triggered when the Job is being scheduled for execution (when you run it).

job.ListenOnRun(() => DoStuff());

Listen On Done

This callback gets triggered when the Job is done executing successfully. It will not get called when the Job was cancelled.

job.ListenOnDone(() => DoStuff());

You might want to subscribe to this whenever you want to queue Jobs together. For example:

jobA.ListenOnDone(() => jobB.Run());

Listen On Cancel

This callback gets triggered when the Job is cancelled.

job.ListenOnCancel(() => DoStuff());

Listen On Pause

This callback gets triggered when the Job is paused.

job.ListenOnPause(() => DoStuff());

Listen On Resume

This callback gets triggered when the Job is resumed/unpaused.

job.ListenOnResume(() => DoStuff());

Listen On Tick

This callback gets triggered in every frame that the Job is running (until it’s done or cancelled).

job.ListenOnTick(() => DoStuff());

Tags

Tags can be assigned to Jobs to perform group controls on all Jobs with specific tags.

To assign a tag to a job:

job.SetTag("SomeTag");

// Alternative during job creation
Invoker.New(() => DoStuff(), "SomeTag");

There are a couple of methods in the Invoker to control all Jobs with certain tags.

Invoker.CancelAllWithTag("SomeTag");
Invoker.PauseAllWithTag("SomeTag");
Invoker.ResumeAllWithTag("SomeTag")

Configuration

This asset usually doesn’t need a whole lot of configuration but those values that can be tweaked can be modified on the Invoker component. If you don’t already have an Invoker in any of your scenes (it will be created with default values if not) you will have to add one. There can only be one Invoker in the loaded scenes so make sure that you don’t add them to multiple scenes that can be loaded during one run.

Inspector window of the Invoker
Inspector window of the Invoker

DIR uses an object pool in the background to reuse previously used Jobs to minimize memory allocations. The way this object pool grows and the maximum size of it can be configured with the following variables:

  • Job Pool Capacity: Defines the maximum size of the pool. An error will be thrown if the number of simultaneous Jobs exceeds this capacity.
  • Job Pool Grow Mode: Defines how the pool should grow. The different grow modes are OneByOne which always allocates 1 new job whenever the current size of the pool is exceeded, and Double which always doubles the current size of the pool.

Memory Optimizations

The system is implemented with special care taken to achieve zero per-frame allocations but there are some properties about C# Actions that you need to be aware of when writing your code to avoid unnecessary garbage collected allocations.

For example, the code below produces about 104 bytes of memory garbage every frame. It is not immediately visible because the nasty new keyword is inserted by the compiler.

public class MemoryDemo: MonoBehaviour {

  public void Update() {
    Invoker.New()
      .SetAction(DoStuff)  // -> SetAction(new Action(DoStuff)) 
      .DelayByGameTime(5)
      .Run();
  }

  private void DoStuff() {
    // ...
  }
}

To avoid these allocations you need to cache the action and pass the cached action into the Job:

public class MemoryDemo: MonoBehaviour {

  private Action cachedAction;

  public void Awake() {
    cachedAction = DoStuff;
  }

  public void Update() { 
    Invoker.New()
      .SetAction(cachedAction)
      .DelayByGameTime(5)
      .Run(); 
  } 

  private void DoStuff() {
    // ... 
  }
}

For a more detailed explanation on this topic have a look here.

Examples

    • Call a method after 5 seconds
      Invoker.New(DoStuff)
        .DelayByGameTime(5)
        .Run();
    • Call a method after 1 frame
      Invoker.New(DoStuff)
        .DelayByFrames(1)
        .Run();
    • Call a method when a condition evaluates to true
      Invoker.New(DoStuff)
        .DelayUntilTrue(IsConditionTrue)
        .Run();
    • Call a method repeatedly every frame for 5 seconds
      Invoker.New(DoStuff)
        .RepeatForever()
        .CancelAfterGameTime(5)
        .Run();
    • Call a method repeatedly every 1 second for 60 seconds
      Invoker.New(DoStuff)
        .RepeatForever()
        .DelayByGameTime(1)
        .CancelAfterGameTime(60)
        .Run();
    • Call a method repeatedly every 1 second until a condition evaluates to true
      Invoker.New(DoStuff)
        .RepeatForever()
        .DelayByGameTime(1)
        .CancelWhenTrue(IsConditionTrue)
        .Run();
    • Call a method when a condition evaluates to true after a minimum delay of 10 seconds
      Invoker.New(DoStuff)
        .DelayByGameTime(10)
        .DelayUntilTrue(IsConditionTrue)
        .Run();