4
\$\begingroup\$

I've decided to make this module of a dialog, sort of like talking to NPCs in games, like this for example.

I've creating the visuals of the window using the code from this post, works pretty well.

Here's an example:

example dialog

I've created a Manager class to manage those pop-ups, and the obvious PopUpDialog class.

Here's the Manager class:

public class PopUpDialogManager
{
    #region Singleton 

    /// <summary>
    /// .Net Lazy object for singleton
    /// </summary>
    private static readonly Lazy<PopUpDialogManager> Lazy =
        new Lazy<PopUpDialogManager>(() => new PopUpDialogManager());

    /// <summary>
    /// Returns the singleton instance of the ScreenManager
    /// </summary>
    public static PopUpDialogManager Instance
    {
        get { return Lazy.Value; }
    }

    /// <summary>
    /// Constructs the PopUpDialogManager
    /// </summary>
    private PopUpDialogManager()
    {

    }

    #endregion

    public GraphicsDevice GraphicsDevice { get; private set; }
    public SpriteFont Font { get; private set; }
    public SpriteFont TitleFont { get; private set; }
    private PopUpDialog CurrentDialog { get; set; }

    public Texture2D GetTexture(int width, int height)
    {
        return RectangleGenerator.CreateRoundedRectangleTexture(
            graphics: GraphicsDevice,
            width: width,
            height: height,
            borderThickness: 2,
            borderRadius: 4,
            borderShadow: 2,
            backgroundColors: new List<Color> { Color.AntiqueWhite },
            borderColors: new List<Color> { Color.Azure },
            initialShadowIntensity: 0.4f,
            finalShadowIntensity: 0.2f);
    }

    public void CloseDialog()
    {
        CurrentDialog = null;
    }

    public void LoadContent(ContentManager content, GraphicsDevice graphicsDevice)
    {
        GraphicsDevice = graphicsDevice;
        Font = content.Load<SpriteFont>("Fonts/Font");
        TitleFont = content.Load<SpriteFont>("Fonts/TitleFont");
    }

    public void CreateDialog(string title,
                             IEnumerable<string> messages,
                             ResizeOption resizeOption = ResizeOption.KeepMaxSize,
                             PositionOption positionOption = PositionOption.Custom,
                             Point? size = null,
                             Point? position = null)
    {
        position = position ?? Point.Zero;
        size = size ?? Point.Zero;
        CurrentDialog = new PopUpDialog(title, messages, new Rectangle(position.Value, size.Value), resizeOption, positionOption);
    }

    public void CreateCustomDialog(PopUpDialog dialog)
    {
        CurrentDialog = dialog;
    }

    public void Update(GameTime gameTime)
    {
        CurrentDialog?.Update(gameTime);
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        CurrentDialog?.Draw(spriteBatch);
    }
}

And here's the PopUpDialog class:

#region Enums

public enum ResizeOption
{
    ResizeToFit,
    KeepMaxSize
}

public enum PositionOption
{
    CenterOfScreen,
    Custom
}

#endregion

public class PopUpDialog
{
    private readonly Texture2D windowImage;

    #region Display Text Related Members

    public List<string> DialogMessages { get; private set; }
    public string Title { get; private set; }
    protected string TextOnDisplay { get; set; }
    private int currentMessage;

    #endregion

    #region Rectangles for display and size restrictions

    private Rectangle windowRectangle;
    private Rectangle textRectangle;
    private Rectangle titleRectangle;

    #endregion

    #region State Related Members

    protected bool ShouldContinueScrolling => currentMessage + 1 < DialogMessages.Count;
    private static bool _canScroll = true;
    private static bool _drawDots = true;

    #endregion

    #region Constants

    private const string ContinuationDots = "...";
    private const int SeperatorSize = 5;

    #endregion

    #region State Related Setters

    protected static void SetScrollable(bool set)
    {
        _canScroll = set;
    }

    private static void SetDrawDots(bool set)
    {
        _drawDots = set;
    }

    #endregion

    public PopUpDialog(string title, IEnumerable<string> messages, Rectangle windowSize,
                       ResizeOption resizeOption, PositionOption positionOption)
    {
        InitializePopUpDialog(title, messages, windowSize, resizeOption, positionOption);
        windowImage = PopUpDialogManager.Instance.GetTexture(windowRectangle.Width, windowRectangle.Height);
    }

    #region Initialization Functions

    /// <summary>
    /// Calls all initialization methods
    /// </summary>
    /// <param name="title">Title of the dialog</param>
    /// <param name="messages">Messages to be shown in the dialog</param>
    /// <param name="windowSize">Size of the window of the dialog</param>
    /// <param name="resizeOption">Option to resize the window</param>
    /// <param name="positionOption">Option to position the window</param>
    private void InitializePopUpDialog(string title, IEnumerable<string> messages, Rectangle windowSize,
                                       ResizeOption resizeOption, PositionOption positionOption)
    {
        ApplyMinimumWindowSize(windowSize);
        SetTitleBorderSize();
        SetTextBorderSize();
        DialogMessages = WrapText(messages, textRectangle.Width, textRectangle.Height);
        ApplyResizeOption(resizeOption);
        ApplyPositionOption(positionOption);
        TextOnDisplay = SetFirstDisplayMessage();
        Title = GetFittingTitle(title, titleRectangle.Width);
    }

    /// <summary>
    /// Sets the window location according to the given PositionOption
    /// </summary>
    /// <param name="positionOption">PositionOption of window position</param>
    private void ApplyPositionOption(PositionOption positionOption)
    {
        switch (positionOption)
        {
            case PositionOption.CenterOfScreen:
            {
                var diff = PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Center - textRectangle.Center;
                windowRectangle.Offset(diff);
                textRectangle.Offset(diff);
                titleRectangle.Offset(diff);
                break;
            }
        }
    }

    /// <summary>
    /// Returns the first message of display for TextOnDisplay
    /// Handling the situation where there are no messages given
    /// </summary>
    /// <returns>First message to be shown in the dialog</returns>
    private string SetFirstDisplayMessage()
    {
        return DialogMessages.Count > 0 ? DialogMessages[0] : "";
    }

    /// <summary>
    /// Sets the size of the rectangle that contains the text
    /// </summary>
    private void SetTextBorderSize()
    {
        textRectangle =
            new Rectangle(windowRectangle.X + SeperatorSize, titleRectangle.Bottom + SeperatorSize,
                windowRectangle.Width - 10, windowRectangle.Height - 40);
    }

    /// <summary>
    /// Sets the size of the rectangle that contains the title
    /// </summary>
    private void SetTitleBorderSize()
    {
        titleRectangle =
            new Rectangle(windowRectangle.X + SeperatorSize, windowRectangle.Y + SeperatorSize,
                windowRectangle.Width - 10, 25);
    }

    /// <summary>
    /// Handles the situation where the window size given is too small
    /// </summary>
    /// <param name="windowSize">The initial window size from input</param>
    private void ApplyMinimumWindowSize(Rectangle windowSize)
    {
        windowRectangle = windowSize;
        windowRectangle.Height = windowRectangle.Height < 100 ? 100 : windowRectangle.Height;
        windowRectangle.Width = windowRectangle.Width < 100 ? 100 : windowRectangle.Width;
    }

    /// <summary>
    /// Resizes the window according to the given ResizeOption
    /// </summary>
    /// <param name="resizeOption">ResizeOption of window size</param>
    private void ApplyResizeOption(ResizeOption resizeOption)
    {
        switch (resizeOption)
        {
            case ResizeOption.ResizeToFit:
            {
                // Get size of continuation dots
                var dotSize = GetSize(ContinuationDots);

                // Get size of the biggest message
                var maxSize =
                    new Vector2(DialogMessages.Max(msg => PopUpDialogManager.Instance.Font.MeasureString(msg).X),
                        DialogMessages.Max(msg => PopUpDialogManager.Instance.Font.MeasureString(msg).Y));

                // Set text rectangle size to fit the message, the dots in the bottom, and seperation spaces 
                textRectangle.Width = (int)maxSize.X + 2 * SeperatorSize;
                textRectangle.Height = (int)maxSize.Y + (int)dotSize.Y + 2 * SeperatorSize;

                // Set window rectangle to fit the title and the text rectangle
                windowRectangle.Height = titleRectangle.Height + textRectangle.Height + 3 * SeperatorSize;
                windowRectangle.Width = textRectangle.Width + 2 * SeperatorSize;

                break;
            }
        }
    }

    /// <summary>
    /// Limits the title given the it's possible width
    /// </summary>
    /// <param name="fullTitle">the full title</param>
    /// <param name="width">the width of the window</param>
    /// <returns>The part of the full title that is within the given width</returns>
    private static string GetFittingTitle(string fullTitle, int width)
    {
        if (GetSize(fullTitle, PopUpDialogManager.Instance.TitleFont).X <= width)
            return fullTitle;

        string fittingTitle = "";

        foreach (var word in fullTitle.Split(' '))
        {
            if (GetSize(word + " ", PopUpDialogManager.Instance.TitleFont).X +
                GetSize(fittingTitle, PopUpDialogManager.Instance.TitleFont).X <
                width)
            {
                fittingTitle += word + " ";
            }
            else
                break;
        }

        return fittingTitle;
    }

    /// <summary>
    /// Converts the messages given to a list of messages that are within the size of the text rectangle
    /// </summary>
    /// <param name="messages">All messages given</param>
    /// <param name="width">Width restriction in px</param>
    /// <param name="height">Height restriction in px</param>
    /// <returns>A list of messages that, if needed, have been split to several more to fit the text rectangle size</returns>
    private static List<string> WrapText(IEnumerable<string> messages, int width, int height)
    {
        var messagesList = new List<string>();
        foreach (var message in messages)
        {
            string currentString = "";
            var words = message.Split(' ');

            foreach (var word in words)
            {
                // When the word fits in the line
                if (GetSize(currentString + word + " ").X < width)
                {
                    currentString += word + " ";
                }
                // When the word doesn't fit in the line
                // And we're not in our height limit
                else if (GetSize(currentString + "\n" + word).Y < height)
                {
                    currentString += "\n" + word + " ";
                }
                // When we're in our height limit and we need a nice list 
                else
                {
                    messagesList.Add(currentString);
                    currentString = word + " ";
                }
            }
            messagesList.Add(currentString);
        }

        return messagesList;
    }

    #endregion

    public virtual void Update(GameTime gameTime)
    {
        if (!Keyboard.GetState().IsKeyDown(Keys.Space) || !_canScroll)
            return;
        ShowNextMessage();
        MessageTransition();
    }

    protected void MessageTransition()
    {
        SetScrollable(false);
        SetDrawDots(false);
        Task.Delay(TimeSpan.FromSeconds(0.25f)).ContinueWith(x =>
        {
            if (ShouldContinueScrolling)
                SetDrawDots(true);
        })
            .ContinueWith(
                _ => Task.Delay(TimeSpan.FromSeconds(0.75f)).ContinueWith(x => { SetScrollable(true); }));
    }

    public void Draw(SpriteBatch spriteBatch)
    {
        DrawDialogBox(spriteBatch);
        spriteBatch.Draw(windowImage, textRectangle, Color.White);
        DrawTitle(spriteBatch);
        DrawText(spriteBatch);
        DrawContinuationDots(spriteBatch, PopUpDialogManager.Instance.TitleFont);
    }

    #region Drawing Functions

    private void DrawContinuationDots(SpriteBatch spriteBatch, SpriteFont font = null)
    {
        if (!ShouldContinueScrolling || !_drawDots)
            return;
        Vector2 size = GetSize(ContinuationDots, font);
        Vector2 position = new Vector2(textRectangle.Right - size.X - SeperatorSize,
            textRectangle.Bottom - size.Y);
        spriteBatch.DrawString(font, ContinuationDots, position, Color.Black);
    }

    private void DrawText(SpriteBatch spriteBatch)
    {
        spriteBatch.DrawString(PopUpDialogManager.Instance.Font, TextOnDisplay,
            new Vector2(textRectangle.X + SeperatorSize, textRectangle.Y + SeperatorSize),
            Color.Black);
    }

    private void DrawTitle(SpriteBatch spriteBatch)
    {
        spriteBatch.DrawString(PopUpDialogManager.Instance.TitleFont, Title,
            new Vector2(titleRectangle.X,
                titleRectangle.Y),
            Color.Black);
    }

    private void DrawDialogBox(SpriteBatch spriteBatch)
    {
        spriteBatch.Draw(windowImage, windowRectangle, Color.White * 0.8f);
    }

    #endregion

    protected static Vector2 GetSize(string word, SpriteFont font = null)
    {
        font = font ?? PopUpDialogManager.Instance.Font;
        return font.MeasureString(word);
    }

    protected void ShowNextMessage()
    {
        if (ShouldContinueScrolling)
            currentMessage++;
        TextOnDisplay = DialogMessages[currentMessage];
    }

And here's a custom dialog I created, by deriving from PopUpDialog:

class TutorialPopUpDialog : PopUpDialog
{
    public static IEnumerable<string> TutorialMessages
    {
        get
        {
            const string FORMAT = "To move {0}, press '{1}'";
            yield return "Welcome to this tutorial dialog";
            yield return "Follow these steps and you'll learn how to operate the game!";
            yield return string.Format(FORMAT, "forward", "W");
            yield return "Good job!";
            yield return string.Format(FORMAT, "backward", "S");
            yield return "Good job!";
            yield return string.Format(FORMAT, "to the right", "D");
            yield return "Good job!";
            yield return string.Format(FORMAT, "to the left", "A");
            yield return "Good job!";
            yield return "Well done player!";
            yield return "You are now ready to start your adventure!";
        }
    } 

    public TutorialPopUpDialog()
        : base("Tutorial Dialog", TutorialMessages, new Rectangle(0, 0, 300, 200), ResizeOption.ResizeToFit, PositionOption.CenterOfScreen)
    {
        Thread tutorial = new Thread(RunTutorial);
        tutorial.Start();
    }

    public override void Update(GameTime gameTime)
    {

    }

    private void RunTutorial()
    {
        /* welcome */
        WaitFor(TimeSpan.FromSeconds(1.5f));
        WaitForInput(Keys.Space);

        /* follow steps */
        ShowNextMessage();
        MessageTransition();
        WaitFor(TimeSpan.FromSeconds(1.5f));
        WaitForInput(Keys.Space);
        foreach (Keys key in new []{ Keys.W, Keys.S, Keys.D, Keys.A })
        {
            /* to move to X, press 'key' */
            ShowNextMessage();
            MessageTransition();
            WaitForInput(key);

            /* good job */
            ShowNextMessage();
            MessageTransition();
            WaitFor(TimeSpan.FromSeconds(1.5));
        }
        ShowNextMessage();
        WaitFor(TimeSpan.FromSeconds(3f));
        ShowNextMessage();
        WaitFor(TimeSpan.FromSeconds(3f));
        PopUpDialogManager.Instance.CloseDialog();
    }

    private static void WaitFor(TimeSpan time)
    {
        SetScrollable(false);
        Thread.Sleep(time);
        SetScrollable(true);
    }

    private static void WaitForInput(Keys key)
    {
        bool isKeyPressed = Keyboard.GetState().IsKeyDown(key);
        while (!isKeyPressed)
        {
            isKeyPressed = Keyboard.GetState().IsKeyDown(key);
            Thread.Sleep(5);
        }
    }
}

I know that I use regions and that people don't like them. I know they're considered an anti-pattern, but I love them, and they're perfectly acceptable and welcomed where I work, so I'm pretty okay with using them.

I would like some advice on how I can make the manager class be more "managing", because, as it is now, I can't do really much with it as a manager, only draw and update when needed.

In the Game class, this is how I create the dialog:

/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
    PopUpDialogManager.Instance.LoadContent(Content, GraphicsDevice);

    /*PopUpDialogManager.Instance.CreateDialog("Dolem Kaki",
        new[]
        {
            "Hello player",
            "Welcome to this test environment",
            "I'm Dolem, and I'll be your tutorial.",
            "To move Forward, Press 'W'",
            "To move Backward, Press 'S'",
            "To move to the Right, Press 'D'",
            "To move to the Left, Press 'A'",
            "This is a very long test and i'll be here with you duck petters"
        },
        ResizeOption.ResizeToFit,
        PositionOption.CenterOfScreen,
        new Point(300, 200));*/

   PopUpDialogManager.Instance.CreateCustomDialog(new TutorialPopUpDialog());
}
\$\endgroup\$
3
\$\begingroup\$

PopUpDialog

  • The name ShouldContinueScrolling doesn't read right IMO and should be changed to ShouldScrollingContinue.
  • ApplyPositionOption() is a little bit over the top. You only have two members for PositionEnum so a simple if with a future else will be enough and reduces the horizontal spacing which makes the code more readable.

  • string SetFirstDisplayMessage() why does a method prefixed with Set returns a value ? This should be named GetFirstDisplayMessage() instead.

  • SetTextBorderSize() doesn't do what the name implies. Either you create the textRectangle somewhere else and use the titleRectangle to adjust the size of it or you should change the name. The same is true for SetTitleBorderSize().

    Clarification based on the comment

    The SetXBorderSize methods tho, I don't really get what's the problem with them.

    If I read SetTextBorderSize or SetTitleBorderSize I expect the method doing just setting a size. In these methods the Rectangles aren't adjusted by setting Width, Height etc but by using the constructor of the struct. If you name that method CreateTextBorder and let it return the created Rectangle it would be more obvious.

  • ApplyResizeOption() has the same "problems" like ApplyPositionOption()

  • GetFittingTitle() seeing string concatination inside a loop is IMO a red sign. If I see something like this the first what comes to my mind is use a StringBuilder.

General

  • don't omit braces {} although they might be optional. This helps to make your code less error prone.

  • if you have choosen a coding style ( like omiting/having braces ) you should stick to that style. Right now you are mixing styles, for instance sometimes you use braces sometimes you don't.

  • adding vertical space (new lines) to group related code will lead to better readability and is therfor better to maintain.

\$\endgroup\$
  • \$\begingroup\$ Hey dude, thanks for the input. I can't work with my project right now but I see how I can use what you wrote. For the ApplyXOption, i decided to do a switch because I know there will be more options, so it's sorta premature optimization. With GetFittingTitle, point taken. SetFirstDisplayMessage, got it. The SetXBorderSize methods tho, I don't really get what's the problem with them. Can you explain a little more? About newlines for readability, it's something that I usually do, and if i don't, i do it in the refactoring phase. Thanks for the input again \$\endgroup\$ – Giora Guttsait Oct 27 '15 at 7:47
  • \$\begingroup\$ I added an update. added some more PositionOption enum values, and updated the ApplyPositionOption. About the omitted {}s, I only omit them when the if(or whatever control statement before) statement is clear enough, and the oneline command afterwards is also clear. f.e: if (!ready) (\n)return;. Also, I refactored some stuff in WrapText, check it out \$\endgroup\$ – Giora Guttsait Oct 27 '15 at 20:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.