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