Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Help with handling a chat box, including word wrapping and scrolling  (Read 4705 times)

0 Members and 2 Guests are viewing this topic.

TheRabbit

  • Newbie
  • *
  • Posts: 26
    • View Profile
My game has a multiplayer chat box in it, and I'm having a tough time implementing a scrolling and word wrapping chatbox without using a hammer. It works, but I feel like I'm using way too many variables and some really messy arrays to get it done.

Here's the relevant code bits (the really relevant parts are down in Game::pushMessage()):
class Game
{
public:
        Game();
        void run();

private:
        //...
        void processEvents();
        void render();
        void processInput(); //handles what happens when the user has types something and presses enter
        void pushMessage(std::string str_in, sf::Color color=sf::Color::Black); //puts a new message onto the stack
        //...

private:
        //...
        sf::Font chatFont;
        int chatLineFocus; //integer showing which chat line the client is currently looking at (allows scrolling text)
        sf::Text chatText[20]; //holds the last 20 rows of text that shows up in the chat box
        //...
};
//...
Game::Game()
{
        //...
        chatFont.loadFromFile("C:/Windows/Fonts/arial.ttf");
        for (int i=0;i<20;i++) //load the chat window with fonts
        {
                chatText[i].setFont(chatFont);
                chatText[i].setCharacterSize(12);
                chatText[i].setColor(sf::Color::Black);
        }
        chatLineFocus = 19; //The client is focused on the very bottom line of text
        //...
}
//...
void Game::processEvents()
{
        //...
                case sf::Event::MouseWheelMoved:
                        if (event.mouseWheel.delta > 0) //mouse wheel up
                        {
                                if (chatLineFocus > 9)
                                {
                                        chatLineFocus--;
                                }
                        }
                        else //mouse wheel down
                        {
                                if (chatLineFocus < 19)
                                        chatLineFocus++;
                        }
                        break;
        //...
        dataPacketFrom.clear();
        if (socketStatus == sf::Socket::Done) //only check if we're connected
        {
                if (socket.receive(dataPacketFrom) != sf::Socket::NotReady)
                {
                        //if there is data available this will return something other than NotReady
                        std::string str_in;
                        dataPacketFrom >> str_in;
                        pushMessage(str_in);
                }
        }
        //...
}
//...
void Game::render()
{
        mWindow.clear();
        //...

        //draw the chatbox
        sf::RectangleShape chatBox(sf::Vector2f(580.f,175.f)); //create a white rectangle
        sf::RectangleShape chatInput(sf::Vector2f(580.f,40.f)); //create another white rectangle

        //position and fill the boxes
        chatBox.setFillColor(sf::Color(255,255,255,125));
        chatBox.setPosition(700.f,545.f);
        chatInput.setFillColor(sf::Color(255,255,255,125));
        chatInput.setPosition(700.f,680.f);

        mWindow.draw(chatBox);
        mWindow.draw(chatInput);
        for (int i = 0; i < 9; i++)
        {
                //draw each of the 9 "visible" texts
                chatText[chatLineFocus - (8 - i)].setPosition(705.f, 550.f + (13.f * float(i)));
                mWindow.draw(chatText[chatLineFocus - (8 - i)]);
                temp2 = chatText[chatLineFocus - (8 - i)].getString().toAnsiString();
                temp = chatText[chatLineFocus - (8 - i)].getPosition();
        }
        mWindow.draw(currentInput);
        //...
        mWindow.display();
}
//here's where it gets ugly...
void Game::pushMessage(std::string str_in, sf::Color color)
{
        unsigned int i;
        sf::String lastLine;
        sf::Glyph glyph; //used for calculating line length
        int lineSize = 0; //used to determine when a line would run off the page
        int lastSpace = -1; //used to keep track of the last space in a line

        for (i=1;i<20;i++)
                chatText[i-1] = chatText[i]; //shift all the messages down to accept a new message
        chatText[19].setString(sf::String(str_in)); //put the new message in
        chatText[19].setColor(color);
       
        //break the most recent chat line up with line breaks
        lastLine = chatText[19].getString();
        for (i = 0; i < lastLine.getSize(); i++)
        {
                glyph = chatFont.getGlyph(lastLine[i],chatText[0].getCharacterSize(),false); //get the current character
                if ((lineSize + glyph.advance) > 570) //check for runoff
                {
                        for (i=1;i<20;i++)
                                chatText[i-1] = chatText[i]; //shift all the messages down again
                        if (lastSpace != -1)
                        {
                                chatText[18].setString(lastLine.toAnsiString().substr(0,lastSpace));//put the first part of the string up a line
                                chatText[19].setString(lastLine.toAnsiString().substr(lastSpace+1));//put the remainder back into slot 19
                        }
                        else
                        {
                                chatText[18].setString(lastLine.toAnsiString().substr(0,i));//put the first part of the string up a line
                                chatText[19].setString(lastLine.toAnsiString().substr(i));//put the remainder back into slot 19
                        }
                        lastLine = chatText[19].getString(); //reset our tracking string and start over to see if this new line is long
                        lastSpace = -1;
                        i=0; //reset our counter
                        lineSize = 0; //reset our line counter
                }
                else
                        lineSize += glyph.advance; //increment linesize by the texture size of the current character
                if (lastLine[i] == '\n')
                        lineSize = 0;
                if (lastLine[i] == ' ')
                        lastSpace = i;
        }
}

 

The other problem I have is that the user's text input just keeps going right off the edge of the screen, I'd like to simply shift the text over so that the text is still visible, but I can't figure out how to do that using an sf::Text.

I've searched around and looked at the way some other people implement things, and it seems like other people's code does way more things than I need it to (like SFGUI and TGUI) or they're dead projects that aren't fully implemented (SFChat and TextArea).

Here's a screenshot of what it looks like when it's running:


Any thoughts on how I could make this be a bit more elegant?
« Last Edit: November 02, 2013, 07:48:43 am by TheRabbit »

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #1 on: November 02, 2013, 11:38:31 am »
Don't use arrays. Replace them with STL containers, usually std::vector (or std::array if you really need fixed size without dynamic allocations, which is not the case here).

Pass class objects in general by const-reference.
void pushMessage(const std::string& str, const sf::Color& color=sf::Color::Black);

Use a consistent name convention. str_in doesn't match the rest.

Combine nested if conditions:
if (event.mouseWheel.delta > 0 && chatLineFocus > 9)
    --chatLineFocus;
else if (event.mouseWheel.delta < 0 && chatLineFocus < 19)
    ++chatLineFocus;

Then, you can use references to create an alias for an often used object:
sf::Text& text = chatText[chatLineFocus - (8 - i)];

text.setPosition(705.f, 550.f + (13.f * float(i)));
mWindow.draw(text);
temp2 = text.getString().toAnsiString();
temp = text.getPosition();

Use more meaningful variable names than "temp". Why do you even need to store the string again? Also consider that sf::String offers implicit conversions.

Check if a socket receipt is successful, and not only if one specific error occurs.
if (socket.receive(dataPacketFrom) == sf::Socket::Done)

Don't declare variables long before they are used. Declaring all necessary variables at the beginning of a function or block is ancient practice originating from C89 (that's a quarter of a century!). This will already make your code a bit less ugly. Don't declare loop variables such as i outside the loop. For example:
sf::Glyph glyph = chatFont.getGlyph(lastLine[i],chatText[0].getCharacterSize(),false);

Instead of
for (i=1;i<20;i++)
    chatText[i-1] = chatText[i]; //shift all the messages down to accept a new message
you can simply erase the front element of an STL container. Maybe std::deque, but also std::vector won't hurt for such few elements.

Instead of
chatText[19].setString(sf::String(str_in)); //put the new message in
you use the container's member function push_back(). Don't ever refer to magic number indices, use front() and back() to highlight the special meaning of these elements. But avoid magic numbers such as 19.

The condition
if (lastSpace != -1)
and its contained statements can be condensed, since only one number is different. Don't hesitate to create new functions that help you for small tasks.

One option is also to separate the graphical update and the draw() calls. After all, it's unnecessary to recompute all the colors, positions and so on if nothing has changed.

Furthermore, after cleaning up the code, make sure the high-level logic is clearly understandable. This will also help you see if there are mistakes or potential for simplification.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

fallahn

  • Hero Member
  • *****
  • Posts: 507
  • Buns.
    • View Profile
    • Trederia
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #2 on: November 02, 2013, 12:04:33 pm »
FWIW sfchat isn't dead (the last commit was only 2 days ago) - it's just meant to implement nothing more than a chat protocol (how well it actually does that is another matter.. ;) ). How you format the string data which is sent/received and how you build the GUI is entirely up to the end user.

TheRabbit

  • Newbie
  • *
  • Posts: 26
    • View Profile
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #3 on: November 02, 2013, 06:34:27 pm »
Don't use arrays. Replace them with STL containers, usually std::vector (or std::array if you really need fixed size without dynamic allocations, which is not the case here).
Good point, I'm working on replacing it with a std::vector now. But I have a couple questions...

Quote
Pass class objects in general by const-reference.
Done.

Quote
Use a consistent name convention. str_in doesn't match the rest.
Done.

Quote
Combine nested if conditions:
Done.

Quote
Then, you can use references to create an alias for an often used object:
Good idea!

Quote
Use more meaningful variable names than "temp". Why do you even need to store the string again?
That was some debugging code that I missed before pasting it here.

Quote
Check if a socket receipt is successful, and not only if one specific error occurs.
Good point, there could be other events that occur other than receiving a message.

Quote
Don't declare variables long before they are used. Declaring all necessary variables at the beginning of a function or block is ancient practice originating from C89 (that's a quarter of a century!).
That's when I first learned to code (early 90's), some habits are so ingrained I have a difficult time changing them.

Quote
Instead of
for (i=1;i<20;i++)
    chatText[i-1] = chatText[i]; //shift all the messages down to accept a new message
you can simply erase the front element of an STL container. Maybe std::deque, but also std::vector won't hurt for such few elements.
Ok, here's where I need help. I know I can pop the ends off of std::vectors, but how do I pop the front off? That for loop basically just shifts everything down a slot (0 holds what used to be 1, 1 holds what used to be 2, etc.). So to effectively pop I'd need to pop element 0 off, and I don't think std::vectors can do that.

I changed the loop to:
        for (std::vector<sf::Text>::iterator it = chatText.begin() + 1; it != chatText.end(); it++)
                *(it - 1) = *it; //this might work?
for now so that I'm not using magic numbers any more.


Quote
Instead of
chatText[19].setString(sf::String(str_in)); //put the new message in
you use the container's member function push_back().
I can't really use this until I find a way to pop the front of the vector off. So instead I'm using:
chatText[MAXHISTORY - 1].setString(sf::String(stringIn)); //put the new message in
for now.

Quote
Don't ever refer to magic number indices, use front() and back() to highlight the special meaning of these elements.
Good point, I'm using front and back now.

Quote
One option is also to separate the graphical update and the draw() calls.
I already had a function for this, I just haven't moved the code into there yet for debugging purposes.

Quote
Furthermore, after cleaning up the code, make sure the high-level logic is clearly understandable. This will also help you see if there are mistakes or potential for simplification.
Thanks for all the tips.

I still can't figure out how to scroll the chat input that the user is typing. I guess I could use a substr or something when I display it?
Edit: I tried using a substring and it looks like I'll have to do the whole glyph calculation thing again thanks to non fixed width characters...
Edit2: got it working:
void Game::updateUserInput()
{
        int lineSize = 0;
        for (unsigned int i = currentInput.length(); i > 0; i--)
        {
                sf::Glyph glyph = chatFont.getGlyph(currentInput[i],drawableInput.getCharacterSize(),false); //get the current character
                if ((lineSize + glyph.advance) > 570) //check for runoff
                {
                        drawableInput.setString(sf::String(currentInput.substr(i)));
                        return; //break out if we get to the point where the chat line is too long!
                }
                else
                        lineSize += glyph.advance; //increment linesize by the texture size of the current character
        }
        //if we made it to here the text the user has inputted isn't longer than the line length
        drawableInput.setString(sf::String(currentInput));
}
« Last Edit: November 02, 2013, 07:15:58 pm by TheRabbit »

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #4 on: November 02, 2013, 08:53:16 pm »
You should probably read a bit about the STL, it's a fundamental part of C++. The API is documented on cppreference, but you should also have a look at the usage patterns of containers, iterators and algorithms, preferably in a good book.

You can erase the first element of a vector with
vector.erase(vector.begin());
The reason why this container does not support pop_front() is that it cannot implement it efficiently. Removing the front element leads to a copy of all the remaining elements. That is, as soon as you have many elements of which you need to remove the front (20 are not many), you should think about a different container type.

Or simply use the trick, where the element to remove is swapped with the last element using std::swap(), and the last one is then removed using std::vector::pop_back(). This works when the element order is not relevant.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

TheRabbit

  • Newbie
  • *
  • Posts: 26
    • View Profile
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #5 on: November 02, 2013, 09:06:56 pm »
Thanks, I think I'll implement the chat history using a deque so I can pop front and back. I'd never heard of these container classes before about a week ago, it looks like they were only added into the standard about 15 years ago, so I've never ran into them at work.

Thanks again.
« Last Edit: November 02, 2013, 09:18:14 pm by TheRabbit »

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6287
  • Thor Developer
    • View Profile
    • Bromeon
Re: Help with handling a chat box, including word wrapping and scrolling
« Reply #6 on: November 03, 2013, 11:33:29 am »
I'd never heard of these container classes before about a week ago, it looks like they were only added into the standard about 15 years ago, so I've never ran into them at work.
"only" :D

Time passes fast ;) you should also have a look at the C++11 standard, it contains very interesting concepts that allow further simplification of the code. For example, instead of an iterator loop
for (std::vector<sf::Text>::iterator itr = vec.begin(); itr != vec.end(); ++itr) // use pre-increment!
type inference can deduce the correct iterator type:
for (auto itr = vec.begin(); itr != vec.end(); ++itr)
And even simpler, you can use the range-based for statement to iterate through all elements:
for (sf::Text& text : vec)

And instead of raw pointers and manual memory management, you can use std::unique_ptr. See also this example that shows RAII in practice.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development:

 

anything