Speech Bubble Sample

So I allowed myself to get sidetracked to throw together a sample to answer a question in the App Hub forums. Since I love RPG games, both playing and developing, this will probably be useful to me sometime in the future so that’s how I’m justifying it.

The sample, which you can get from here, shows a quick semi-dynamic speech bubble with a couple of options. The class is less than 150 lines, allows however many lines of text you want, shows a graphic to let the user know if there’s more text that the character has to provide, and shows a pointer to the character that can be placed on either the left or right side of the bottom of the bubble. It uses more of my fantastic programmer art, but should work with whatever graphics you replace it with, assuming you don’t deviate from how I’ve set up the graphics. I could have used just one corner graphic and rotated for the other 3 sides, but I was lazy. Smile

Here’s the class in all its glory:

public enum PointerType
{
    None,
    Left,
    Right
}

public class SpeechBubble
{
    private int _width;

    private Vector2 _location;

    private string[] _text;

    private SpriteFont _font;

    //bubble textures
    private Texture2D _bottomBorder;
    private Texture2D _interior;
    private Texture2D _leftBorder;
    private Texture2D _leftBottomCorner;
    private Texture2D _leftTopCorner;
    private Texture2D _rightBorder;
    private Texture2D _rightBottomCorner;
    private Texture2D _rightTopCorner;
    private Texture2D _topBorder;

    private bool _more;
    private Texture2D _moreGraphic;

    private PointerType _pointerType;
    private Texture2D _pointer;

    public SpeechBubble(ContentManager content, int width, Vector2 location, string[] text, bool more = false, PointerType pointerType = PointerType.None)
    {
        _font = content.Load<SpriteFont>("font");

        _bottomBorder = content.Load<Texture2D>("bottomBorder");
        _interior = content.Load<Texture2D>("interior");
        _leftBorder = content.Load<Texture2D>("leftBorder");
        _leftBottomCorner = content.Load<Texture2D>("leftBottomCorner");
        _leftTopCorner = content.Load<Texture2D>("leftTopCorner");
        _rightBorder = content.Load<Texture2D>("rightBorder");
        _rightBottomCorner = content.Load<Texture2D>("rightBottomCorner");
        _rightTopCorner = content.Load<Texture2D>("rightTopCorner");
        _topBorder = content.Load<Texture2D>("topBorder");

        _moreGraphic = content.Load<Texture2D>("more");

        _pointer = content.Load<Texture2D>("pointer");

        _location = location;
        _width = width;

        _text = text;

        _more = more;

        _pointerType = pointerType;
    }

    public void Draw(SpriteBatch sb)
    {
        //top
        sb.Draw(_leftTopCorner, _location, Color.White);
        sb.Draw(_topBorder, new Rectangle((int)_location.X + _leftTopCorner.Width, (int)_location.Y, _width - _leftTopCorner.Width * 2, _leftTopCorner.Height), Color.White);
        sb.Draw(_rightTopCorner, _location + new Vector2(_width - _rightTopCorner.Width, 0), Color.White);

        //lines
        for (int i = 0; i < _text.Length + (_more ? 1 : 0); i++)
        {
            sb.Draw(_leftBorder, new Vector2(_location.X, _location.Y + _leftTopCorner.Height + (i * _leftBorder.Height)), Color.White);
            sb.Draw(_interior, new Rectangle((int)_location.X + _leftBorder.Width,
                                    (int)_location.Y + _leftTopCorner.Height + (i * _leftBorder.Height),
                                    _width - _leftBorder.Width * 2,
                                    _leftBorder.Height),
                    Color.White);
            sb.Draw(_rightBorder, new Vector2(_location.X + _width - _rightBorder.Width, _location.Y + _leftTopCorner.Height + (i * _leftBorder.Height)), Color.White);

            //leave space for more graphic if necessary
            if (i < _text.Length)
                sb.DrawString(_font, _text[i], new Vector2((int)_location.X + _leftBorder.Width, (int)_location.Y + _leftTopCorner.Height + (i * _leftBorder.Height)), Color.Black);
        }


        //bottom
        sb.Draw(_leftBottomCorner, _location + new Vector2(0, _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height), Color.White);

        switch(_pointerType)
        {
            case PointerType.Left:
            {
                sb.Draw(_pointer, _location + new Vector2(_leftBottomCorner.Width, _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height), Color.White);

                sb.Draw(_bottomBorder, new Rectangle((int)_location.X + _leftBorder.Width + _pointer.Width, 
                                                        (int)_location.Y + _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height, 
                                                        _width - _leftBorder.Width - _pointer.Width - _rightBorder.Width, 
                                                        _bottomBorder.Height), 
                                        Color.White);

                break;
            }
            case PointerType.Right:
            {
                sb.Draw(_bottomBorder, new Rectangle((int)_location.X + _leftBorder.Width,
                                                    (int)_location.Y + _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height, 
                                                    _width - _leftBorder.Width - _pointer.Width - _rightBorder.Width, 
                                                    _bottomBorder.Height), 
                                        Color.White);

                sb.Draw(_pointer, _location + new Vector2(_width - _pointer.Width - _rightBorder.Width, _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height), Color.White);

                break;
            }
            case PointerType.None:
            {
                sb.Draw(_bottomBorder, new Rectangle((int)_location.X + _leftBorder.Width,
                                                    (int)_location.Y + _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height,
                                                    _width - _leftBorder.Width - _rightBorder.Width, 
                                                    _bottomBorder.Height), 
                                        Color.White);

                break;
            }
        }

        sb.Draw(_rightBottomCorner, _location +  new Vector2(_width - _rightBottomCorner.Width, _leftTopCorner.Height + (_text.Length + (_more ? 1 : 0)) * _leftBorder.Height), Color.White);

        if (_more)
            sb.Draw(_moreGraphic, new Vector2(_location.X + _width - _rightBorder.Width - _moreGraphic.Width, _location.Y + _leftTopCorner.Height + (_text.Length * _leftBorder.Height)), Color.White);
    }
}

 

Creating an instance is fairly straightforward:

SpeechBubble _bubble1;
SpeechBubble _bubble2;
SpeechBubble _bubble3;

protected override void LoadContent()
{
      ...

      _bubble1 = new SpeechBubble(Content, 150, new Vector2(50, 50), new string[] { "This is a test", "This is only a test", "More text follows..." }, true);
      _bubble2 = new SpeechBubble(Content, 150, new Vector2(250, 250), new string[] { "This is a test", "This is only a test", "Nothing here" },false, PointerType.Left);
      _bubble3 = new SpeechBubble(Content, 150, new Vector2(150, 450), new string[] { "Another test", "Still testing", "More follows..." }, true, PointerType.Right);
}

Simply call the Draw method of the class in your own draw code:

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin();
    _bubble1.Draw(spriteBatch);
    _bubble2.Draw(spriteBatch);
    _bubble3.Draw(spriteBatch);
    spriteBatch.End();

    base.Draw(gameTime);
}

Feel free to let me know if I missed something that would be useful in the class.

30 Comments

  1. Wouldn’t using a texture atlas be more effective?

  2. Mach X Games says:

    Effective in what way. It’s not something that’s complicated, it wouldn’t really save much in terms of memory or rendering, and you don’t have to worry about pulling out chunks from the texture, making the code a bit easier to understand IMO.

  3. If you draw just a couple of items this way, then you wouldn’t save much, I agree. If you get into drawing hundreds, all these small inefficiencies add up.

    Just to draw these 3 bubbles you have over 60 texture changes, if I’m counting right (use my proxy I hacked to get the count: http://pastebin.com/9bjzzwzy). SpriteSortMode.Texture does not help much with HUD elements, as you usually need to control how to draw them in detail. Unfortunately, SpriteBatch doesn’t allow for injection of custom sorting modes as of XNA 4.0, even though it internally instantiates one of implementations of IComparer instance to deal with sorting. Also, SpriteBatch doesn’t expose any virtual methods, that you could override to Draw smarter.

    Bottom line is – your code, while conceptually easier to understand, is showing an example of something that may easily get out of control.

  4. Also, I did some testing for different SpriteSortModes. I looped through the drawing code for 3 bubbles 100 times, and here are the results on my machine:

    SpriteSortMode.Immediate = 31 FPS,
    SpriteSortMode.Deferred = 70 FPS,
    SpriteSortMode.Texture = 91 FPS,
    SpriteSortMode.BackToFront = 53 FPS and incorrect rendering,
    SpriteSortMode.FrontToBack = 50 FPS and incorrect rendering,

    While tempting, the SpriteSortMode.Texture, is not the mode you would want to use for HUD drawing.

  5. More results, with my hacked-in TextureAtlas:

    Immediate = 30 FPS,
    Deferred = 120 FPS,
    Texture = 90 FPS,
    BackToFront = 60 FPS,
    FrontToBack = 60 FPS,

    With text rendering disabled:

    NoAtlas+Immediate = 110 FPS
    NoAtlas+Deferred = 105 FPS,
    NoAtlas+Texture = ~380 FPS,
    NoAtlas+BackToFront = 100 FPS,
    NoAtlas+FrontToBack = 100 FPS,

    Atlas+Immediate = 105 FPS,
    Atlas+Deferred = ~500 FPS,
    Atlas+Texture = ~300 FPS,
    Atlas+BackToFront = ~350 FPS,
    Atlas+FrontToBack = ~350 FPS,

    It turns out that most of the time is spent drawing text. XNA’s SpriteFont doesn’t shine here.

  6. Oh, and the hack implementation is here if you would like to have a look: http://pastebin.com/UtfGj8P2

  7. Juan Santos says:

    Hi Jim, thanks for the tutorial. I wanted to ask if I can use your code for my XNA game? Thanks.

  8. Mach X Games says:

    Sure, go ahead and use it if it’ll help you.

  9. Juan Santos says:

    @Mach X Games One question, how can I increase the space between lines? If my font is bigger than the one you provide it doesnt display correctly. Thanks.

  10. Mach X Games says:

    You need to make the interior border graphic bigger and change the height parameter for the draw methods.

  11. Juan Santos says:

    @Mach X Games Sorry for bothering you again, but I’m confused with all the draw methods. Is this one ‘sb.Draw(_interior,..’ the one I need to modify?

  12. James says:

    @Mach X Games Sorry to disturb. I want to show the speech bubble when the player is touched with the NPC. The NPC is just standing, no movement. But the game got horizontal scrolling. How can I implement it?

  13. Mach X Games says:

    You would need to have some kind of collision detection between your sprites. The scrolling is irrelevant, somewhat. The usual way would be to call your collision detection code from the Game class’s Update method if the player is moving.

    • James says:

      Can u suggest me how can I do to stable the bubble in a place when I m doing scrolling?

      • Mach X Games says:

        You would simply draw it on the screen at the same location irrelevant of the world location that’s visible.

        • James says:

          in the update method of level class, I made to check when the player is collide with the npc, call the bubble.draw method. But its seems not working.

          • Mach X Games says:

            You shouldn’t be calling the Draw method from the Update method. You’re probably clearing the bubble from the main Draw method. Set a flag in the bubble class to tell whether it should be drawn or not and use this in the main Draw method to tell whether or not to call its Draw method.

  14. James says:

    okay. I set the flag in the bubble class and I used it in the draw method of bubble class. But under where should I reset the flag.

  15. James says:

    Just bad luck. In debugging mode, I saw the draw method is executed, I step over and over, the spriteBatch also executed. But unable to see the drawing on the screen. 🙁 Sorry to bother you.

  16. James says:

    If you don’t mind, can you have a look to my codes?
    If yes, how can I send it to you?

  17. James says:

    I’m afraid it wont be convenient coz its my school project.

  18. James says:

    hi, Before using Game State Management of XNA Platformer, I was able to add videos like this under main method of PlatformerGame.cs
    private Video[] videos;
    private List vList = new List();

    Texture2D vidTexture;
    Rectangle vidRectangle;

    After using Game State Management, I want to put like above under the GamePlayScreen.cs but didn’t work although I already put
    using Microsoft.Xna.Framework.Media;

    How can I do it?

Leave a Reply

You must be logged in to post a comment.