Jump to content
  • Advertisement

Project: Unsettled World

Stateless Frustrations Frustratingly Stated (Handling Improperly Disconnected UDP Clients?)

Septopus

715 views

So, after weeks of strange and seemingly intermittent/coincidental UDP server crashes(Receive Thread Halt/Un-catchable Exception) and the ensuing frustration that that causes when you aren't even trying to work on the server itself.  I sat down and spent a day trying to figure out the problem..  After an almost complete server rewrite from scratch with no real improvement in the problem.. I turned to random poking about on the internets..

Hmmm...  It seems like the problem only happens when I'm restarting the client..

Here's a post!!

https://stackoverflow.com/questions/38191968/c-sharp-udp-an-existing-connection-was-forcibly-closed-by-the-remote-host

And Here!

https://social.msdn.microsoft.com/Forums/en-US/6b7aa353-259b-4637-b0ae-d4e550c13e38/c-udp-socket-exception-on-bad-disconnect?forum=netfxnetcom

Yup, that's the problem for sure.

It appears that even though UDP isn't supposed to care about the other ends state, apparently this isn't true if the state is Port Unreachable.  When the server attempts to receive data (BeginReceiveFrom/BeginReceive/EndRecieveFrom/EndReceive/etc..) from an async connection and the client has (Crashed/Quit/Etc.) an ICMP port unavailable packet is generated and this causes the UDP socket to generate an exception that (even if you try/catch the hell out of it) causes the async receive thread to halt(I assume, this is the effect anyhow).

The best solution I've found, as stated in the link(s) above is to change the underlying IOControl behavior(Prevent the Exception from being thrown).

//This value tells the underlying IO system to IGNORE the ICMP error.
public const int SIO_UDP_CONNRESET = -1744830452;
private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

_socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null);
_socket.Bind(new IPEndPoint(IPAddress.Any, port));

The first line is the value that needs to be set, and the second to last line is how you can set it.  And these are the live bits of code that are WORKING without error in my server, Finally! 

So, there, I've posted it again because it seems way more obscure than it should be.

Enjoy. ;)




0 Comments


Recommended Comments

I guess it's worth adding that my server handles client timeouts as part of it's core behavior, simple timeouts for inactivity/etc, and doesn't use any of the UDP connections "state" info for the decision process..  So the only potential negative side-effect of not properly trapping this exception and removing the user's EndPoint immediately is that a few extra packet sends get attempted to the dead client before the player's EndPoint gets forgotten anyhow.  So far, it's not an issue, hopefully it stays that way. ;)

Share this comment


Link to comment

Additionally I may be experimenting with rewriting my server so that it doesn't use the VERY same thread for every single receive operation, doing this probably would have prevented my server from having issues in the first place as only the thread serving the dead client would be halting.  And the extra exception was already going unnoticed anyways. lol... 

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • Blog Entries

  • Similar Content

    • By Brizzler
      Hey Everyone, 
      I'd like to review some of your indies games! I'm interested in games made for the Universal Windows Platform (UWP).
      Reply to this post if your interested in having your games reviewed and youtubed on my channel:
      UWPdeveloper
       
      Cheers,
       
      B
    • By JustHusk
      Hi there,
      So i'm an inexperienced student learning games dev at college and im studying c#, in my spare time im trying to work on a text adventure game with a narrative. My issue is that i dont know how to make the decisions, well not simply anyhow.
      I could write out 50 variables for each decision to read from the player but that would become very tedious and too messy to even comprehend when im adding content. I was thinking of adding functions to hold each decision or area but im at a lack of knowledge on how to jump to a function or if it's even a good solution. I know i could use an array and each decision be a number but that just doesn't seem like a good solution to me either.
      Any advice would be appreciated and this whole topic might sound dumb to a professional so be understanding please haha.
      Thanks,
      Luke
    • By Septopus
      Okay, looking for some constructive feedback/criticism here... 
      What follows is the code of my c# UDP Socket Class.  I've written this based on a number of online examples and a few distant memories from the past, so I really have no idea how "bad" the code is/could be in a 100+ concurrent connection scenario(my dev environment is just two machines at the moment)...  It works, I know that, it is fairly stable(I can't break it when using it normally), and it behaves how I believe I want it to. 
      It is a dual purpose class for handling both Servers and Clients.  It uses an asynchronous receive and synchronous send(I may switch to async send if it proves beneficial later on).  My servers use Client sockets to communicate with each other and to perform internal connection tests in a multiple server, "single shard" type environment.  I'll be devising some heavy load testing methods a little further down the line, but I'm hoping to fish out most of the major gotchas before I get there.
      So, please, let me know how to fix it up if it smells terribly to you, or if not, that's even better...
      Sorry for the large code dump, but THANKS for checking it out!
      using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using UWAvatarServerData; namespace UWAvatarServer { public class UDPSocket : IDisposable { //Some simple string length counters and averages to get a rough idea of generated traffic. public int sndMax = 0; public MovingAverage sndAvg = new MovingAverage(); public int csndMax = 0; public MovingAverage csndAvg = new MovingAverage(); public int rcvMax = 0; public MovingAverage rcvAvg = new MovingAverage(); //Constant for configuring the prevention of ICMP connection resets private const int SIO_UDP_CONNRESET = -1744830452; //UDP socket private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); //Buffer Size Constant private const int bufSize = 8 * 1024; //Raw string data from client packets private Dictionary<EndPoint, Queue<string>> messageDictionary; //Queue for holding raw string data from server packets when in client mode. private Queue<string> cQ; //Referece to the data store used by the server(for access to the current game time clock) private UWDataStore dataStore; //Time code storage for last sent/received messages private double lastSentMessage = 0; private double lastReceivedMessage = 0; //Boolean to determine which mode we're in so received messages get put in the right place. private bool clientMode = false; //Max string lenght allowed by the servers. private int maxMessageLength = 1450; //IDisposable stuff public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { // free managed resources if (_socket != null) { _socket.Dispose(); _socket = null; } } } //State class for async receive. public class State { public byte[] buffer = new byte[bufSize]; public EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0); } //Server "Mode" public UDPSocket(Dictionary<EndPoint, Queue<string>> msgsDict, UWDataStore DATASTORE) { clientMode = false; messageDictionary = msgsDict; dataStore = DATASTORE; lastSentMessage = dataStore.UWServerSeconds; } //Client "Mode" public UDPSocket(Queue<string> mq, UWDataStore DATASTORE) { clientMode = true; cQ = mq; dataStore = DATASTORE; lastSentMessage = dataStore.UWServerSeconds; } public void CloseSocket() { _socket.Close(); } //Internal connection status checking public int SendHowStale() { if (lastSentMessage == 0) lastSentMessage = dataStore.UWServerSeconds; return (int)(dataStore.UWServerSeconds - lastSentMessage); } //Internal connection status checking public int ReceiveHowStale() { if (lastReceivedMessage == 0) lastReceivedMessage = dataStore.UWServerSeconds; return (int)(dataStore.UWServerSeconds - lastReceivedMessage); } //Start/Bind a Server. public void Server(string address, int port) { //In case restarting uncleanly, dunno if this actually does anything.. _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true); //Ensure all async packets contain endpoint info and etc. _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); //Ignore ICMP port unreachable exceptions. _socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null); //Bind to port. if (address == "all") { _socket.Bind(new IPEndPoint(IPAddress.Any, port)); } else { _socket.Bind(new IPEndPoint(IPAddress.Parse(address), port)); } //Start receive callback process. Receive(); } //Setup a Client to Server socket. public void Client(string address, int port) { //Dunno if these two options do anything for client sockets, but they don't seem to break anything. _socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); _socket.IOControl((IOControlCode)SIO_UDP_CONNRESET, new byte[] { 0, 0, 0, 0 }, null); _socket.Connect(IPAddress.Parse(address), port); //Start receive callback. Receive(); } //ServerSend sends to any EndPoint from THIS server. public void ServerSend(string text, EndPoint ep) { try { byte[] data = Encoding.ASCII.GetBytes(text); _socket.SendTo(data, ep); lastSentMessage = dataStore.UWServerSeconds; if (text.Length > sndMax) sndMax = text.Length; sndAvg.ComputeAverage((float)text.Length); //Console.WriteLine("TO NET: " + text); } catch (Exception ex) { Console.WriteLine("ServerSend Exception: " + ex.Message); } } //Client Send only sends to the connected Server. public void cSend(string text) { try { byte[] data = Encoding.ASCII.GetBytes(text); _socket.Send(data); lastSentMessage = dataStore.UWServerSeconds; if (text.Length > sndMax) sndMax = text.Length; sndAvg.ComputeAverage((float)text.Length); //Console.WriteLine("TO NET: " + text); } catch (Exception ex) { Console.WriteLine("cSend Exception: " + ex.Message); } } //Setup Async Callback private void Receive() { try { State so = new State(); _socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref so.epFrom, new AsyncCallback(_Receive), so); } catch (Exception) { } } //Receive Callback private void _Receive(IAsyncResult ar) { try { State so = (State)ar.AsyncState; int bytes = _socket.EndReceiveFrom(ar, ref so.epFrom); lastReceivedMessage = dataStore.UWServerSeconds; string smessage = Encoding.ASCII.GetString(so.buffer, 0, bytes); //Console.WriteLine("FROM NET: " + text); if (smessage.Length < maxMessageLength) { if (smessage.Length > rcvMax) rcvMax = smessage.Length; rcvAvg.ComputeAverage((float)smessage.Length); if (clientMode) { cQ.Enqueue(smessage); } else { if (!messageDictionary.ContainsKey(so.epFrom)) { messageDictionary.Add(so.epFrom, new Queue<string> { }); } messageDictionary[so.epFrom].Enqueue(smessage); } } _socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref so.epFrom, new AsyncCallback(_Receive), so); } catch (Exception) { } } } } Generally speaking I use it as such:
      public static bool running = true; static void UDPServer() { using (s = new UDPSocket(MessageDictionary, UWDataStore)) { s.Server("all", 37373); while(running) { //Servery Stuff Goes Here. //Like reiteratively dequeuing the Message Dictionary Queues and processing/replying to all commands/etc... } } } All processing of individual messages from clients is handled with Task.Factory tasks, and a reference to the server's socket varible (s), and the client's EndPoint is sent along the ride for use in replying to clients.  There's no game logic and there are no client replies that happen directly from within the UDP server's main while loop, mostly just connection status checking and reorganizing of the Message Queues into Tasks.
      Thanks again for making it this far.
    • By MatsK
      I'm creating a textbox for my UI, and I found a way to place the cursor where the user clicks on the text.
      Here is all of my code:
      using System; using System.Collections.Generic; using System.Timers; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace TextEditorTest { public class Cursor { public int CharacterIndex = 0; public string Symbol = "|"; public Vector2 Position = new Vector2(0, 0); public int LineIndex = 0; public bool Visible = true; } /// <summary> /// This is the main type for your game. /// </summary> public class Game1 : Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; private GapBuffer<string> m_Text = new GapBuffer<string>(); //private List<Vector2> m_CharacterPositions = new List<Vector2>(); private Dictionary<int, Vector2> m_CharacterPositions = new Dictionary<int, Vector2>(); private List<Rectangle> m_HitBoxes = new List<Rectangle>(); private bool m_UpdateCharPositions = true; private const int NUM_CHARS_IN_LINE = 10; private Vector2 m_TextEditPosition = new Vector2(50, 0), m_TextEditSize = new Vector2(250, 320); private SpriteFont m_Font; private Timer m_CursorVisibilityTimer = new Timer(); private Cursor m_Cursor = new Cursor(); private InputHelper m_Input = new InputHelper(); private int m_NumLinesInText = 1; private bool m_HasFocus = true; private bool m_MultiLine = true; private bool m_CapitalLetters = false; //For how long has a key been presseed? private DateTime m_DownSince = DateTime.Now; private float m_TimeUntilRepInMillis = 100f; private int m_RepsPerSec = 15; private DateTime m_LastRep = DateTime.Now; private Keys? m_RepChar; //A character currently being pressed (repeated). private Vector2 m_TextPosition = new Vector2(0, 0); //Coordinate for anchoring the text. public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; m_Cursor.Position = m_TextEditPosition; m_CursorVisibilityTimer = new Timer(100); m_CursorVisibilityTimer.Enabled = true; m_CursorVisibilityTimer.Elapsed += CursorVisibilityTimer_Elapsed; m_CursorVisibilityTimer.Start(); IsMouseVisible = true; Window.TextInput += Window_TextInput; m_TextPosition = m_TextEditPosition; } private void CursorVisibilityTimer_Elapsed(object sender, ElapsedEventArgs e) { if (m_HasFocus) { if (m_Cursor.Visible) m_Cursor.Visible = false; else m_Cursor.Visible = true; } } /// <summary> /// The text in this TextEditor instance, containing "\n". /// </summary> private string TextWithLBreaks { get { string Text = ""; foreach (string Str in m_Text) Text += Str; return Text; } } /// <summary> /// Returns a line of text. /// </summary> /// <param name="LineIndex">The index of the line of text to get.</param> /// <returns>The line of text, as indicated by the line index.</returns> private string GetLine(int LineIndex) { try { if (TextWithLBreaks.Contains("\n")) return TextWithLBreaks.Split("\n".ToCharArray())[LineIndex]; } catch(Exception) { return ""; } return TextWithLBreaks; } /// <summary> /// Make sure cursor's index is valid and in range. /// </summary> private void FixCursorIndex() { if (m_Cursor.CharacterIndex < 0) m_Cursor.CharacterIndex = 0; if (m_Cursor.CharacterIndex == m_Text.Count) m_Cursor.CharacterIndex = m_Text.Count - 1; } /// <summary> /// Make sure cursor's position is valid and in range. /// </summary> private void FixCursorPosition() { if (m_Cursor.Position.X < m_TextEditPosition.X) m_Cursor.Position.X = m_TextEditPosition.X; m_UpdateCharPositions = true; UpdateCharacterPositions(); UpdateHitboxes(); //Find the curor's real character index. int RealCharIndex = m_Cursor.CharacterIndex; RealCharIndex = (RealCharIndex < m_CharacterPositions.Count) ? //Make sure it doesn't overflow. RealCharIndex : m_CharacterPositions.Count - 1; if (RealCharIndex < 0) RealCharIndex = 0; //Make sure it doesn't underflow. //Adjust the character's position based on the real character index. if (m_Text.Count > 0) { Vector2 IndexPosition = m_CharacterPositions[(RealCharIndex > 0) ? RealCharIndex : 0]; if (m_Cursor.Position.X < IndexPosition.X) m_Cursor.Position.X = IndexPosition.X; if (m_Cursor.Position.Y != IndexPosition.Y) m_Cursor.Position.Y = IndexPosition.Y; } } /// <summary> /// The text in this TextEditor instance, without \n /// (except for those explicitly added by pressing backspace). /// </summary> public string Text { get { string Text = ""; foreach (string Str in m_Text) Text += Str; return Text.Replace("\n", ""); } } /// <summary> /// Returns the width of a character in this font. /// </summary> /// <returns>The width of the character in floating point numbers.</returns> private float CharacterWidth { get { return m_Font.MeasureString("a").X; } } /// <summary> /// Returns the width of a capitalized character in this font. /// </summary> /// <returns>The width of the capitalized character in floating point numbers.</returns> private float CapitalCharacterWidth { get { return m_Font.MeasureString("A").X; } } /// <summary> /// Returns the height of a character in this font. /// </summary> /// <returns>The height of the character in floating point numbers.</returns> private float CharacterHeight { get { return m_Font.MeasureString("a").Y; } } /// <summary> /// Returns the height of a capitalized character in this font. /// </summary> /// <returns>The height of the capitalized character in floating point numbers.</returns> private float CapitalCharacterHeight { get { return m_Font.MeasureString("A").Y; } } /// <summary> /// Returns the last line of text in the gap buffer. /// </summary> /// <returns></returns> private string CurrentLine { get { if (m_Text.Count > 1) { if (TextWithLBreaks.Contains("\n")) { string[] Lines = TextWithLBreaks.Split("\n".ToCharArray()); return Lines[Lines.Length - 1]; } else return TextWithLBreaks; } if (m_Text.Count > 0) return m_Text[0]; else return ""; } } /// <summary> /// The control received text input. /// </summary> private void Window_TextInput(object sender, TextInputEventArgs e) { if (e.Character != (char)Keys.Back) { int Index = TextWithLBreaks.LastIndexOf("\n", m_Cursor.CharacterIndex); if (Index == -1) //No occurence was found!! { if (Text.Length <= NUM_CHARS_IN_LINE) { AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() : e.Character.ToString()); m_CapitalLetters = false; m_UpdateCharPositions = true; return; } else { if (m_MultiLine) { AddNewline(); return; } } } if ((m_Cursor.CharacterIndex - Index) <= NUM_CHARS_IN_LINE) { //If the cursor has moved away from the end of the text... if (m_Cursor.CharacterIndex < (m_Text.Count - (1 + m_NumLinesInText))) { //... insert it at the cursor's position. m_Text.Insert(m_Cursor.CharacterIndex, (m_CapitalLetters == true) ? e.Character.ToString().ToUpper() : e.Character.ToString()); m_CapitalLetters = false; m_UpdateCharPositions = true; } else { AddText((m_CapitalLetters == true) ? e.Character.ToString().ToUpper() : e.Character.ToString()); //... just add the text as usual. m_CapitalLetters = false; m_UpdateCharPositions = true; } } else { if(m_MultiLine) AddNewline(); } } } /// <summary> /// Adds a string to m_Text, and updates the cursor. /// </summary> /// <param name="Text">The string to add.</param> private void AddText(string Text) { m_Text.Add(Text); m_Cursor.CharacterIndex++; m_Cursor.Position.X += CharacterWidth; } //Can the cursor move further down or has it reached the end of the textbox? private bool m_CanMoveCursorDown = true; /// <summary> /// Adds a newline to m_Text, and updates the cursor. /// </summary> private void AddNewline() { m_Text.Add("\n"); m_Cursor.CharacterIndex++; m_Cursor.Position.X = m_TextEditPosition.X; m_Cursor.LineIndex++; //Scroll the text up if it's gone beyond the borders of the control. if ((m_TextEditPosition.Y - TextSize().Y) < (m_TextEditPosition.Y - m_TextEditSize.Y)) { m_TextPosition.Y -= CapitalCharacterHeight; m_CanMoveCursorDown = false; m_UpdateCharPositions = true; } if (m_CanMoveCursorDown) m_Cursor.Position.Y += CapitalCharacterHeight; m_NumLinesInText++; } /// <summary> /// Removes text from m_Text. /// </summary> private void RemoveText() { FixCursorIndex(); FixCursorPosition(); if (m_Cursor.Position.X > m_TextEditPosition.X) { m_Text.RemoveAt(m_Cursor.CharacterIndex); m_Cursor.CharacterIndex--; m_Cursor.Position.X -= CharacterWidth; } if (m_Cursor.Position.X <= m_TextEditPosition.X) { if (m_Cursor.LineIndex != 0) { m_Cursor.Position.X = m_TextEditPosition.X + m_Font.MeasureString(GetLine(m_Cursor.LineIndex - 1)).X; if (m_MultiLine) { m_Cursor.Position.Y -= CapitalCharacterHeight; m_Cursor.LineIndex--; m_NumLinesInText--; if (m_TextPosition.Y < m_TextEditPosition.Y) m_TextPosition.Y += m_Font.LineSpacing; } } } } /// <summary> /// Moves m_Cursor left. /// </summary> private void MoveCursorLeft() { if (m_Cursor.Position.X > m_TextEditPosition.X) { m_Cursor.CharacterIndex -= (((NUM_CHARS_IN_LINE + 1) - GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length); m_Cursor.Position.X -= CapitalCharacterHeight; } //Scroll the text right if the cursor is at the beginning of the control. if (m_Cursor.Position.X == m_TextEditPosition.X) { if (m_TextPosition.X > m_TextEditPosition.X) m_TextPosition.X -= m_Font.LineSpacing; } } /// <summary> /// Moves m_Cursor right. /// </summary> private void MoveCursorRight() { if (m_Cursor.Position.X < (m_TextEditPosition.X + m_TextEditSize.X)) { m_Cursor.CharacterIndex += (((NUM_CHARS_IN_LINE + 1) - GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length); m_Cursor.Position.X += CapitalCharacterHeight; } //Scroll the text right if the cursor is at the beginning of the control. if (m_Cursor.Position.X == m_TextEditPosition.X) { if (m_TextPosition.X < m_TextEditPosition.X) m_TextPosition.X += m_Font.LineSpacing; } } /// <summary> /// Moves m_Cursor up. /// </summary> private void MoveCursorUp() { if (m_Cursor.Position.Y > m_TextEditPosition.Y) { m_Cursor.LineIndex--; m_Cursor.CharacterIndex -= (((NUM_CHARS_IN_LINE + 1) - GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length); m_Cursor.Position.Y -= CapitalCharacterHeight; m_CanMoveCursorDown = true; } //Scroll the text down if the cursor is at the top of the control. if (m_Cursor.Position.Y == m_TextEditPosition.Y) { if (m_TextPosition.Y < m_TextEditPosition.Y) m_TextPosition.Y += m_Font.LineSpacing; } } /// <summary> /// Moves m_Cursor down. /// </summary> private void MoveCursorDown() { if (m_Cursor.Position.Y < (m_TextEditPosition.Y + m_TextEditSize.Y)) { m_Cursor.LineIndex++; m_Cursor.CharacterIndex += (((NUM_CHARS_IN_LINE + 1) - GetLine(m_Cursor.LineIndex).Length) + GetLine(m_Cursor.LineIndex).Length); m_Cursor.Position.Y += CapitalCharacterHeight; } else //Scroll the text up if the cursor is at the bottom of the control. { if ((m_TextPosition.Y + TextSize().Y) > (m_TextEditPosition.Y + m_TextEditSize.Y)) m_TextPosition.Y -= m_Font.LineSpacing; } } /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // TODO: Add your initialization logic here base.Initialize(); } /// <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); // TODO: use this.Content to load your game content here m_Font = Content.Load<SpriteFont>("ProjectDollhouse_11px"); } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// game-specific content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// <summary> /// Calculates the size of all the text in the textbox. /// </summary> /// <returns>A Vector2 containing the width and height of the text.</returns> private Vector2 TextSize() { float Width = 0.0f, Height = 0.0f; foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray())) { Vector2 Size = m_Font.MeasureString(Str); Width = Size.X; Height += Size.Y; } return new Vector2(Width, Height); } /// <summary> /// Update the hitboxes for the characters in the textbox. /// The hitboxes are used to detect collision(s) with the mouse cursor. /// </summary> private void UpdateHitboxes() { if (m_UpdateCharPositions) { int Height = 0; m_HitBoxes.Clear(); //Make sure it doesn't go out of bounds... if (m_Text.Count >= 1) { for (int i = 0; i < m_CharacterPositions.Count; i++) { //Make sure it doesn't go out of bounds... Height = (int)m_Font.MeasureString(m_Text[i < m_Text.Count ? i : m_Text.Count - 1]).Y; //Create a hitbox for each character if the character isn't the last one. if (i != m_CharacterPositions.Count - 1) { Rectangle Hitbox = new Rectangle((int)m_CharacterPositions[i].X, (int)m_CharacterPositions[i].Y, (int)(m_CharacterPositions[i + 1].X - m_CharacterPositions[i].X), Height); m_HitBoxes.Add(Hitbox); } } } m_UpdateCharPositions = false; } } /// <summary> /// Updates the positions of the characters. /// Called when a character is added or deleted from the textbox. /// </summary> private void UpdateCharacterPositions() { Vector2 Position = m_TextEditPosition; float XPosition = 0, YPosition = 0; if (m_UpdateCharPositions) { m_CharacterPositions.Clear(); int CharIndex = 0; foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray())) { XPosition = 0; for (int i = 0; i < Str.Length; i++) { float CharWidth = m_Font.MeasureString(Str.Substring(i, 1)).X; XPosition += CharWidth; m_CharacterPositions.Add(CharIndex, new Vector2(XPosition + m_TextEditPosition.X, Position.Y + m_TextEditPosition.Y)); CharIndex++; } YPosition += CapitalCharacterHeight; Position.Y = YPosition; } ///This shouldn't be set here, because it is set in UpdateHitboxes(); //m_UpdateCharPositions = false; } } /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); m_Input.Update(); UpdateCharacterPositions(); UpdateHitboxes(); foreach (Rectangle Hitbox in m_HitBoxes) { if (Hitbox.Contains(new Vector2(m_Input.MousePosition.X, m_Input.MousePosition.Y)) && m_Input.IsNewPress(MouseButtons.LeftButton)) { m_Cursor.Position = new Vector2(Hitbox.X, Hitbox.Y); int CharIndex = m_CharacterPositions.FirstOrDefault(x => x.Value == m_Cursor.Position).Key; if (CharIndex != -1) m_Cursor.CharacterIndex = CharIndex; } } foreach (Keys Key in (Keys[])Enum.GetValues(typeof(Keys))) { if (m_Input.IsNewPress(Key)) { m_DownSince = DateTime.Now; m_RepChar = Key; } else if (m_Input.IsOldPress(Key)) { if (m_RepChar == Key) m_RepChar = null; } if (m_RepChar != null && m_RepChar == Key && m_Input.CurrentKeyboardState.IsKeyDown(Key)) { DateTime Now = DateTime.Now; TimeSpan DownFor = Now.Subtract(m_DownSince); if (DownFor.CompareTo(TimeSpan.FromMilliseconds(m_TimeUntilRepInMillis)) > 0) { // Should repeat since the wait time is over now. TimeSpan repeatSince = Now.Subtract(m_LastRep); if (repeatSince.CompareTo(TimeSpan.FromMilliseconds(1000f / m_RepsPerSec)) > 0) // Time for another key-stroke. m_LastRep = Now; } } } Keys[] PressedKeys = m_Input.CurrentKeyboardState.GetPressedKeys(); //Are these keys being held down since the last update? if (m_RepChar == Keys.Back && m_LastRep == DateTime.Now) RemoveText(); if (m_RepChar == Keys.Up && m_LastRep == DateTime.Now) { if(m_MultiLine) MoveCursorUp(); } if (m_RepChar == Keys.Down && m_LastRep == DateTime.Now) { if(m_MultiLine) MoveCursorDown(); } foreach (Keys K in PressedKeys) { if (m_Input.IsNewPress(K)) { switch(K) { case Keys.Up: if (m_RepChar != Keys.Up || m_LastRep != DateTime.Now) { if(m_MultiLine) MoveCursorUp(); } break; case Keys.Down: if (m_RepChar != Keys.Down || m_LastRep != DateTime.Now) { if (m_MultiLine) MoveCursorDown(); } break; case Keys.Left: if (!m_MultiLine) MoveCursorLeft(); break; case Keys.Right: if (!m_MultiLine) MoveCursorRight(); break; case Keys.Back: if (m_RepChar != Keys.Back || m_LastRep != DateTime.Now) RemoveText(); break; case Keys.LeftShift: m_CapitalLetters = true; break; case Keys.RightShift: m_CapitalLetters = true; break; case Keys.Enter: AddNewline(); break; } } } base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle((int)m_TextEditPosition.X, (int)m_TextEditPosition.Y, (int)m_TextEditSize.X, (int)m_TextEditSize.Y); Vector2 Position = m_TextPosition; spriteBatch.Begin(SpriteSortMode.BackToFront); if(m_Cursor.Visible) spriteBatch.DrawString(m_Font, m_Cursor.Symbol, m_Cursor.Position, Color.White); // TODO: Add your drawing code here foreach (string Str in TextWithLBreaks.Split("\n".ToCharArray())) { spriteBatch.DrawString(m_Font, Str, Position, Color.White); Position.Y += CapitalCharacterHeight; } spriteBatch.End(); base.Draw(gameTime); } } } UpdateCharacterPositions() and UpdateHitboxes() seems to work perfectly. That is, UNTIL the text has been scrolled up:
      /// <summary> /// Adds a newline to m_Text, and updates the cursor. /// </summary> private void AddNewline() { m_Text.Add("\n"); m_Cursor.CharacterIndex++; m_Cursor.Position.X = m_TextEditPosition.X; m_Cursor.LineIndex++; //Scroll the text up if it's gone beyond the borders of the control. if ((m_TextEditPosition.Y - TextSize().Y) < (m_TextEditPosition.Y - m_TextEditSize.Y)) { m_TextPosition.Y -= CapitalCharacterHeight; m_CanMoveCursorDown = false; m_UpdateCharPositions = true; } if (m_CanMoveCursorDown) m_Cursor.Position.Y += CapitalCharacterHeight; m_NumLinesInText++; } I'm guessing that I need to take m_TextPosition.Y into consideration here:
      foreach (Rectangle Hitbox in m_HitBoxes) { if (Hitbox.Contains(new Vector2(m_Input.MousePosition.X, m_Input.MousePosition.Y)) && m_Input.IsNewPress(MouseButtons.LeftButton)) { m_Cursor.Position = new Vector2(Hitbox.X, Hitbox.Y); int CharIndex = m_CharacterPositions.FirstOrDefault(x => x.Value == m_Cursor.Position).Key; if (CharIndex != -1) m_Cursor.CharacterIndex = CharIndex; } } But I'm not entirely sure what to do. I tried both multiplying and adding m_TextPosition.Y to m_Input.MousePosition.Y and Hitbox.Y, but none of them seemed to work.
      Please help!
    • By zuhane
      Hello forum dwellers!
      I've recently decided to dive into using ECS, as it looks like it solves so many issues with game development, especially with the game I am currently making (which involved many complex entities seamlessly interacting at the same time). It all seemed relatively straightforward. So far, I understand that:
      1.) The entity is simply a container for components (which can also have an identifier)
      2.) The components are interchangeable blocks of data which change the way the entity will behave (only containing data - no methods/functions/logic)
      3.) The systems loop through all the entities and checks to see which components they contain. If it contains the appropriate components, act on them.
      So far, this has been easy to implement and worked for the basic design. For example, I have a player, block and a ball. The rendering component is present in all 3, colliders in all 3, physics in the player and ball, and input only in the player. I recently looked up how components interact with one another, and that also seems simple. For example, if I want a player to animate due to a button press, I would create a system which requires a rendering component AND input component. The problem I seem to be having trouble understanding is exactly how to nest more complex entities and components.
      For example, I want my player to be able to punch the ball around. If the player presses the "E" key, I want to create a fist which punches to the left (drawn over the player without the player animation changing). However, the fist itself punching the ball technically has a sprite that needs rendering and also a collision box, so should I create a "fist" component which contains 2 components (rendering and collision), or should I generate a new entity which contains those 2 components and let the systems act naturally on it? I feel like by nesting components within components, I end up with this spaghetti code where systems are calling other systems, removing the whole point of an ECS. It feels more "natural" to just leave the systems to wait and act on entities which contain the correct components. However, if I create a new "fist" entity, this needs to be tied to the player, so I ended up with an entity containing an entity which also seems ugly, or a weird entity with some kind of reference of linking ID to its creator. Am I missing something?

      On another note, is it unsafe to add multiple instances of the same component? I've heard that it's bad practice too. For example, I might have entities which all require rendering, but some might want to use animated sprites, multiple layers of sprites, shaders, etc. Is it better to have multiple rendering components which can optionally interact, or possibly use a rendering component interface which multiple rendering components can inherit from? Is this inheritance not defeating the purpose of using an ECS in the first place?
       
      Any help would be greatly appreciated!
      Thanks, Zuhane
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!