0
\$\begingroup\$

This is an interesting question I have, so I ask you to hear me out. No, I'm not just going to ask "how do I do this" and hope someone will give me code.

I've been programming for a long time and am beginning work on a side-project, a text-based game.

Without going into details, the game needs commands (a Console.ReadLine() sort of situation) but also needs timer-based systems (say, for instance, decreasing a variable energy every 10 seconds)

I've racked my brain about how to accomplish this, but came up empty handed.

My best guess is using asynchronous functions but I don't fully understand those and they don't seem like they will accomplish what I'm after.

TL;DR I want to be able to use Console.ReadLine() and also increase/decrease a variable every n milliseconds (even while waiting for the console input).

I think threading might be a way to do it, but that seems excessive for a [relatively] simple usecase.

Can anyone provide insight about how to accomplish this?

Thanks in advance.

EDIT: Ideally this would be a general answer that could apply to running any time consuming task in the background, while keeping the console available to read lines, print output, etc.

\$\endgroup\$
9
  • 1
    \$\begingroup\$ Did you search for how to implement Console.ReadLine asynchronously? \$\endgroup\$
    – DMGregory
    Aug 4 at 20:55
  • \$\begingroup\$ I didn't think of that term, no. It looks like this might be a similar question. \$\endgroup\$
    – REMCodes
    Aug 4 at 20:59
  • 1
    \$\begingroup\$ Does your game need to continually update a UI element with the energy value? Or would the updated value only need to be displayed after the user sends input? \$\endgroup\$ Aug 5 at 3:51
  • \$\begingroup\$ Ideally, I would be able to increment a public variable every, say, 10 seconds in the background, so that I am able to receive input. \$\endgroup\$
    – REMCodes
    Aug 5 at 16:44
  • \$\begingroup\$ @DragonDePlatino it's a text based game without a UI, so I'd only need to get the value when the user enters a command \$\endgroup\$
    – REMCodes
    Aug 5 at 16:45

3 Answers 3

2
\$\begingroup\$

In a standard engine, dt represents the time in milliseconds since the last frame. In a text adventure, dt can represent the passage of time since the last command. The game loop should run as follows:

  1. Record the current time in milliseconds as start.
  2. Wait for user input.
  3. Record the current time as end.
  4. Calculate dt = end - start as your delta time.
  5. Process all in-game actors and timers using this dt.
  6. Process user's input.

It is critical that the user's input is processed after the game has ticked, otherwise the user can interact with things that have already expired (like shining a flashlight that has burned out, or cooking a hotdog over an extinguished fire).

An example in C#:

using System;
using System.Diagnostics;
using System.Collections.Generic;

public abstract class Actor {
    public abstract void Tick(long dt);
}

public class Campfire : Actor {
    private long life = 10 * 1000;
    
    public override void Tick(long dt) {
        life = Math.Max(0, life - dt);
        
        if (life > 5 * 1000) {
            Console.WriteLine("Your campfire is burning brightly.");
        } else if (life > 0) {
            Console.WriteLine("Your campfire is running low.");
        } else {
            Console.WriteLine("Your campfire burned out.");
        }
    }
}

public class Program {
    public static void Main(string[] args){
        var actors = new List<Actor>();
        actors.Add(new Campfire());

        Console.WriteLine("You stand in front of a campfire.");
        while (true) {
            // Fetch time and user input.
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            string command = Console.ReadLine();
            
            // Advance simulation.
            long dt = stopwatch.ElapsedMilliseconds;
            foreach (var actor in actors) {
                actor.Tick(dt);
            }

            Console.WriteLine("Command: " + command);
        }
    }
}```
\$\endgroup\$
4
  • 1
    \$\begingroup\$ "In a standard engine, dt represents the time in milliseconds since the last frame. In a text adventure, dt can represent the passage of time since the last command." I disagree. With this example, Console.ReadLine() is blocking, and so nothing will show if the user does not type anything. The player will never see that their camp fire is burning. The only message from it will potentially be "Your campfire burned out." if they start the game then go fetch something to drink. Plus, events will likely occur while they're not entering commands and miss them completely. \$\endgroup\$
    – Vaillancourt
    Aug 5 at 18:04
  • \$\begingroup\$ @Vaillancourt OP mentioned they'd only need to display values after commands with no live UI updates, so blocking for user input would be sufficient. \$\endgroup\$ Aug 5 at 18:15
  • \$\begingroup\$ You're right. They'll likely face that kind of issue eventually :) \$\endgroup\$
    – Vaillancourt
    Aug 5 at 18:17
  • \$\begingroup\$ I'm on mobile right now, but this seems like a pretty good solution. I will test it once I am at my computer and mark it as accepted if it helps. Thanks! \$\endgroup\$
    – REMCodes
    Aug 6 at 18:27
2
\$\begingroup\$

You can try adapting main loops from graphical video games.

Have your main loop run as often as your logic rate. At each iteration, test if you have received any input from the terminal. I do not know C#, but I found Console.KeyAvailable. Using this should be simple if your user input is line-buffered. If not, you'll have to buffer it yourself and react to it only once you receive a valid, full command.

Most text-based video games have a very fixed-function, procedural design, but such a loop will make it friendlier and easier to data-oriented designs instead. This will turn your game into a big state machine that loads the "plot" and scenes from some externally-defined data, rather than being embedded within the code. In other words, an engine.

This is how I would do it, were I building a text-based game from scratch. If this is too big a change for your already existing project, then threading is probably your next best option. One thread could handle user input, while the other performs asynchronous events. However you will need to update the rest of your code to prevent race conditions.

New contributor
mid is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
0
\$\begingroup\$

Speaking more to the any time consuming task part, one way is a time slice treatment. Where you a) limit the process to a fixed amount of time; b) when the threshold is reached push the process state onto a stack; c) then the next opportunity pop its state and give it another time slice.

Since I'm experimenting in Go goroutines, I use a context with Timeout on the group Wait. With the hope of preventing tasks from running away as a collection. Then I use a Ticker individually, to avoid one task from "starving" its siblings.

\$\endgroup\$
2
  • \$\begingroup\$ Note that this question is tagged C#, not Go \$\endgroup\$
    – DMGregory
    Aug 6 at 1:54
  • \$\begingroup\$ I was afraid to mention Go, but hoped the ideas and types would be a general enough. BUT that was a big assumption on my part..... \$\endgroup\$
    – 杜興怡
    Aug 6 at 2:06

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .