You probably want an Agent Task System
Organizing a variety of tasks or orders in game development
No matter what kind of game you're creating, if you have units, characters or agents performing different kinds of actions you probably want an agent task system. In this article, we'll use the separation of concerns principle to create an adaptable and modular agent task system for games. It's aimed at novice developers.
As I mentioned in my introductory article about real-time strategy games, most games with characters or agents benefit from a modular task system. This is due to the variety of what the agents should be capable of doing. For example, the same agent is usually capable of moving, attacking, gathering resources, returning resources, entering transports and constructing buildings.
A user on Reddit, commenting on the previous article, asked me to elaborate on this point, so here goes.
Disclaimer
What we'll cover is just one example of a barebones implementation, your mileage may vary. It's first and foremost intended to help you get started. This article is heavier on code, but it is by no means a full implementation.
Henceforth, when talking of agents or characters, I'll use the word Unit. Likewise, I'll use the word Order for tasks. This is purely a semantic choice to fit the context of a real-time strategy game — though, as I emphasize, the system itself is also well-suited to other genres.
As always, the code here targets Unity and C# in a game development context.
If you were to implement all this functionality in one and the same class, you'd quickly end up with a mess.
We want to create an organized system. You could call this system a "Command System", "Task System", "Unit Command System" or something else. There does not seem to be an agreed-upon standard. I've chosen to call it "agent task system" for the purpose of this article, on the basis that we'll focus on making a system for agents performing tasks.
The Problem
We end up in a mess trying to tangle the functionality of cutting trees, patrolling, moving and attacking in the same place. The bigger the mess, the harder for one to understand, debug and adjust.
With the problem identified and decision taken to implement a system to handle it, we define our goal, what do we want the system to achieve?
- The logic of different orders should be separate and not affect one another.
- It should be easy for developers (you) to add, remove or adjust different kinds of orders.
What requirements do we have? In other words, what would we, the players, expect from a task system. Something like:
- Orders should be queueable. e.g., move here, then there, then cast a spell.
- Orders should be interruptible at any point. Either by the player or other game factors.
- Orders start and they finish. They're not designed to exist forever.
- Orders do something — the unit should not care what.
What other facts do we know?
- The 'owner' of an order, in our case, is for now, always a unit.
- Orders have different interests, a gathering order must know what resource to target, a move order a position to move to, and so on.
Given these goals and requirements, we should follow the separation of concerns design principle. It's a principle equally applicable to game development as to software- or webdevelopment. Like most principles, it's abstract (we'll soon return to that term) and can be applied in almost anything you develop. But — it's a good one! I recommend you try to keep it in mind no matter what you're programming. Doing so will make it easier to share systems between your games and projects.

Bring it down a level of abstraction and we find the Command Pattern. No exact definition exists here either, but there is some common ground among most. For example, we'll not be implementing redo/undo functionality as is included in the classic definition, but it's still the nearest we'll get to any known 'pattern'. Here are some resources if you want to read up more on the pattern itself:
- Command, Refactoring.Guru
- Command, Robert Nystrom
- Command pattern, Wikipedia
The core idea from the command pattern that we'll use is encapsulating an action as an object, decoupling execution — in order to achieve separation of concerns. So instead of the tangled mess that might or might not be our unit class right now, we'll tackle this with an object-oriented approach where each task is responsible for its own execution.
Creating an Order Manager
Again, as we don't want to clog our class Unit
, we create an class OrderManager
.
Each unit will have one of its own public OrderManager Orders
. The first thing we give the manager is a collection, because we know units can have multiple orders. For the sake of simplicity, we'll go with a list for now.
Hold on! A list of what? Orders of course. However, these orders vary in nature — that's the whole problem! We need to use abstraction here. Abstract classes and interfaces provide a way of defining behavior without implementation.
About abstraction
Abstraction hides complexity by exposing only essential behavior. In C#, you use abstract classes or interfaces to define this behavior, but they serve different purposes.
An abstract class
is a partial implementation: it can contain fields, constructors, and shared method logic. It's ideal when related classes share common functionality but also need to implement specific methods.
An interface
defines a contract without implementation*. It's used when unrelated types need to guarantee certain behaviors, like IExecutable or ICancelable.
*Since C# 8.0, interfaces can provide a default implementation of methods. The Unity Engine uses C# 9.0. But they're still only contracts, they cannot have fields or constructors.
In other words, abstract is a "template" which can contain base logic and variables, while interface is just an empty "template" stating what an inheriting class must implement.
Another important thing to note is that a class can implement multiple interfaces, but only ever inherit from one class. In general, use an interface if you can.
So we create an interface IOrder
.
The I is a common way of prepending interface names and lets us know at a glance what we're looking at.
If you at a later point find that you're copying code between different order types (violating the DRY principle — Don't repeat yourself),
you can make an abstract class BaseOrder: IOrder
or class BaseOrder: IOrder
(with virtual
methods) which implements IOrder, keeping the same contract for modularity. For now, we don't do anything more in the interface, it's empty.
Going back to our requirements, we know that orders should start, finish, be queueable and interruptible and most importantly, do something. So we'll need at least these methods to call from our unit.
- Requirement "start": a method to start a new order
- Requirement "be queueable": a method to queue a new order
- Requirement "do something": a method to process the current order
The class OrderManager
could look something like this:
public List<IOrder> Queue = new List<IOrder>();
// A shorthand to get the first (current) order in the list
public IOrder Current => Queue.Count == 0 ? null : Queue[0];
// REQUIREMENT: "be queueable" and "start".
public void New(IOrder order, bool queue = false)
{
// 1) Check if the order is valid,
// this logic is ideally a part of the Order type itself,
// implemented through a Validate() method
if (!order.Validate()) return;
// Think about what we want to do if a order is not valid.
// Perhaps we should Clear all current orders?
// In this case, we just return and ignore it.
// 2) Add it our collection, front or end based on queue parameter
// For example: the queue parameter will probably be true
// if the player is holding down shift.
if (!queue)
Clear();
Queue.Add(order);
// 3) Run it if it is the first order in the list
if (Current == order)
order.Run();
}
// REQUIREMENT: "be interruptible"
public void Clear()
{
// When a unit dies or or just ordered to cancel all orders,
// we call this method, clearing the list.
if (Current != null)
{
Current.ClearState();
}
Queue.Clear();
}
// REQUIREMENT: "be interruptible" and "finish"
// Primarily called by an order when it finishes.
// Could also be called through player action.
public void RemoveOrder(IOrder order)
{
if (order == null || !Queue.Contains(order)) return;
order.ClearState();
Queue.Remove(order);
// You have to decide if you want to run the next order (if queued)
// here immediately or just wait for the next Process()
}
// REQUIREMENT: "do something"
public void Process()
{
// Process order if we have any.
if (Current == null)
return;
Current.Process();
}
Now we can start a new order by calling unit.Orders.New(...)
, either by clearing all orders queue = false
or queueing it queue = true
.
But wait, say we have a class MoveOrder : IOrder
with a bool IsAttackMove
parameter. Our intent with that is that if the unit comes across an enemy,
we want the MoveOrder
to start an AttackOrder
without removing itself from the OrderManager
.
When the AttackOrder
is finished, the MoveOrder
will continue.
Currently, we could only either replace the whole list with the AttackOrder
, or put it at the end of it.
public void NewPriority(IOrder order)
{
// This should pause the current order and
// issue a new order in front of the list.
// _Without_ clearing all orders.
// If we have some logic, effects or other things that
// should only be active while an order is running
// we call a ClearState() method
// _whenever_ we stop an order.
if (Current != null)
{
Current.ClearState();
}
// ... move new order to front
}
Sweet, this is what we'll call when we want to issue a new order without forgetting about those in the queue. Usually it's not something the players could do themselves directly.
We could use this same functionality in a GatherOrder
to run StoreResources
when the unit cannot carry any more resources, by which GatherOrder
will continue when the resources have been stored!
Alternative solution to prioritizing new orders
Alternatively, a cleaner solution could be to implement an enum and use that as a parameter instead of a bool in the void New()
method, outright skipping the void NewPriority()
method.
public enum OrderIssueType
{
FrontAndClear, // Used instead of queue = false
BackAndKeep, // Used instead of queue = true
FrontAndKeep // Used instead of NewPriority()
}
Defining the IOrder Interface
With the base of public OrderManager
in place, it's time to define the structure of an order. We're already referencing all these methods in our OrderManager
.
Every order must implement this contract — providing methods for starting, processing, and clearing the state.
public interface IOrder {
// FACT: Owner is always a unit.
// The Unit whose order this is.
// Further on, you might expand the OrderManager to also handle buildings,
// in which case, you should target a common interface or parent class of
// the two.
public Unit Owner { get; set; }
// Returns whether the order has been instantiated with valid parameters.
bool Validate();
// REQUIREMENT: "do something"
// Called once when the order begins execution.
// Should do the heavy lifting, set all references, perform pathfinding, etc.
void Run();
// REQUIREMENT: "do something"
// Called every frame or tick while this is the current order
// by the OrderManager.
// Must check if the order is finished.
void Process();
// REQUIREMENT: "be interruptible" and "finish"
// Called when the order is finished OR interrupted.
void ClearState();
}
Each implementation of IOrder
can be stateful and tailored to its purpose — but they all obey the same structure. Let's look at an example.
Example: MoveOrder
The MoveOrder
instructs a unit to move to a specific world position. It needs a reference to the unit and a destination, and an optional value for whether to attack move (stopping to attack enemies along the way).
public class MoveOrder : IOrder
{
public Unit Owner { get; set; }
private Vector3 destination;
private bool isAttackMove;
private const float margin = 0.1f;
private FlagMarker flagMarker;
public MoveOrder(Unit unit, Vector3 destination, bool isAttackMove = false)
{
Owner = unit;
this.destination = destination;
this.isAttackMove = isAttackMove;
}
public bool Validate()
{
// In this example, the order is valid if:
// the Unit has been set to an instance and is Alive,
// destination has been set and is within World space
return Owner != null &&
Owner.IsAlive &&
destination != null &&
World.IsVectorWithinBounds(destination);
}
public void Run()
{
// The unit MoveTo() method performs the necessary
// pathfinding calculations and starts the movement.
Owner.MoveTo(destination, OnArrived);
// In this example, we want to display a flag marker at the destination
// as long as the order is active.
// This is purely for showcasing the need of a ClearState method.
flagMarker = new FlagMarker(destination);
}
public void Process()
{
// Movement is likely handled by the Unit itself.
// The only thing we do is in this case is
// check whether the unit has arrived.
if (Owner.DistanceTo(destination) <= margin)
{
Complete();
return;
}
// Here is the logic for switching to an AttackOrder
// in case isAttackMove is true, mentioned earlier.
if (!isAttackMove)
return;
Unit nearestEnemy = Owner.NearestEnemyWithinAcquisitionRange();
if (nearestEnemy != null)
{
Owner.NewPriority(new AttackOrder(Owner, nearestEnemy));
// We return in case we add more code below in the future.
return;
}
}
// Only called within this class, see comments below.
private void Complete()
{
ClearState();
Owner.Orders.RemoveOrder(this);
}
// This gets called by both Complete() and OrderManager.RemoveOrder().
// If we called Complete() from the OrderManager we would
// end up with circular logic (like an endless loop).
// Also, we might run specific logic only when an order completes.
// An example could be an AttackOrder which immediately searches
// for new targets upon killing (completing) the current target.
public void ClearState()
{
flagMarker.Destroy();
}
}
Putting It All Together
Each unit has its own OrderManager
. When the player issues a command, the unit receives a new IOrder
like MoveOrder
, AttackOrder
, or BuildOrder
through OrderManager.New()
.
The system runs per-frame (or tick) by calling OrderManager.Process()
on each unit. Orders run, check if they're complete, and gracefully transition to the next. If the queue becomes empty, the unit is idle.
When we use the system in our game, it could look like:
orderedUnit.Orders.New(new MoveOrder(orderedUnit, LocalPlayer.MouseWorldPosition));
You might notice something odd here
Do we really need to mention orderedUnit twice?
You could leave out the Unit
parameter from the Order constructor and add it through the OrderManager.New()
methods.
Because realistically, you would probably never want to assign an order to a unit which belongs.. to another unit.
You might need it in the constructor though, for validation, so we've left it there to keep it simple. The alternative is to leave the constructor empty and
create a new method called for example void Create()
or void Instantiate()
which runs after the OrderManager
has set the Unit Owner
property.
Then, in order to call it, you need to define the new method in the interface IOrder()
, because that's the only thing the OrderManager
speaks with.
Further Improvements
Here are a collection of some things you might want to implement further on, which I've found useful myself but we have not brought up in this article:
- Implement events in the OrderManager for Cleared, Idle, IdleEnded, if your game has need of them.
- Make a
class BaseClass : IOrder
with abstract or virtual methods which holdspublic Unit Owner { get; set; }
, base validation and other shared functionality — in order to avoid repeating yourself. - Add an
IOrder Next
property to the OrderManager, some orders might vary their behavior depending on the next queued order. - Make a way for different orders to respond to the Unit getting stuck, depending on your pathfinding solution.
- Implement logging and tracking the history of orders on a unit instance basis for debugging purposes.

Final Thoughts
We have achieved our goals which were:
- The logic of different orders should be separate and not affect one another.
- It should be easy for developers (you) to add, remove or adjust different kinds of orders.
Now, we have something that is:
- Modular: Each order is self-contained and easy to test or extend.
- Composable: Orders can queue other orders, forming intelligent behaviors.
- Interruptible: You can cancel or override orders on the fly, enabling responsive unit control.
This system is simple to start with, yet powerful enough to scale into more complex logic. I believe you will soon realize how you can use the same principles for other systems in your game development endeavours. And as was the point with the previous article, this system is not exclusive to real-time strategy games. The moment you have units, agents or characters which need to handle multiple kinds of tasks — don't write monolithic logic. Make a task system.
I hope this article was as enjoyable for you to read as it was for me to write. If you're interested in future posts, follow me on your preferred platform:
Ps. if you prefer less rambling, follow the THUNGSTEN page instead. Links in the footer below.