6
\$\begingroup\$

Looking to build a terminal based game.

I am assuming X-Term like terminal.
This means:

  • I can use the X-Term control codes to clear and move around the screen.
  • I can use tcsetattr functions to make standard input none buffered (I think).

I know this game loop runs as basically a busy wait. I will sove that at the select() command by adding the appropriate timeout there (I just need to calculate an appropriate value).

Note: I installed the signal handlers so that when the application is being forced to quit I can still restore the terminal to its original state before the application quite. You will notice the std::cout << showCursor; to turn the cursor back on and the destructor sets the terminal state back to its original value: tcsetattr(STDIN_FILENO, TCSAFLUSH, &originalConfig);

This is the shell for my game (I have not implemented game logic). I wanted to make sure I am not messing stuff up around display and terminal manipulation before I go any further.

#include <iostream>

#include <signal.h>
#include <termios.h>
#include <stdio.h>
#include <unistd.h>

static volatile sig_atomic_t  done = 0;

extern "C" void handle_done(int signum)
{
    if (!done) {
        done = signum;
    }
}

class Game
{
    static constexpr int    width   = 40;
    static constexpr int    height  = 20;
    static constexpr char   clearScreen[]       = "\033[2J";
    static constexpr char   movetoZeroZero[]    = "\033[0;0H";
    static constexpr char   hideCursor[]        = "\033[?25l";
    static constexpr char   showCursor[]        = "\033[?25h";

    termios originalConfig;
    bool    gameOver;

    char getChar(int x, int y)
    {
        if (y == 0 || y == height -1 || x == 0 || x == width -1 ) {
            return '#';
        }
        return ' ';
    }
    void draw(bool dirty)
    {
        std::cout << movetoZeroZero
                  << "Snake V1\n";
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                std::cout << getChar(x, y);
            }
            std::cout << "\n";
        }
        std::cout << std::flush;
    }

    bool input()
    {
        if (done) {
            gameOver = true;
        }
        fd_set  input;
        FD_ZERO(&input);
        FD_SET(STDIN_FILENO, &input);

        timeval timeout{0, 0};

        if (select(STDIN_FILENO+1, &input, nullptr, nullptr, &timeout) > 0) {
            char key = std::cin.get();
            if (key == 'Q') {
                gameOver = true;
            }
        }
        return false;
    }
    bool logic()
    {
        return false;
    }

    int install_done(const int signum)
    {
        struct sigaction  act {0};
        memset(&act, 0, sizeof act);
        sigemptyset(&act.sa_mask);
        act.sa_handler = handle_done;
        act.sa_flags = 0;
        if (sigaction(signum, &act, NULL) == -1) {
            return errno;
        }
        return 0;
    }

    public:
        Game()
            : gameOver(false)
        {
            if (install_done(SIGINT) || install_done(SIGTERM) || install_done(SIGHUP)) {
                throw std::runtime_error("Fail: Installing handlers\n");
            }
            termios  config;
            if (!isatty(STDIN_FILENO) || tcgetattr(STDIN_FILENO, &originalConfig) || tcgetattr(STDIN_FILENO, &config)) {
                throw std::runtime_error("Fail: Setting up input stream\n");
            }

            config.c_lflag &= ~(ICANON | ECHO);
            config.c_cc[VMIN] = 1; /* Blocking input */
            config.c_cc[VTIME] = 0;
            tcsetattr(STDIN_FILENO, TCSANOW, &config);
        }
        ~Game()
        {
            tcsetattr(STDIN_FILENO, TCSAFLUSH, &originalConfig);
        }
        void run()
        {
            std::cout << clearScreen
                      << hideCursor;

            bool dirty = true;

            while (!gameOver)
            {
                draw(dirty);
                dirty   = input();
                dirty   = logic() || dirty;
            }

            std::cout << showCursor;
        }
};

int main()
{
    Game    game;

    game.run();
}
\$\endgroup\$
5
  • \$\begingroup\$ Not exactly what you asked, but since a terminal game will likely not require extreme execution speed (no 3D or heavy graphics) I’d strongly suggest switching to a script language such as Javascript (node.js, deno) or Python. That will free you from resource management and save you a huge amount of time. \$\endgroup\$
    – Offirmo
    Mar 20 at 8:59
  • \$\begingroup\$ @Offirmo: Martin is an experienced C++ developer who has the C++ and C++11 tag badges; chances are he'll be faster using C++ than any other language. \$\endgroup\$ Mar 20 at 10:48
  • \$\begingroup\$ @Offirmo I think another language will take me much much longer (because those are not my primary languages). Resource management has been solved in C++ (this is not C). But I am also trying to show that it is relatively simple (and quick) to throw together a quick easy game. BUT I would love to see how this is done in Javascript if you have a link. \$\endgroup\$ Mar 20 at 16:04
  • \$\begingroup\$ @MartinYork Do add the youtube link when you're done. :) \$\endgroup\$
    – Harith
    Mar 20 at 16:46
  • 1
    \$\begingroup\$ @Harith A couple of more attempts at making it better first. But I will. \$\endgroup\$ Mar 20 at 16:50

2 Answers 2

12
\$\begingroup\$

Use a curses library or something similar

There are many details to worry about when writing low-level terminal code. I strongly recommend you use a library that takes away that burden, and that will let you focus more on writing the game itself.

I would recommend that you use a curses library, like ncurses. The API is standardized, and while ncurses is perhaps the most well-known one for UNIX, there are curses libraries for other operating systems as well, including Windows. Curses provides functions to hide the cursor, clear the screen, draw only changed portions of the screen, and even handle keyboard input, so it covers everything you already do in the code you posted. It will probably also cover many things you will need in the future.

However, there are other terminal libraries as well that can go even further, for example FTXUI.

\$\endgroup\$
4
  • 3
    \$\begingroup\$ That is my plan. I should have mentioned. The plan is to do a first version using low level stuff like this. Step two is to show how it is simplified by ncurses. Step three move to a GUI library. Was planning on dipping my hand into creating YouTube videos on programming. This would be my first attempt at a video. \$\endgroup\$ Mar 18 at 20:17
  • 3
    \$\begingroup\$ For a tutorial, I think too many things are going on at once here. You have a game object with a main loop, you have raw terminal I/O, you have signal handling, and you are mixing C++ with C. Try to focus on one thing at a time, and keep the rest as simple as possible. I also question the educational value of showing something that is obviously quite nasty (the raw terminal I/O), only to say "now we'll ditch this and use a nice library instead". You're also not starting with assembly or manual system calls. Of course, I don't know who your target audience will be, so feel free to ignore this. \$\endgroup\$
    – G. Sliepen
    Mar 18 at 21:58
  • \$\begingroup\$ As someone who made a game engine nobody uses, this ^ Focus on a game to the exclusion of technical detail and only move in when it breaks. You'll have way more fun \$\endgroup\$
    – Raven
    Mar 19 at 21:50
  • \$\begingroup\$ I think you are correct. I did this from scratch on Video and it took an hour. When I sat down I though 20-30 minutes (but that could just be my bad presentation skills). Also need to use a much bigger font on the next take. :-) \$\endgroup\$ Mar 20 at 16:13
4
\$\begingroup\$

Unless there is some game timer event pending (like if it's not just turn-based, or you have some kind of animation), then the appropriate timeout for select is infinity, not zero. So just NULL for that arg.

Essentially just block until there's a keypress, only using select over simple blocking read because you might want to monitor multiple input sources (fds), and as infrastructure for the presumed case that there will be some next event at the head of a priority queue which you'll want to wake up for. With a timeout of then - now(), perhaps with a microsecond offset to wake up slightly before the event, since waking up may take some small amount of time. (And late wakeup is also possible under heavy load.)

The Linux select man page recommends poll or epoll for modern applications because select is limited to fd numbers below 1024. But that's presumably fine for a game that doesn't do peer-to-peer networking with huge numbers of sockets.

\$\endgroup\$
4
  • \$\begingroup\$ I want the game to be animated (snake) so there will be some timeout. Like you mentioned something along the line nextFrameUpdateTime - lastFrameDrawTime. As your score increases the latency between moves decreases. \$\endgroup\$ Mar 20 at 16:08
  • \$\begingroup\$ I actually prefer libevent as it uses the best of poll, epoll or select depending on system. So abstracting away that low level system call. BUT it has been a few years since I used it so there could be something better out there by now. But this seems overkill for something this simple. \$\endgroup\$ Mar 20 at 16:10
  • \$\begingroup\$ @MartinYork: Ok, but your game skeleton doesn't currently have any timers or animation. So the appropriate placeholder is infinity, not zero. \$\endgroup\$ Mar 20 at 17:39
  • \$\begingroup\$ That's a fair comment.I'll look to abstract the time amount. So users can select no timout (for step based games) or a value for animated games. \$\endgroup\$ Mar 20 at 23:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.