🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Basic game loop advice (text based)

Started by
6 comments, last by ICanC 4 years, 5 months ago

hello, i'm making a simple text based strategy game in C++, using ncurses for the ‘terminal screen’ and SFML for audio. I'm using std::chrono for timers. This is the first time i've dealt with a game loop proper and a concept of time in my games. There is 1 game timer, which “ticks” every 5 seconds, and runs all my update functions. The (basic) problem I have now is I need all this to happen silently in the background, while the player goes about their business, navigating text menus etc. Right now as soon as the loop starts, the game is locked in the while loop and input is impossible. I assume I need some kind of “getscreen()” function that redraws whatever the player is looking at, and a “getinput()” function, to keep checking for a character input, but i'm not exactly sure how best to implement this without the screen constantly “locking”. Any advice would be appreciated. Current main is below, and just to give you an idea for what a “screen” is to me, an example menu

int main()
{
	auto start_time = std::chrono::system_clock::now();
	initscr(); --start ncurses
	createMap(); 
	gameMenu();
	gameTick();

	while (true)
	{
		auto end_time = std::chrono::system_clock::now();
		std::chrono::duration<double> diff = end_time - start_time;
		if (diff.count() > 5.0)
		{
			start_time = end_time;
			g_gameTick++;
			gameTick();
		}
		//getinput(), getscreen()?
	}
	return 0;
}
//example menu

void buildingsMenu(race* self)
{
	clear();
	title();

	int line = LINE_START;
	int pos = POS_START;

	jprint("=== BUILDING MANAGER ===", line, pos); line += 3;
	jprint("1. Shipyard", line, pos); line++;
	jprint("(1000 Metal, 500 Steel)", line, pos); line += 3;

	line = LINE_START;
	pos = POS_START + 40;
	jprint("=== UNDER CONSTRUCTION ===", line, pos); line += 3;
	for (auto&amp; i : self->getBuildingsUnderConstruction())
	{
		jprint(i.getName(), line, pos); pos += 10;
		jprint("Completion Tick: ", line, pos); pos += 5;
		jprint(i.completionTick(), line, pos);
		line += 2; pos = POS_START + 40;
	}

	line = 20;
	pos = POS_START;

	jprint("> ", line, pos);
	char c = input(); //"scanw"

	switch (c)
	{
	case '1': building shipyard("Shipyard", 0, 5);
		self->build(shipyard);
		break;
	}
}
Advertisement

If you're looking to run a game loop you should find out how often you want to run your updates per second. For example if you run your updates 30 times a second you would do your logic once the tick is due, then wait until the next tick is due to run it again. Your ticks are defined by where in a time line they would have to start to fit 30 cycles in 1 second and this is tracked by your running clock. Display can update either independently, or locked into that tick as you have fixed time steps, variable time steps, semi-fixed time steps.

Input can be handled either as part of your update cycle, or you can make it event based.

Read these articles to get a better understanding:

https://dewitters.com/dewitters-gameloop/

https://gafferongames.com/post/fix_your_timestep/

Programmer and 3D Artist

Please don't use auto. It obscures the meaning of the code.

taby said:

Please don't use auto. It obscures the meaning of the code.

please don‘t give advice like that. Auto is perfectly fine, as long as you don‘t overuse it (and even there is major debate to be had on where that overuse is; see AlmostAlwaysAuto).

OPs usage of auto is a good example on where its useful - there is no use in typing std::chrono::timepoint<std::chrono::system_clock>time=…

So please don‘t listen to that @OP.

By explicitly declaring a type, you are implicitly declaring a type conversion. In other words, this:

int x = f();

is equivalent to this:

auto x = implicit_cast<int>(f());

If you don't need a type conversion, don't explicitly declare types. And if you do need a type conversion, it's better to spell out the cast than to rely on implicit conversions.

ICanC said:
There is 1 game timer, which “ticks” every 5 seconds, and runs all my update functions.

So basically you need a loop that does some stuff every 5 seconds. A key change you need to make is you can't have things like “buildingMenu” that want to stop and wait on input as by waiting there you stop the 5 second loop from running (and I really don't advise using threads to “solve” that problem).

The other key thing here is you probably don't want to sit in any sort of sleep for as long as 5 seconds, as it will make input entirely unresponsive, no echo or anything. Whichever approach I can't think of a quick way to get the normal line entry functionality like backspace/delete/left/right/etc. right now without doing it yourself, maybe I missed an obvious API in ncurses.

You can use non-blocking at a much higher update rate, then it's much like the normal game loops, but replace reading events from a window with reading console input. You can make it non-blocking in ncurses I believe with timeout(0) and then use something like getch or getnstr until there is no more input available. I believe select on stdin should also work. Then us some other timer check to handle the 5 second stuff (e.g. say you loop at a fixed 60Hz, then do the 5 second stuff on every 300th frame).

Alternatively, you could block on input until it is “complete” (say enter key / line), or until 5 seconds are up. Unfortunately getnstr etc. plus timeout does not appear to work in the desired fashion, every key stroke resets the timeout rather than it being a total, so someone typing can stop it returning forever/buffer limit. So a loop with say getch would work but you have to handle a bunch of kinda annoying stuff like backspace and cursor navigation yourself it seems. I think something like this thinking about it really quickly:


int main()
{
    std::chrono::seconds step(5);
    auto next_frame = std::chrono::system_clock::now();
    initscr();
    int frame_count = 0;
    std::string input_str;
    while (true)
    {
        // Do input (with delay for loop speed)
        do
        {
            auto time_until_next_frame = next_frame - std::chrono::system_clock::now();
            auto time_until_next_frame_ms = std::chrono::duration_cast<std::chrono::duration<int,std::milli>>(
                time_until_next_frame).count();
            mvprintw(12, 0, "Waiting %ims", time_until_next_frame_ms);
            timeout(std::max(0, time_until_next_frame_ms)); // negative would wait forever, but still want to check for input
            move(0, (int)input_str.size()); // put the cursor in right place for any echo
            int c = getch(); // Note returns an int, there are also a bunch of KEY_* defines for special keys plus ERR, hence int not char!
            if (c != ERR) // I think all the special KEY_* stuff is 256 and above, probably should check the return in more detail
            {
                // will want to consider things like delete, backspace, etc. maybe even left/right cursor navigation, insert.
                // and anything with a KEY_* define
                // I don't think there is a simple solution however
                if (c == '\n') input_str += "\\n"; // might want to do something on this immediately
 or any other special keys
                else input_str += (char)c;
            }
        }
        while (std::chrono::system_clock::now() < next_frame);

        // Any "every 5 seconds" update
        // Or handle input here if player also must wait for the 5 second tick
        mvprintw(10, 0, "Entered so far: %s", input_str.c_str());
        ++frame_count;
        next_frame += step;
        mvprintw(11, 0, "Frame: %d", frame_count);
    }
    endwin();
    return 0;
}

thanks a lot Rutin and SyncViews, i'll read through and give that a try.

This topic is closed to new replies.

Advertisement