Search code examples
c++winapiconsolewindows-console

How can I add line breaks in a wchar_t array?


I'm working on a console game. It uses a screen buffer to refresh the console window after updating the map.

Here's the main while loop.

while (true) {
    //player.doStuff(_kbhit());
    //map.update();
    WriteConsoleOutputCharacter(
        console.getScreenBuffer(),
        (LPWSTR)map.getScreen(),
        map.getWidth() * map.getHeight(),
        { 0, 0 }, console.getBytesWritten()
    );
    Sleep(1000 / 30);
}

Before this loop, I'm getting the layout of the map from a .txt file:

class Map {
    int width, height;
    wchar_t* screen;
public:
    wchar_t* getScreen() {
        return screen;
    }
    void setScreen(std::string layoutFile, std::string levelDataFile) {
        std::ifstream levelData(levelDataFile);
        levelData >> width >> height;
        screen = new wchar_t[(width + 1) * height];
        levelData.close();

        std::wifstream layout(layoutFile);
        std::wstring line;

        for (int j = 0; j < height; j++) {
            std::getline<wchar_t>(layout, line);
            for(int i = 0; i < width; i++) {
                screen[j * width + i] = line.at(i);
            }
            screen[width * (j + 1)] = L'\n';
        }
        layout.close();
    }
};


map.setScreen("demo.txt", "demo_data.txt");

The problem is that the printed map displays as one string without any line breaks, like this:

00000__00000

When I expected it to look like this instead:

0000
0__0 
0000

I tried adding L'\n', L'\r\n' after every line written, but it doesn't work.


Solution

  • In short

    There are two independent problems here:

    • The first one is that your newline characters get overwritten.
    • The second, once you've corrected the first, is that the windows console API does not handle newlines

    More details

    The problem with the overwriting

    I assume that the width does not include the trailing newline at the end of each line, since your allocation for the screen is:

    new wchar_t[(width + 1) * height];   // real width is width + 1 for '\n'
    

    Now looking at your logic, the last '\n' that you add to the line, is set at:

    screen[ width * (j + 1) ]   // same as screen[ j * width + width ]
    

    This seems fine according to your indexing scheme, since you copy the layout characters to:

    screen[ j * width + i ]`   // where `i` is between `0` and `width-1` 
    

    so the newline would be at screen[ j * width + width ].

    Unfortunately, with your indexing formula, the first character of the next line overwrites the same place: replacing j with j+1 and i with 0 gives:

    screen[ (j + 1) * width + 0 ]  
    

    which is

    screen[ (j + 1) * width ]    // same location as screen [ width * (j+1)]
    

    The solution for having trailing newlines on very line

    Correct your indexing scheme, taking into account that the real width of the line is width+1.

    So the indexing formula becomes:

        for(int i = 0; i < width; i++) {
            screen[j * (width+1) + i] = line.at(i);
        }
    

    and of course the trailing newline:

        screen[j * (width+1) + width] = L'\n';  // self-explaining
        screen[(j+1) * (width+1) -1] = L'\n';   // or alternative 
    

    The problem with the console API

    The WriteConsoleOutputCharacter() provides no real support for newlines and control characters. These are diplayed as a question mark.

    The documentation refers to a possibility to get those control characters handled depending on the console mode, but I've tried with Windows 10, and even with the variant WriteConsoleOutputCharacterA() (to be sure to exclude issues with wide characters), it simply does not work.

    You have two solutions to make this work, but the two require some rework:

    • display the output line by line and control the cursor position accordingly
    • use the WriteConsoleOutput() which allows you to specify the target rectangle (heigth and width) and write the characters in a rectangle without need for a newline. Unfortunately, the array shall then be of CHAR_INFO instead of simple characters.

    Example for the second way:

    std::string w = "SATORAREPOTENETOPERAROTAS";
    SMALL_RECT sr{ 2, 2, 6, 6 };
    CHAR_INFO t[25];
    for (int i = 0; i < 25; i++) { t[i].Char.AsciiChar = w[i]; t[i].Attributes = BACKGROUND_GREEN; }
    WriteConsoleOutputA(
        hOut,
        t,
        { 5,5 },
        { 0,0 },
        &sr
    );