How I Built a Unity Task Scheduler That Finally Made My Combat System Stop Breaking
Learn how to build a Unity task scheduler with proper event queue system that prevents action chaos in turn-based combat and sequential game events.
Here's the thing—when I first started building turn-based combat systems in Unity, I had this nightmare where my character would start attacking, then immediately get interrupted by the enemy's attack, which would trigger some UI animation, which would somehow fire off three different sound effects at once. The whole thing was a chaotic mess. Turns out, I wasn't alone. This is one of those problems that trips up almost every developer the first time they try to build anything with sequential actions.
The solution? A Unity task scheduler with a proper event queue system. It sounds complicated, but actually, it's one of those elegant solutions that makes you wonder why you didn't think of it sooner. Let me show you exactly how I use this system now, and how it completely transformed the way I handle game events.
What's Actually Going On When Your Game Events Collide
Let me paint you a picture. You've got your Unity game running, and suddenly five different things want to happen at the exact same moment. Maybe the player clicks "Attack" while an enemy is already mid-animation, and there's a dialogue box trying to pop up, and some background music needs to fade in. Without a Unity task scheduler controlling the order, these events fire off instantly and simultaneously, creating what I call "action chaos."
The core issue is that by default, Unity doesn't care about the order of your game logic. If three scripts all trigger methods in the same frame, they'll all execute immediately. This creates race conditions—situations where the outcome depends on which script happened to run first that frame. Been there, spent way too many hours debugging that mess.
A Unity event queue system solves this by introducing a critical concept: sequential processing. Instead of letting every action fire the moment it's called, you add those actions to a queue (like a waiting line), and a scheduler processes them one at a time, in the exact order they arrived. This is the foundation of predictable, stable game logic.
The Grocery Store Analogy That Finally Made This Click
Actually, wait—before we dive into code, let me share the analogy that made this concept instantly clear for me.
Think of your game events like customers at a grocery store checkout. Without a queue system, it's like having no checkout line at all—every customer rushes the cashier at once, trying to pay simultaneously. Total chaos, right? Nobody gets served properly, transactions get mixed up, and the whole system breaks down.
Now add a queue. Customers line up. The cashier (your Unity task scheduler) processes one transaction completely before moving to the next person. First in, first out. Simple, organized, predictable. That's exactly what we're building for your game events.
This First-In, First-Out (FIFO) principle is what makes event queues so powerful. The first action you add to the queue is the first one that gets executed. No jumping the line, no interruptions—just clean, sequential processing.
The Core Building Blocks You Need to Understand
Alright, let's break down the technical pieces you need. I'm going to explain each one the way I wish someone had explained them to me at CMU.
Queue: This is a data structure that follows the FIFO principle I just mentioned. You add items to the back of the line and remove them from the front. Unity C# gives us System.Collections.Generic.Queue<T> for this exact purpose.
System.Collections.Generic.Queue<T>: This is the specific C# class you'll use. It provides two essential methods: Enqueue() adds an item to the back of the queue, and Dequeue() removes and returns the item from the front. Here's what that looks like in code:
using System.Collections.Generic;
// A queue that will hold tasks of type 'ICommand'.
private Queue<ICommand> taskQueue = new Queue<ICommand>();
Task (or Command): An object representing a single unit of work. This could be "move character to position," "play attack animation," "display dialogue box"—any discrete action your game needs to perform. The key is that every task knows how to execute itself.
Scheduler (or Processor): The manager component that runs your queue. Each frame, it checks: "Is a task currently running? No? Cool, let me grab the next one from the queue and execute it." This is the traffic cop of your Unity game event system.
Asynchronous Task: Tasks that take multiple frames to complete. Moving a character across the screen isn't instant—it takes time. Your scheduler needs to wait for these tasks to finish before starting the next one. This is where Unity coroutines become your best friend.
Callback: A function passed as an argument to another method. In Unity task scheduling, callbacks let asynchronous tasks signal "Hey, I'm done now!" back to the scheduler. This is typically done with C#'s Action delegate.
Command Pattern vs Simple Action Queue—Which One Should You Actually Use?
Here's a decision you'll face early: do you use a simple queue of Action delegates, or do you implement the full Command Pattern with an ICommand interface? I've used both, and here's my honest take on when each makes sense.
| Criteria | Approach A: Simple Queue of Actions | Approach B: Command Pattern (Queue of ICommand) |
|---|---|---|
| Best For | Simple, fire-and-forget event systems where the actions are instantaneous and don't require any state or parameters (e.g., a simple sound effect queue). | Complex, stateful operations, especially those that take time to complete or need to be configured with data (e.g., moving a unit, playing a cutscene, displaying dialogue). |
| Performance | Extremely high performance. A queue of Action delegates is very lightweight. |
Slightly more overhead due to the creation of new class instances for each command object. This is generally not a concern unless you are queuing thousands of tasks per frame. |
| Complexity | Very easy to implement. It's just a Queue<Action>. |
More complex to set up due to the need for an interface and separate classes for each command. However, it is far more powerful and scalable. |
| Code Example | Queue<Action> myQueue; myQueue.Enqueue(() => Debug.Log("Hi")); |
Queue<ICommand> myQueue; myQueue.Enqueue(new MoveCommand(targetPos)); |
My recommendation? If you're building anything beyond a simple prototype—especially a Unity turn based combat system or cutscene manager—go with the Command Pattern. The initial setup takes a bit more work, but you'll thank yourself later when you need to add state, undo functionality, or debugging capabilities.
Why This System is the Secret Behind Every Great Turn-Based Game
Let me tell you why this matters beyond just "cleaner code." This system solves real problems that will bite you if you don't handle them.
Creates Order from Chaos: It prevents race conditions and unpredictable behavior. When you know for certain that your "player attack" action will complete fully before the "enemy counterattack" action begins, your game logic becomes rock solid. No more weird edge cases where animations overlap or damage calculations happen in the wrong order.
Enables Turn-Based Logic: Every Unity turn based combat system I've analyzed uses some form of task queue. Whether it's Final Fantasy, XCOM, or Into the Breach, the pattern is the same: queue up the actions for this turn, process them sequentially, move to the next turn. The Unity command pattern tutorial approach makes this trivial to implement.
Improves Code Readability and Maintenance: By encapsulating each action into its own ICommand class, you create clean, self-contained logic. When you come back to your code six months later (or when another developer joins your project), they can instantly see what each command does without digging through a tangled mess of manager scripts.
Makes Complex Sequences Possible: Want to build a tutorial system where the player completes action A, then you show hint B, then wait for action C? Just enqueue those commands in order. The scheduler handles all the timing automatically. Same goes for cutscenes, quest chains, or any multi-step gameplay sequence.
Here's How I Set Up My Task Scheduler in Real Projects
Alright, let's build the actual system. I'm going to show you the exact setup I use, step by step.
First, define a common task interface. This is your ICommand interface—every task you create will implement this, guaranteeing it has an Execute method:
public interface ICommand
{
void Execute();
}
This simple interface is the contract: "Any class that implements ICommand promises to provide an Execute() method." That's how we can store different types of tasks (move, attack, dialogue) in the same queue—they all speak the same language.
Next, create the scheduler loop. This is the brain of your Unity task scheduler. Each frame, it checks if a task is running. If not, and if there are tasks waiting in the queue, it dequeues the next one and executes it:
private bool isTaskRunning = false;
void Update()
{
if (!isTaskRunning && taskQueue.Count > 0)
{
ICommand nextTask = taskQueue.Dequeue();
isTaskRunning = true;
// How you execute depends on if the task is instant or takes time.
nextTask.Execute();
}
}
This pattern ensures only one task runs at a time. The isTaskRunning flag is your safety lock—it prevents the next task from starting until the current one is done.
Now, handle Unity sequential commands that take time. For tasks that span multiple frames (like animations or movement), you need a way for the task to signal completion. The standard approach is to pass a callback Action that the task will invoke when it's done:
// The interface is updated to support a callback.
public interface ICommand
{
void Execute(Action onComplete);
}
// The scheduler passes a method to the task.
ICommand nextTask = taskQueue.Dequeue();
nextTask.Execute(() => { isTaskRunning = false; }); // The callback sets the flag to false.
When the task finishes (maybe the character reaches their destination), it calls onComplete(), which flips isTaskRunning back to false. This signals to the scheduler: "I'm done, you can start the next task now."
The Coroutine Trick That Makes Everything Elegant
Actually, wait—there's a cleaner way to handle the scheduler loop. Instead of managing boolean flags in Update, I've found that using a Unity coroutine queue approach is way more elegant.
Here's the pattern I actually use in production:
IEnumerator ProcessQueue()
{
while (true) // Loop forever
{
if (taskQueue.Count > 0)
{
ICommand task = taskQueue.Dequeue();
yield return StartCoroutine(task.Execute()); // Assumes Execute returns IEnumerator
}
else
{
yield return null; // Wait for the next frame if queue is empty
}
}
}
This coroutine runs continuously from the moment you start it (usually in your scheduler's Start method). The beauty is in the yield return StartCoroutine(task.Execute()) line—this automatically waits for the task's coroutine to complete before moving to the next iteration of the loop. No boolean flags needed. The coroutine system handles all the synchronization for you.
For this to work, your ICommand interface changes slightly:
public interface ICommand
{
IEnumerator Execute();
}
Now every command returns an IEnumerator, making it inherently compatible with Unity's coroutine system.
How I Connect This with an Event Bus for Maximum Power
You know what's funny? When I first built this system, I hardcoded everything. "Player presses button → create AttackCommand → add to queue." It worked, but it was rigid as hell.
Then I learned to combine the Unity task scheduler with an event bus, and it completely leveled up the architecture. Here's the pattern:
Events are broadcast loosely: When something happens in your game (player attacks, enemy dies, door opens), you publish an event to a central event bus. Any script can listen for these events.
// Player.cs
// EventBus.Publish(new PlayerAttackEvent(target));
// GameManager.cs
// OnPlayerAttackEvent(e) { taskScheduler.Enqueue(new AttackCommand(player, e.target)); }
Your GameManager (or combat manager, or whatever) listens for these events and converts them into concrete commands that get added to the scheduler queue. This decouples the "what happened" from the "how do we handle it."
The result? Your player script doesn't need to know about the task scheduler. Your task scheduler doesn't need to know about player input. Each system has a clear, single responsibility, and they communicate through clean event messages. From my time working on complex Unity systems, this separation is what makes the difference between a project that's a joy to maintain and one that becomes spaghetti code.
Real Games That Nail This System
Let me show you how this plays out in actual shipped games. I've seen these techniques used brilliantly, and they're perfect examples to learn from.
Final Fantasy X - Turn-Based Combat Perfection
One of my favorite implementations of this is in Final Fantasy X. The combat is classic turn-based—each character and enemy takes their action in an order determined by their speed stat.
Here's what I find fascinating about their approach: The game likely has a central CombatScheduler managing a queue of ICommand objects (or their equivalent). When it's a character's turn, the player's input gets converted into an AttackCommand, MagicCommand, or ItemCommand and added to the queue. The scheduler processes exactly one command at a time, waiting for its full animation and effects to complete before starting the next turn.
The player experience? Combat feels strategic and predictable. You can plan multiple moves ahead because you know actions will resolve in a specific, visible order. That reliability is what makes the depth possible.
Hearthstone - Event Chain Mastery
Hearthstone is where task scheduling gets really complex. When you play a card, it can trigger a massive chain reaction: a Battlecry effect, which triggers an opponent's Secret, which causes a minion to die, which triggers its Deathrattle. These events must happen in a strict, defined order, or the game breaks.
After analyzing dozens of games, this stands out because: Every single action—playing a card, a minion attacking, an effect triggering—gets added to a master event queue. The game loop processes one event at a time. If that event triggers another event, the new one gets added to the queue (often at the front for immediate resolution), ensuring the chain reaction feels correct and consistent.
The player experience? Even with hundreds of possible card combinations, interactions resolve consistently and fairly every single time. Players can learn and predict outcomes because the event resolution order is deterministic. This reliability is the foundation of competitive play.
Into the Breach - Preview System Magic
Let me tell you about how Into the Breach solved this exact problem in a brilliant way. It's a turn-based strategy game where you see what enemies are planning to do before they act, giving you a chance to counter their moves.
What makes this brilliant from a developer's perspective: At the start of the enemy turn, the AI generates a list of AttackCommands and adds them to a queue. But instead of executing them immediately, the scheduler first runs a Preview() method on each command to show the player where attacks will land. When the player ends their turn, the scheduler then calls Execute() on that same queue of commands.
Here's how you can adapt this for your own game: The task queue system cleanly separates the "planning" phase from the "execution" phase. Same commands, same queue, but processed in two different ways depending on game state. I always tell my students to look at how this game handles preview systems—it's a masterclass in flexible command design.
Your Complete Implementation Blueprint
Alright, let's actually build this thing from scratch. Let me show you how I approach this—the exact method I use when teaching this concept.
What We're Building
Goal: Create a basic Unity task scheduler that executes Unity sequential commands one after another, waiting for each to complete before starting the next. We'll make a cube move to two target positions in sequence.
Unity Editor Setup
Here's the exact setup I use:
- Create a new GameObject called "Scheduler" and attach a
TaskSchedulerscript to it (we'll write this script next). - Create a "Mover" GameObject—a simple Cube works perfectly.
- Create two empty GameObjects called "TargetA" and "TargetB". Position them at different locations in your scene so you can see the movement clearly.
Step-by-Step Code Implementation
Let me walk you through the process, starting with the command structure.
Step 1: Define the ICommand Interface and a MoveCommand
First, we need our Unity ICommand interface. Create a new script called ICommand.cs:
// In a new script, ICommand.cs
using System.Collections;
public interface ICommand { IEnumerator Execute(); }
This interface forces every command to provide an Execute() method that returns an IEnumerator, making it coroutine-compatible.
Now create the actual movement command in MoveCommand.cs:
// In a new script, MoveCommand.cs
using UnityEngine;
using System.Collections;
public class MoveCommand : ICommand
{
private Transform _transform;
private Vector3 _destination;
public MoveCommand(Transform transform, Vector3 destination)
{
_transform = transform;
_destination = destination;
}
public IEnumerator Execute()
{
while (Vector3.Distance(_transform.position, _destination) > 0.1f)
{
_transform.position = Vector3.MoveTowards(_transform.position, _destination, 5f * Time.deltaTime);
yield return null;
}
}
}
This command stores the transform to move and the destination. When Execute() is called, it runs a coroutine that smoothly moves the object over multiple frames. The yield return null pauses execution each frame, allowing the movement to happen gradually.
Step 2: Create the TaskScheduler
Now for the brain of the operation. Create TaskScheduler.cs:
// In TaskScheduler.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class TaskScheduler : MonoBehaviour
{
private Queue<ICommand> _taskQueue = new Queue<ICommand>();
void Start()
{
StartCoroutine(ProcessQueue());
}
public void AddTask(ICommand task)
{
_taskQueue.Enqueue(task);
}
IEnumerator ProcessQueue()
{
while (true)
{
if (_taskQueue.Count > 0)
{
yield return StartCoroutine(_taskQueue.Dequeue().Execute());
}
else
{
yield return null;
}
}
}
}
This is the pattern I've configured dozens of times. The ProcessQueue coroutine runs forever, checking each frame if there are tasks waiting. If there are, it dequeues one and waits for its Execute() coroutine to finish before looping back to check for the next task.
Step 3: Create a Demo Script to Queue Up Tasks
Finally, let's put it all together. Create a Demo.cs script and attach it to your Scheduler GameObject:
// In a new script, Demo.cs, attached to the Scheduler
using UnityEngine;
public class Demo : MonoBehaviour
{
[SerializeField] private Transform mover;
[SerializeField] private Transform targetA;
[SerializeField] private Transform targetB;
private TaskScheduler _scheduler;
void Start()
{
_scheduler = GetComponent<TaskScheduler>();
_scheduler.AddTask(new MoveCommand(mover, targetA.position));
_scheduler.AddTask(new MoveCommand(mover, targetB.position));
}
}
In the Inspector, drag your Mover cube to the mover field, and your target empties to targetA and targetB.
When you hit Play: The cube will move to TargetA first, wait until it arrives, then automatically move to TargetB. No overlapping movement, no chaos—just clean, sequential execution. That's your Unity task scheduler in action.
Extending This System
From my time working on multiple Unity projects, here's what I typically add next:
A Command base class instead of just an interface, which can include shared functionality like status flags or priority levels. Here's the exact code I use:
public abstract class Command
{
public bool IsFinished { get; protected set; }
public abstract IEnumerator Execute();
}
This abstract base class provides a common IsFinished property that any command can use to track its completion state, while still requiring each concrete command to implement its own Execute() method. This gives you more flexibility than a pure interface while maintaining the polymorphic benefits.
Additional command types like WaitCommand (pauses for X seconds), DialogueCommand (shows text and waits for player input), or AnimationCommand (plays an animation clip).
Queue prioritization where certain commands can jump to the front of the queue for urgent actions.
What This System Will Actually Do for Your Game
Here's the practical reality of implementing a Unity task scheduler. After working on multiple Unity projects, I can tell you exactly what changes:
Your game logic becomes predictable. No more "it works sometimes but breaks other times" bugs. When you know every action executes in a defined order, debugging becomes dramatically easier. You can trace exactly what happened and when.
Complex features become simple. Building a tutorial system? Just queue up the instruction commands. Creating a cutscene? Queue up the camera movements, character actions, and dialogue. The scheduler handles all the synchronization—you just define the sequence.
Your code becomes more maintainable. Six months from now when you need to add a new action type, you'll create a new ICommand class and be done. No hunting through manager scripts to figure out where to hook in your logic.
You'll handle edge cases gracefully. Player tries to attack while already attacking? The second attack gets queued and happens after the first completes. No crashes, no weird animation blending—just logical, sequential behavior.
This is why I always recommend studying this approach early in your Unity learning journey. It's one of those patterns that separates hobby projects from professional-quality games.
Your Next Steps
If you're building your first Unity event queue system, here's what I recommend:
Start with the simple implementation I showed above. Get the basic scheduler working with a couple of movement commands. Actually build it—reading code isn't the same as writing it. You'll understand the pattern far better once you've debugged your first queue issue.
Then expand with different command types. Add a WaitCommand for delays. Add a DebugCommand that just logs a message. The more command types you create, the more you'll internalize the flexibility of the pattern.
Explore coroutine-based approaches. Unity coroutines are incredibly powerful for this kind of system. The Unity Docs on Coroutines are worth reading thoroughly.
Study turn-based games. Load up Final Fantasy, XCOM, Into the Breach, or any tactical RPG and pay attention to how actions resolve. You're now equipped to recognize the task scheduling patterns they're using under the hood.
Wrapping Up: The System That Changed How I Build Games
The Unity task scheduler with an event queue isn't just a technical pattern—it's a mindset shift. Instead of thinking "how do I make this action happen right now," you start thinking "what sequence of actions do I want, and in what order?"
Once you internalize this approach, you'll find yourself reaching for it constantly. Turn-based combat, cutscenes, UI sequences, tutorial flows—they all become variations on the same elegant solution: define your commands, queue them up, let the scheduler handle the timing.
This is the system that finally made my combat logic stop breaking. It's the foundation of every reliable, maintainable game event system I've built since. And now it's yours to use.
Ready to Start Building Your First Game?
If you've made it this far, you're clearly serious about learning Unity game development. Here's the thing—reading about systems like task schedulers is valuable, but nothing beats actually building a complete game from scratch.
I created the Mr. Blocks Course specifically for developers like you who want to go from understanding concepts to shipping a finished product. You'll build a full game from the ground up, applying patterns like the one we just covered in a real project context.
The course takes you from basic Unity setup all the way through to a polished, professional game experience. You'll learn by doing—which, trust me, is the only way this stuff really sticks.
Check out the Mr. Blocks Course here and start building the game you've been planning in your head.
Key Takeaways
- Unity task scheduler systems prevent action chaos by processing game events sequentially in a First-In, First-Out queue, ensuring predictable and stable game logic.
- The Command Pattern (
ICommandinterface) is superior for complex games because it allows stateful, configurable commands that can span multiple frames, unlike simple Action delegates. - Use
System.Collections.Generic.Queue<T>as your core data structure withEnqueue()to add tasks andDequeue()to remove them in order. - Coroutine-based schedulers are more elegant than boolean flag approaches—
yield return StartCoroutine(task.Execute())automatically waits for task completion. - Callbacks (Action delegates) let asynchronous tasks signal completion to the scheduler, enabling proper synchronization of multi-frame operations.
- This pattern is the foundation of all turn-based games—from Final Fantasy to Hearthstone to Into the Breach—because it guarantees action order and prevents race conditions.
- Combining task schedulers with an event bus creates powerful decoupling where game events are broadcast loosely and converted into queued commands by a central manager.
- The
IEnumerator Execute()interface signature makes commands inherently coroutine-compatible, allowing natural integration with Unity's timing system. - Abstract base classes like
Commandwith shared properties (IsFinished) provide more flexibility than pure interfaces while maintaining polymorphic benefits for command queues.
Common Questions
What is a Unity task scheduler and why do I need one?
A Unity task scheduler is a system that manages the execution order of game events using a queue. You need one to prevent multiple actions from firing simultaneously and creating unpredictable behavior, especially in turn-based games, cutscenes, or any situation where actions must happen in a specific sequence.
How does a Unity event queue differ from just calling functions directly?
When you call functions directly, they execute immediately and can interrupt each other. A Unity event queue adds actions to a waiting line and processes them one at a time in order, ensuring each action completes fully before the next one begins. This creates predictable, controllable game logic.
What's the difference between a simple Action queue and the Command Pattern?
A simple Queue<Action> is lightweight and perfect for instant, stateless operations like triggering sound effects. The Command Pattern using ICommand is better for complex, stateful operations that take time to complete (like animations or movement) because each command can store data and manage its own execution across multiple frames.
When should I use Unity sequential commands instead of firing events immediately?
Use Unity sequential commands whenever you need actions to happen in a specific order without interrupting each other—turn-based combat, tutorial sequences, cutscenes, quest chains, or any multi-step gameplay flow where timing and order are critical.
How do I handle tasks that take multiple frames to complete?
Use Unity coroutines. Make your ICommand.Execute() method return an IEnumerator, then use yield return null inside the command to spread execution across frames. The scheduler waits for the coroutine to finish before processing the next command.
What's the best way to implement a Unity turn based combat system?
Use a task scheduler with the Command Pattern. Create commands for each action type (AttackCommand, MagicCommand, MoveCommand), queue them up based on turn order, and let the scheduler process them sequentially. This ensures animations and effects complete fully before the next character acts.
Can I use Unity coroutine queue systems for things other than combat?
Absolutely. I use this pattern for cutscenes (camera movements, character actions, dialogue), tutorial systems (wait for player action → show next hint), UI animation sequences, and any gameplay feature that requires controlled timing and order.
How do I integrate a Unity game event system with a task scheduler?
Use an event bus to broadcast game events loosely, then have a central manager listen for those events and convert them into commands that get enqueued. This decouples the "what happened" from "how we handle it," keeping your code clean and maintainable.
What is the Unity ICommand interface and why do I need it?
The ICommand interface is a contract that guarantees every command class has an Execute() method. This lets you store different types of commands (move, attack, dialogue) in the same queue and process them uniformly, even though they do completely different things internally.
Should I use Update() with boolean flags or a coroutine for my scheduler loop?
Use a coroutine. The while(true) coroutine pattern with yield return StartCoroutine(task.Execute()) is cleaner and automatically handles synchronization. Boolean flag approaches work but require more manual state management and are easier to mess up.