Tools

Debugging & Development Tools

Bugs are inevitable. This guide equips you with practical debugging techniques, profiling strategies, and development tools to diagnose and fix issues in your MonoGame projects quickly.

Download the runnable sample project

Use the verified ZIP bundle for this article to review the extracted example files, run the sample host, and repackage the sources without bin or obj folders.

In-game debug overlay

A debug overlay shows FPS, entity count, memory usage, and custom metrics directly on screen. Toggle it with a key press so it is always available during development.

public class DebugOverlay
{
    private SpriteFont _font;
    private int _frameCount;
    private float _elapsed;
    private float _fps;
    private bool _visible = true;

    public DebugOverlay(SpriteFont font) => _font = font;

    public void Toggle() => _visible = !_visible;

    public void Update(GameTime gameTime)
    {
        _elapsed += (float)gameTime.ElapsedGameTime.TotalSeconds;
        _frameCount++;
        if (_elapsed >= 1f)
        {
            _fps = _frameCount / _elapsed;
            _frameCount = 0;
            _elapsed = 0f;
        }
    }

    public void Draw(SpriteBatch sb, int entityCount, long memoryBytes)
    {
        if (!_visible) return;
        sb.Begin();
        float y = 10f;
        sb.DrawString(_font, $"FPS: {_fps:F1}", new Vector2(10, y), Color.LimeGreen);
        y += 20f;
        sb.DrawString(_font, $"Entities: {entityCount}", new Vector2(10, y), Color.Cyan);
        y += 20f;
        sb.DrawString(_font, $"Memory: {memoryBytes / 1024 / 1024} MB",
            new Vector2(10, y), Color.Yellow);
        sb.End();
    }
}

// Toggle with F3
if (kb.IsKeyDown(Keys.F3) && !prevKb.IsKeyDown(Keys.F3))
    _debugOverlay.Toggle();

Structured logging

Use System.Diagnostics.Debug and Trace for development logging. For production, consider a lightweight logger that writes to a file.

public static class GameLogger
{
    private static readonly string LogPath =
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game.log");

    public static void Info(string message) =>
        Log("INFO", message);

    public static void Warn(string message) =>
        Log("WARN", message);

    public static void Error(string message, Exception ex = null) =>
        Log("ERROR", ex != null ? $"{message} | {ex}" : message);

    private static void Log(string level, string message)
    {
        string line = $"[{DateTime.Now:HH:mm:ss.fff}] [{level}] {message}";
        System.Diagnostics.Debug.WriteLine(line);
        try { File.AppendAllText(LogPath, line + Environment.NewLine); }
        catch { /* swallow I/O errors in logging */ }
    }
}

// Usage:
GameLogger.Info("Player spawned at (100, 200)");
GameLogger.Error("Failed to load texture", ex);

Visual Studio debugging tips

Essential shortcuts

Conditional breakpoints

Right-click a breakpoint and add a condition. For example, break only when _health < 0 or when entityId == 42. This avoids hitting breakpoints thousands of times per frame.

Watch window and immediate window

Add expressions to the Watch window to monitor values across frames. Use the Immediate window to evaluate code at a breakpoint (_player.Position, _entities.Count).

Edit and Continue

Pause the game at a breakpoint, edit code, and press F5 to continue with the changes applied. This works for most logic changes and avoids restarting the game.

Performance profiling

Use the built-in Stopwatch class to measure specific code sections, or use Visual Studio’s Performance Profiler for a full session analysis.

private Stopwatch _updateTimer = new();
private Stopwatch _drawTimer = new();

protected override void Update(GameTime gameTime)
{
    _updateTimer.Restart();
    // ... game logic ...
    _updateTimer.Stop();
    base.Update(gameTime);
}

protected override void Draw(GameTime gameTime)
{
    _drawTimer.Restart();
    // ... rendering ...
    _drawTimer.Stop();

    // Log if frame takes too long
    if (_drawTimer.ElapsedMilliseconds > 16)
        GameLogger.Warn($"Draw took {_drawTimer.ElapsedMilliseconds}ms");

    base.Draw(gameTime);
}

Visual Studio Performance Profiler

  1. Go to Debug → Performance Profiler.
  2. Select CPU Usage and/or .NET Object Allocation Tracking.
  3. Run your game for a representative session.
  4. Stop profiling and review the hot path analysis.

Common errors and fixes

Error Cause Fix
ContentLoadException Asset name mismatch or missing build Check content name, rebuild content, verify MGCB entry
InvalidOperationException: Begin must be called before Draw Missing SpriteBatch.Begin() Ensure every Draw call is between Begin/End
ObjectDisposedException Using a texture or effect after disposal Check object lifetime, do not dispose shared resources
White or pink texture Texture failed to load or wrong path Verify Content path and check build output for errors
Game runs too fast or too slow Not using delta time Multiply all movement by ElapsedGameTime.TotalSeconds
DllNotFoundException: SDL2 Missing native library on Linux/macOS Install libsdl2-dev (see Platform Setup)
Shader compilation error HLSL syntax error or wrong shader model Check MGCB output, target ps_3_0 minimum for DesktopGL
No audio output OpenAL not installed or device not initialised Install OpenAL, check MediaPlayer and SoundEffect setup

Graphics debugging

Use tools to inspect draw calls, textures, shaders, and the GPU pipeline in real time.

RenderDoc

Visual Studio Graphics Debugger

Debug draw helpers

public static class DebugDraw
{
    private static Texture2D _pixel;

    public static void Init(GraphicsDevice gd)
    {
        _pixel = new Texture2D(gd, 1, 1);
        _pixel.SetData(new[] { Color.White });
    }

    public static void DrawRect(SpriteBatch sb, Rectangle rect,
        Color colour, int thickness = 1)
    {
        sb.Draw(_pixel, new Rectangle(rect.X, rect.Y, rect.Width, thickness), colour);
        sb.Draw(_pixel, new Rectangle(rect.X, rect.Bottom - thickness, rect.Width, thickness), colour);
        sb.Draw(_pixel, new Rectangle(rect.X, rect.Y, thickness, rect.Height), colour);
        sb.Draw(_pixel, new Rectangle(rect.Right - thickness, rect.Y, thickness, rect.Height), colour);
    }
}

// Usage: DebugDraw.DrawRect(_spriteBatch, playerBounds, Color.Red);

Memory and GC analysis

Garbage collection pauses cause visible hitches. Monitor allocations and avoid creating objects in Update or Draw.

private int _lastGCCount;

protected override void Update(GameTime gameTime)
{
    int gcCount = GC.CollectionCount(0);
    if (gcCount != _lastGCCount)
    {
        GameLogger.Warn($"GC occurred! Gen0 collections: {gcCount}");
        _lastGCCount = gcCount;
    }
    base.Update(gameTime);
}

// Tips to reduce allocations:
// - Use struct over class for short-lived data
// - Pre-allocate lists and arrays
// - Use object pools (see Game Architecture guide)
// - Avoid string concatenation in hot paths; use StringBuilder
// - Cache delegates and lambda closures

Hot reload workflow

Speed up iteration by hot-reloading content without restarting the game. Reload textures, sounds, and data files on a key press during development.

#if DEBUG
if (kb.IsKeyDown(Keys.F5) && !prevKb.IsKeyDown(Keys.F5))
{
    // Dispose old textures
    _playerTexture?.Dispose();

    // Force content manager to reload
    Content.Unload();
    _playerTexture = Content.Load<Texture2D>("player");
    _font = Content.Load<SpriteFont>("DefaultFont");

    GameLogger.Info("Content hot-reloaded");
}
#endif

Hot reload is a development-only feature. Wrap it in #if DEBUG to ensure it is stripped from release builds.