Command Queue for Actions


Overview

A Command Queue is a design pattern that uses a queue-based structure (FIFO) to collect commands each frame and process them consistently, separating game systems from object behavior. It organizes game logic around queued commands and helps control actions in a clean and flexible way. (e.g., used in D3D12 FlightDemo)

⚠️ Note: This pattern improves structure and modularity, but may introduce overhead due to deferred execution, and can be harder to debug if commands are misrouted or delayed unexpectedly.

View Repository


Structure Diagram

  • White border: process flow
  • Yellow border: commands container

Class Descriptions

Command Struct - Code Breakdown

#pragma once
#include <functional>
 
struct FCommand
{
	using Action = std::function<void(float)>;
 
	Action action;
};
Wraps a callable action (usually a lambda or function object) that takes float deltaTime as its parameter. This allows the command to handle time-dependent logic.


CommandQueue Class - Code Breakdown

#pragma once
#include <queue>
#include "Command.h"
 
class CCommandQueue
{
public:
	CCommandQueue() = default;
	~CCommandQueue() = default;
 
private:
	std::queue<FCommand> mQueue;
 
public:
	void Enqueue(const FCommand& command)
	{
		mQueue.push(command);
	}
 
	FCommand Dequeue()
	{
		if (IsEmpty())
			return FCommand{};
 
		FCommand cmd = mQueue.front();
		mQueue.pop();
 
		return cmd;
	}
 
	bool IsEmpty() const
	{
		return mQueue.empty();
	}
};
The CCommandQueue manages a FIFO list of FCommand and handles their execution. It decouples command creation from execution timing, allowing cleaner and more modular game logic.


Code Breakdown

Usage Example

#define FakeDeltaTime 0.016f
 
int main()
{
	CScene*   scene   = new CScene;
	CPlayer*  player  = new CPlayer;
	CMonster* monster = new CMonster;
 
	scene->AddObject(player);
	scene->AddObject(monster);
 
	// This simulates 5 frames, pushing and executing one command per frame.
	for (int frame = 1; frame <= 5; frame++)
	{
		std::cout << ">> [Frame " << frame << "] <<\n";
 
		FCommand cmd;
		cmd.action = [player, monster, frame](float dt)
		{
			if (frame % 2 == 0)
				player->PlayerAction(dt);
			else
				monster->MonsterAction(dt);
		};
		scene->PushCommand(cmd);
		
		scene->Update(FakeDeltaTime);
 
		std::cout << "\n";
	}
	delete scene;
 
	return 0;
}
This example simulates a 5-frame loop where each frame generates a command using a lambda that captures player, monster, and frame. Each command is pushed to the scene via PushCommand() and executed during Scene::Update().


CScene::Update()

void CScene::Update(float deltaTime)
{
	// Process all pending commands
	while (!mCommandQueue.IsEmpty())
	{
		FCommand cmd = mCommandQueue.Dequeue();
		if (cmd.action)
			cmd.action(deltaTime);
	}
}
This method dequeues from mCommandQueue, then executes all pending commands in order.


Conclusion

The command queue pattern can clearly separate logic from execution, making frame-based system more modular and maintainable, while also improving structure, timing control, and batch processing optimization.

On the other hand, it may introduce overhead due to deferred execution, complicate command ordering and debugging, and increase memory usage if commands are continuously retained in the queue.

Therefore, it’s important to consider these carefully and decide whether to adopt it based on the specific requirements of the project.