Debug Line and On-Screen Log Drawing Components for XNA


LIke the scaffolding around an under-construction building, it’s important to have the right structures in place to aid your work even though you won’t need them once the job is done. In the case of games, this takes the form of the various bits of functionality used to debug games as they’re being built.

In the hope that they prove useful to other XNA developers, I’d like to introduce a couple of GameComponents that I use to provide debug functionality for my games: LineBatchComponent and ScreenLogComponent.

As nice as it is, XNA doesn’t include an off-the-shelf solution for drawing lines on the screen so, in order to do so, you have to implement line drawing manually with DrawUserIndexedPrimitive. Having implemented the same code in a couple different projects I finally decided to wrap the code up in a component so that it would be trivially easy to pop into future projects.

With the line drawing component added to your game, drawing lines is as easy as calling the static AddLine function. I also implemented utility functions for drawing rectangles since that seemed to be what I was using it for most of the time. If you set a transformation matrix on a sprite batch, you can pass the same matrix to the SetWorldMatrix() function to keep the debug lines aligned with your rendered sprites.

The other debug component I often use is for drawing text to the screen in a scrolling log. Like the line drawing component, you simply need to add it to your game and then call a static function to output text to the log. As new text is added to the log, older entries are pushed up the screen until they disappear, creating logs like the following:

This sort of debug functionality is pretty basic stuff, but once you start using it, you won’t realize how much you’d miss it if it weren’t there. With that in mind, here is the code for those who’d like it.

LineBatchComponent:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace EstrellaLib.Debug
{
    struct LineData
    {
        public LineData(Vector2 point1, Vector2 point2, Color color)
        {
            Point1 = point1;
            Point2 = point2;
            LineColor = color;
        }

        public Vector2 Point1;
        public Vector2 Point2;
        public Color LineColor;
    }

    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class LineBatchComponent : Microsoft.Xna.Framework.DrawableGameComponent
    {
        Matrix viewMatrix;
        Matrix projectionMatrix;

        BasicEffect basicEffect;
        VertexDeclaration vertexDeclaration;
        VertexPositionColor[] pointList;
        VertexBuffer vertexBuffer;

        const int LINE_BATCH_VERTEX_COUNT = 4096;
        short[] lineListIndices;
        int linesToDraw;

        static Matrix worldTransform;
        static List<LineData> lines = new List<LineData>(1024 / 2);

        public LineBatchComponent(Game game)
            : base(game)
        {
            worldTransform = Matrix.CreateTranslation(0.0f, 0.0f, 0.0f);
        }

        public static void AddLine(Vector2 point1, Vector2 point2, Color color)
        {
#if SHIPPING
#else
            lines.Add(new LineData(point1, point2, color));
#endif
        }

        public static void AddRect(Rectangle rect, Color color)
        {
#if SHIPPING
#else
            AddRect(new Vector2(rect.Left, rect.Top), new Vector2(rect.Right, rect.Bottom), color);
#endif
        }

        public static void AddRect(Vector2 upperLeft, Vector2 lowerRight, Color color)
        {
#if SHIPPING
#else
            AddRect(upperLeft, new Vector2(lowerRight.X, upperLeft.Y), new Vector2(upperLeft.X, lowerRight.Y), lowerRight, color);
#endif
        }

        public static void AddRect(Vector2 upperLeft, Vector2 upperRight, Vector2 lowerLeft, Vector2 lowerRight, Color color)
        {
#if SHIPPING
#else
            AddLine(upperLeft, upperRight, color);
            AddLine(upperRight, lowerRight, color);
            AddLine(lowerRight, lowerLeft, color);
            AddLine(lowerLeft, upperLeft, color);
#endif
        }

        public static void SetWorldMatrix(Matrix world)
        {
            worldTransform = world;
        }

        public override void Initialize()
        {
#if SHIPPING
#else
            base.Initialize();

            InitializeTransform();
            InitializeEffect();
            InitializePoints();
            InitializeLineList();
#endif
        }

        private void InitializeTransform()
        {

            viewMatrix = Matrix.CreateLookAt(
                new Vector3(0.0f, 0.0f, 1.0f),
                Vector3.Zero,
                Vector3.Up
                );

            projectionMatrix = Matrix.CreateOrthographicOffCenter(
                0,
                (float)GraphicsDevice.Viewport.Width,
                (float)GraphicsDevice.Viewport.Height,
                0,
                1.0f, 1000.0f);
        }

        private void InitializeEffect()
        {
            vertexDeclaration = new VertexDeclaration(new VertexElement[]
                {
                    new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
                    new VertexElement(12, VertexElementFormat.Color, VertexElementUsage.Color, 0)
                }
            );

            basicEffect = new BasicEffect(GraphicsDevice);
            basicEffect.VertexColorEnabled = true;

            basicEffect.World = worldTransform;
            basicEffect.View = viewMatrix;
            basicEffect.Projection = projectionMatrix;
        }

        private void InitializePoints()
        {
            pointList = new VertexPositionColor[LINE_BATCH_VERTEX_COUNT];

            // Initialize the vertex buffer, allocating memory for each vertex.
            vertexBuffer = new DynamicVertexBuffer(Game.GraphicsDevice, vertexDeclaration,
                LINE_BATCH_VERTEX_COUNT, BufferUsage.None);

            // Set the vertex buffer data to the array of vertices.
            vertexBuffer.SetData<VertexPositionColor>(pointList);
        }

        private void InitializeLineList()
        {
            // Initialize an array of indices of type short.
            lineListIndices = new short[LINE_BATCH_VERTEX_COUNT];
        }

        protected override void LoadContent()
        {
#if SHIPPING
#else
            base.LoadContent();
#endif
        }
        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
#if SHIPPING
#else
            base.Update(gameTime);
#endif
        }

        public override void Draw(GameTime gameTime)
        {
#if SHIPPING
#else
            linesToDraw = 0;
            int currentVertex = 0;
            int currentIndex = 0;
            foreach (LineData line in lines)
            {
                lineListIndices[currentIndex++] = (short)currentVertex;
                pointList[currentVertex++] = new VertexPositionColor(new Vector3(line.Point1.X, line.Point1.Y, 0.0f), line.LineColor);

                lineListIndices[currentIndex++] = (short)currentVertex;
                pointList[currentVertex++] = new VertexPositionColor(new Vector3(line.Point2.X, line.Point2.Y, 0.0f), line.LineColor);

                ++linesToDraw;
                if (LINE_BATCH_VERTEX_COUNT - currentVertex < 2)
                {
                    break;
                }
            }
            lines.Clear();
            vertexBuffer.SetData<VertexPositionColor>(pointList);

            basicEffect.World = worldTransform;
            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                DrawLineList();
            }
            base.Draw(gameTime);
#endif
        }

        /// <summary>
        /// Draws the line list.
        /// </summary>
        private void DrawLineList()
        {
            if (linesToDraw > 0)
            {
                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(
                    PrimitiveType.LineList,
                    pointList,
                    0,  // vertex buffer offset to add to each element of the index buffer
                    linesToDraw * 2,  // number of vertices in pointList
                    lineListIndices,  // the index buffer
                    0,  // first index element to read
                    linesToDraw   // number of primitives to draw
                );
            }
        }
    }
}

ScreenLogComponent:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace EstrellaLib.Debug
{
    public class ScreenLogComponent : Microsoft.Xna.Framework.DrawableGameComponent
    {
        SpriteFont _logFont;
        SpriteBatch _logSpriteBatch;
        float _lineHeight;
        
        public Vector2 DisplayPosition;
        public Color DisplayColor;

        const int DISPLAY_LINE_COUNT = 10;
        static List<string> _logStrings = new List<string>(DISPLAY_LINE_COUNT);
        static GameTime _lastTime;

        public ScreenLogComponent(Game game)
            : base(game)
        {
        }

        public override void Initialize()
        {
            base.Initialize();

            DisplayPosition = new Vector2((float)Game.GraphicsDevice.PresentationParameters.BackBufferWidth * 0.05f, (float)Game.GraphicsDevice.PresentationParameters.BackBufferHeight * 0.9f);
            DisplayColor = Color.Magenta;
        }

        protected override void LoadContent()
        {
            base.LoadContent();

            _logFont = Game.Content.Load<SpriteFont>("Debug/Fonts/ScreenLog");
            _logSpriteBatch = new SpriteBatch(Game.GraphicsDevice);
            _lineHeight = _logFont.MeasureString("A").Y;
        }

        public static void AddLog(String log)
        {
            _logStrings.Insert(0, String.Format("[{0}]: {1}", _lastTime.TotalGameTime, log));
            while( _logStrings.Count >= DISPLAY_LINE_COUNT )
            {
                _logStrings.RemoveAt(_logStrings.Count - 1);
            }
        }

        public override void Update(GameTime gameTime)
        {
            _lastTime = gameTime;
            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            Vector2 drawPosition = DisplayPosition;
            _logSpriteBatch.Begin();
            foreach (String log in _logStrings)
            {
                _logSpriteBatch.DrawString(_logFont, log, drawPosition, DisplayColor);
                drawPosition.Y -= _lineHeight;
            }
            _logSpriteBatch.End();
        }
    }
}
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print