2
\$\begingroup\$

Link to the previous question, before refactoring.


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,
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight
}

#endregion

/// <summary>
/// Represents a pop up dialog
/// </summary>
/// <remarks>
/// To create a custom dialog, derive from PopUpDialog
/// And start it with:
/// PopUpDialogManager.CreateCustomDialog(new CustomPopUpDialog(args));
/// </remarks>
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

    internal 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);
    }

    /// <summary>
    /// Updates the dialog.
    /// </summary>
    /// <param name="gameTime"></param>
    /// <remarks>
    /// If your custom dialog works with a method and a thread
    /// Instead of updating in a loop, override Update and keep it empty,
    /// Or have whatever update logic you want that don't have anything to
    /// Do with the dialog messages flow
    /// </remarks>
    public virtual void Update(GameTime gameTime)
    {
        if (!Keyboard.GetState().IsKeyDown(Keys.Space) || !_canScroll)
            return;
        ShowNextMessage();
        MessageTransition();
    }

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

    #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);
        CreateTitleBorder();
        CreateTextBorder();
        DialogMessages = WrapText(messages, textRectangle.Width, textRectangle.Height);
        ApplyResizeOption(resizeOption);
        ApplyPositionOption(positionOption);
        TextOnDisplay = GetFirstDisplayMessage();
        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)
    {
        Action<Point> offsetRectangles = diff =>
        {
            windowRectangle.Offset(diff);
            textRectangle.Offset(diff);
            titleRectangle.Offset(diff);
        };

        switch (positionOption)
        {
            case PositionOption.CenterOfScreen:
            {
                var diff = PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Center -
                           windowRectangle.Center;
                offsetRectangles(diff);
                break;
            }
            case PositionOption.TopLeft:
            {
                var diff =
                    new Point(PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.X - windowRectangle.X,
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Y - windowRectangle.Y);
                offsetRectangles(diff);
                break;
            }
            case PositionOption.TopRight:
            {
                var diff =
                    new Point(
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Right - windowRectangle.Right,
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Y - windowRectangle.Y);
                offsetRectangles(diff);
                break;
            }
            case PositionOption.BottomLeft:
            {
                var diff =
                    new Point(PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.X - windowRectangle.X,
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Bottom - windowRectangle.Bottom);
                offsetRectangles(diff);
                break;
            }
            case PositionOption.BottomRight:
            {
                var diff =
                    new Point(
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Right - windowRectangle.Right,
                        PopUpDialogManager.Instance.GraphicsDevice.Viewport.Bounds.Bottom - windowRectangle.Bottom);
                offsetRectangles(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 GetFirstDisplayMessage()
    {
        return DialogMessages.Count > 0 ? DialogMessages[0] : "";
    }

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

    /// <summary>
    /// Sets the size of the rectangle that contains the title
    /// </summary>
    private void CreateTitleBorder()
    {
        titleRectangle =
            new Rectangle(
                x: windowRectangle.X + SeperatorSize,
                y: windowRectangle.Y + SeperatorSize,
                width: windowRectangle.Width - 2 * SeperatorSize,
                height: (int)GetSize("test", PopUpDialogManager.Instance.Font).Y);
    }

    /// <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;

                if (windowRectangle.Height < 100)
                {
                    windowRectangle.Height = 100;
                    textRectangle.Height = windowRectangle.Height - titleRectangle.Height - 3 * SeperatorSize;
                        // - (int)dotSize.Y;
                }
                if (windowRectangle.Width < 100)
                {
                    windowRectangle.Width = 100;
                    textRectangle.Width = windowRectangle.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;

        StringBuilder fittingTitle = new StringBuilder();

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

        return fittingTitle.ToString();
    }

    /// <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>();
        StringBuilder currentString = new StringBuilder();
        foreach (var message in messages)
        {
            var messageSize = GetSize(message);
            if (messageSize.X <= width && messageSize.Y <= height)
            {
                messagesList.Add(message);
                continue;
            }

            currentString.Clear();
            var words = message.Split(' ');

            foreach (var word in words)
            {
                // When the word fits in the line
                if (GetSize(currentString + word + " ").X < width)
                {
                    currentString.AppendFormat("{0} ", 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.AppendFormat("\n{0} ", word);
                }
                // When we're in our height limit and we need a nice list 
                else
                {
                    messagesList.Add(currentString.ToString());
                    currentString.Clear();
                    currentString.AppendFormat("{0} ", word);
                }
            }

            messagesList.Add(currentString.ToString());
        }

        return messagesList;
    }

    #endregion

    #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

    #region Dialog Logic Creation Methods

    /// <summary>
    /// Call after ShowNextMessage()
    /// </summary>
    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); }));
    }

    /// <summary>
    /// Gets the size of a text 'word' using spritfont 'font'
    /// </summary>
    /// <param name="word">Word to get size of</param>
    /// <param name="font">Font to get size with</param>
    /// <returns>Vector2 of {Width, Height}</returns>
    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];
    }

    /// <summary>
    /// For custom dialogs
    /// </summary>
    /// <param name="time">Time to wait</param>
    protected static void WaitFor(TimeSpan time)
    {
        SetScrollable(false);
        Thread.Sleep(time);
        SetScrollable(true);
    }

    /// <summary>
    /// Will wait until user inputs the given key
    /// </summary>
    /// <param name="key">Key to wait for</param>
    protected static void WaitForInput(Keys key)
    {
        bool isKeyPressed = Keyboard.GetState().IsKeyDown(key);
        while (!isKeyPressed)
        {
            isKeyPressed = Keyboard.GetState().IsKeyDown(key);
            Thread.Sleep(5);
        }
    }

    #endregion
}

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

internal 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(
              title: "Tutorial Dialog",
              messages: TutorialMessages, 
              windowSize: new Rectangle(0, 0, 300, 200), 
              resizeOption: ResizeOption.ResizeToFit, 
              positionOption: PositionOption.CenterOfScreen)
    {
        Thread tutorial = new Thread(RunTutorial);
        tutorial.Start();
    }

    public override void Update(GameTime gameTime)
    { /* no need for existing update */ }

    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));
        }

        /* continuation messages */
        ShowNextMessage();
        WaitFor(TimeSpan.FromSeconds(3f));
        ShowNextMessage();
        WaitFor(TimeSpan.FromSeconds(3f));
        PopUpDialogManager.Instance.CloseDialog();
    }
}

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\$
2
\$\begingroup\$

So this will be a answer in progress as I get more information, but my first thought is that there isn't an overall architecture that is immediately apparent from the code posted. So I'd first try to map your code to a standard MVVM architecture with a structure like so:

  • Model
    • Instructions
    • Other Game Stuff TBD
  • ViewModel
    • IntroductionViewModel
      • Handles presenting the introductions, and navigation to/from.
      • Instructs the view to create the pop-up.
      • Gets UI instructions/events from the view. Should know what happened, but not how.
  • View
    • Tutorial Screen - Handles all elements of the tutorial, and includes logic to display the pop-up dialog.
    • Pop-Up Dialog
      • Handles the specific rendering of the pop-up dialog.
      • Should probably also interpret the input (likely through a delegate, and view should handle if not the dialog).

So right now, your PopupDialogs don't necessarily need a dedicated manager class. If anything, all of the code related to the presentational aspects of the dialog could be relegated to a view + factory. I could see a Dialog Manager making sense if you had multiple concurrent pop-ups that needed to be managed in context of each other.

\$\endgroup\$

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.