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
- F5 — Start debugging
- F9 — Toggle breakpoint
- F10 — Step over
- F11 — Step into
- Shift+F5 — Stop debugging
- Ctrl+Shift+F5 — Restart debugging
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
- Go to Debug → Performance Profiler.
- Select CPU Usage and/or .NET Object Allocation Tracking.
- Run your game for a representative session.
- 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
- Free, open-source frame debugger that works with OpenGL and DirectX.
- Capture a frame, step through draw calls, inspect textures and shader state.
- Launch your game through RenderDoc or inject at runtime.
Visual Studio Graphics Debugger
- Available for DirectX projects (WindowsDX template).
- Debug → Graphics → Start Graphics Debugging.
- Capture frames and inspect pixel history, shader debugging, and draw call details.
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.