Understanding Time Based Actions in Unity: Coroutines, Invoke and Timers

Tom McDonald
Published on:
Time Based Actions

One of the most common needs when developing games in Unity is the ability to schedule actions over time. Whether triggering events after a delay, repeating behaviors at regular intervals, or executing logic across multiple frames, Unity provides tools such as Coroutines, Invoke, and Timers to handle time based Actions.

Coroutines allow you to spread tasks over multiple frames without blocking the main thread. The Invoke method lets you easily call a method after a specified delay. Async/await enables asynchronous logic so you can perform long operations without freezing your game. And of course, timers are useful for repeating actions at set intervals.

Mastering these time-related functions will allow you to create smoother, more optimized game logic. Things like damage over time effects, delayed power-up spawns, AI behavior routines, and more can be implemented efficiently.

In this post, we’ll explore time based actions in Unity, including code examples and performance considerations. You’ll learn when and how to use coroutines, Invoke, async/await, and timers to craft solid game logic incorporating delayed or recurring actions.

What Are CoRoutines?

Coroutines allow you to pause and resume a method’s execution across multiple frames by yielding control back to Unity. This lets you spread tasks out to prevent expensive operations from blocking the main thread. This is especially handy for operations that need to happen over time, like animations or gradual changes.

Here is a coroutine that implements a simple damage over time effect on a player character:

// Attached to player character

IEnumerator DamageOverTime (int damage, int interval) {

  while(true) {

    TakeDamage(damage);
    
    yield return new WaitForSeconds(interval);
  }
}

void OnTriggerEnter(Collider other) {

  if(other.tag == "Hazard") {

    StartCoroutine(DamageOverTime(10, 1)); 
  }
}

In this snippet, we implement a damage-over-time effect. The player takes 10 points of damage every second. Notice how WaitForSeconds is used to pause the coroutine, preventing it from turning into an endless loop.

A tip for performance: Declare WaitForSeconds outside your loop to avoid unnecessary allocations and reduce garbage collection.

Stopping a coroutine is easy. Just call StopCoroutine with the coroutine’s name:

StopCoroutine("DamageOverTime");

A coroutine stops automatically if the GameObject it’s attached to is destroyed. This is a key difference from async/await, which continues executing until completion.

Here is an example of how I use a Coroutine to implement Damage Over Time in my game Diminishing Light

Coroutines are great for scenarios like:

  • Implementing damage over time effects.
  • Gradually fading UI elements.
  • Moving a platform or character along a set path
  • Handling complex AI routines.
  • Playing animation or audio clips sequentially

Managing Exceptions in Coroutines

When working with Coroutines, it’s important to handle exceptions carefully. Unlike regular methods, exceptions thrown inside a Coroutine are not propagated to the calling method. This means if something goes wrong inside a Coroutine, it could fail silently without you even knowing. To tackle this, wrap your Coroutine logic in try-catch blocks. Here’s an example:

IEnumerator ExampleCoroutine() {
    try {
        // Coroutine logic
    } catch (Exception e) {
        Debug.LogError("Coroutine failed: " + e.Message);
        // Handle the exception
    }
}

This lets you catch and handle exceptions effectively, preventing silent failures and making debugging easier.

Behavior During Scene Changes and Game Pauses

Coroutines are tied to the GameObject they are attached to. This means the Coroutine is stopped if the GameObject is destroyed, such as during a scene change. However, be aware that Coroutines do not automatically pause when the game is paused using Time.timeScale = 0. If you need your Coroutine to pause and resume the game, you’ll need to check Time.timeScale within the Coroutine or use a global flag to control its execution.

Using Invoke In Unity

After exploring coroutines, let’s look at another handy Unity feature: the Invoke method. This method is for scheduling method calls either after a delay (Invoke) or at regular intervals (InvokeRepeating). It’s handy for actions that don’t need the complexity and flexibility of coroutines but still require timing control.

How Does Invoke Work?

Invoke allows you to call a method after a specified time delay. The method takes two parameters: a string representing the method name, and a float indicating the number of seconds before the method is called. This is ideal for triggering an event or action after a pause. For example, you might want to delay an explosion effect in your game for dramatic effect:

void TriggerExplosion() {
    Invoke("Explode", 3f); // Calls Explode method after 3 seconds
}

void Explode() {
    // Explosion logic here
}

Where Invoke is used for single delayed actions, InvokeRepeating is used for repetitive tasks. It calls a method at regular intervals, making it perfect for spawning enemies, updating game states, or any other periodic activity. Here’s how you might use it to spawn enemies:

void Start() {
    InvokeRepeating("SpawnEnemy", 2f, 5f); // Spawns an enemy every 5 seconds, starting after a 2-second delay
}

void SpawnEnemy() {
    // Enemy spawning logic here
}

Key Considerations with Invoke

  • Method Signature: The methods you call with Invoke and InvokeRepeating must have no parameters and return void. This is a crucial point to remember.
  • Performance: For simple tasks, Invoke and InvokeRepeating are efficient and have low overhead. However, coroutines might offer better performance and flexibility when you need to frequently start, stop, or modify these invocations.
  • Cancelling Invokes: You can cancel an invoke using CancelInvoke. This is useful if the action conditions change or the GameObject is destroyed.

Use Cases in Game Development

While Invoke and InvokeRepeating don’t offer the same level of control as coroutines, their simplicity and efficiency make them an excellent choice for many time-based tasks in Unity. Invoke and InvokeRepeating are particularly useful in game development for:

  1. Spawning Objects: Regularly create new instances of objects at set intervals.
  2. Delayed Actions: Execute a function after a certain delay, like a delayed sound effect.
  3. Repeating Mechanics: Implement game mechanics that occur regularly, such as a health regeneration over time.
  4. Timed Events: Trigger events after specific intervals, like changing the game environment.
  5. Animation Triggers: Start animations or visual effects at certain times.

Using Timers In Unity

Timers are one of the most used mechanics in games. They are used for implementing time-dependent actions in your games, such as spawning projectiles, managing cooldowns, or triggering events. In this last section I will I will show you how to create a simple timer and cover common scenarios on how they are used.

What Are Timers and How Do They Work?

Timers in Unity are mechanisms to track and manage the passage of time. They allow you to execute certain actions after a set duration or at regular intervals. Unlike Coroutines and Invoke, which are built-in Unity features, timers are typically custom implementations using Unity’s Time class, especially Time.deltaTime.

Implementing a Simple Timer

A basic timer in Unity can be implemented using Time.deltaTime, representing the time in seconds it took to complete the last frame. By accumulating Time.deltaTime, you can create a countdown or count-up timer. Here’s a simple example:

private float timer;

void Update() {
    timer += Time.deltaTime;

    if (timer >= threshold) {
        // Execute timed action
        timer = 0;
    }
}

This incredibly flexible pattern forms the basis of many time-based mechanics in game development.

Common Uses of Timers in Games

  1. Projectile Spawning: Control the rate at which projectiles are spawned, like in a shooter game, to balance the gameplay.
  2. Cooldown Management: Implement cooldowns for abilities, attacks, or usage of items. For instance, allowing a player to attack every second.
  3. Resource Generation: In strategy games, timers can manage the periodic generation of resources.
  4. Building and Unit Production: Timers are used to track the construction time of buildings or the training time of units in RTS games.
  5. Event Scheduling: Trigger in-game events at specific times or after certain intervals, enhancing the dynamic nature of the game environment.

Tips for Optimizing Timer Usage

  • Avoid Excessive Checks: If you have multiple timers, consider consolidating them to avoid numerous Update checks.
  • Use Fixed Update for Physics-Based Timers: For timers related to physics, use FixedUpdate instead of Update for more consistent results.
  • Consider Unity’s Coroutine-Based Timers: For more complex timing needs, consider using Coroutines with WaitForSeconds, which can offer more control and readability.
  • Global vs Local Timers: Decide whether a timer should be global (affecting the entire game) or local (affecting only a specific object or scene).

Example: Implementing an Attack Cooldown

Here’s an example of how you might implement a timer for an attack cooldown in an action game:

private float attackCooldown = 1f;
private float attackTimer = 0;

void Update() {
    if (attackTimer > 0) {
        attackTimer -= Time.deltaTime;
    }

    if (Input.GetKeyDown(KeyCode.Space) && attackTimer <= 0) {
        Attack();
        attackTimer = attackCooldown;
    }
}

void Attack() {
    // Attack logic
}

In this example, the player can only attack once the timer resets, adding a strategic layer to the combat mechanics.

Timers are an important part of game development, providing the backbone for many gameplay elements that rely on timing and delays. Whether you’re developing an action-packed shooter, a strategy game, or anything in between, using timers will significantly enhance the player’s experience and the overall polish of your game.

Final Thoughts

Photo of author

Author

With over two decades of experience in technical and developer support, Tom has expertise in troubleshooting APIs. Over the years, he has built a many websites and tools with a primary focus on web development technologies including C#, ASP.NET, Blazor, HTML, and CSS. Last year, Tom starting to learn game development and is currently working on his first game "Last Stand," a zombie base defense game designed in Unity 3D.