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());
}
1 Answer 1
PopUpDialog
- The name
ShouldContinueScrolling
doesn't read right IMO and should be changed toShouldScrollingContinue
. ApplyPositionOption()
is a little bit over the top. You only have two members forPositionEnum
so a simpleif
with a futureelse
will be enough and reduces the horizontal spacing which makes the code more readable.string SetFirstDisplayMessage()
why does a method prefixed withSet
returns a value ? This should be namedGetFirstDisplayMessage()
instead.SetTextBorderSize()
doesn't do what the name implies. Either you create thetextRectangle
somewhere else and use thetitleRectangle
to adjust the size of it or you should change the name. The same is true forSetTitleBorderSize()
.Clarification based on the comment
The SetXBorderSize methods tho, I don't really get what's the problem with them.
If I read
SetTextBorderSize
orSetTitleBorderSize
I expect the method doing just setting a size. In these methods theRectangles
aren't adjusted by settingWidth
,Height
etc but by using the constructor of the struct. If you name that methodCreateTextBorder
and let it return the createdRectangle
it would be more obvious.ApplyResizeOption()
has the same "problems" likeApplyPositionOption()
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 aStringBuilder
.
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.
-
\$\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– Giora Guttsait2015年10月27日 07:47:07 +00:00Commented Oct 27, 2015 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– Giora Guttsait2015年10月27日 20:28:57 +00:00Commented Oct 27, 2015 at 20:28