2
\$\begingroup\$

I'm currently in the process of making a game loop in a console application. I'm trying to make the game loop wait at the end of its loop for (1000*10000/fps) - ElapsedTicks to put a cap on game loop speed. For reference 1 millisecond is 10,000 ticks.

If the ElapsedTicks < FrameTimeTicks I execute await Task.Delay(FrameTimeTicks - ElapsedTicks) but this ends my console application.

The code responsible is the await Task.Delay so I assume I'm not using it properly.

My code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApp1
{
    class Program
    {
        static byte fps = 30;
        public static byte Fps { get { return fps; } set { fps = value; } }
        //user specified fps

        static long frameTimeTicks = (1000 * 10000) / Fps;
        static long FrameTimeTicks { get { return frameTimeTicks; } }
        //amount of ticks desired in one frame

        static bool draw = true;
        public static bool Draw { get { return draw; } set { draw = value; } }

        static long elapsedTicks;
        static long ElapsedTicks { get { return elapsedTicks; } set { elapsedTicks = value; } }
        //ticks elapsed in one game loop

        static void Main(string[] args)
        {
            GameLoop();
        }

        static void UpdateGame()
        {
            int a = 0;
            a++;
        }
        static void Render()
        {
            Console.WriteLine("@");
        }

        public static async void GameLoop()
        {
            Stopwatch gameLoopTicks = Stopwatch.StartNew();

            while (true)
            {
                gameLoopTicks.Restart();

                if (Console.KeyAvailable == true)
                {
                     char KeyPressed = Console.ReadKey().KeyChar;
                }
                UpdateGame();
                if (Draw == true)
                {
                    Render();
                }
                ElapsedTicks = gameLoopTicks.ElapsedTicks;
                if (ElapsedTicks > FrameTimeTicks)
                {
                    Draw = false;
                }
                else
                {
                    Draw = true;
                    await Task.Delay(TimeSpan.FromTicks(FrameTimeTicks - ElapsedTicks));
                }
            }
        }
    }
}

I Have also tried this variation but to the same result, my console application ends.

public static async void GameLoop()
    {
        Stopwatch gameLoopTicks = Stopwatch.StartNew();
        async Task PauseGameLoop()
        {
            //Console.ReadKey(); execution ends here!
            await Task.Delay(TimeSpan.FromTicks(FrameTimeTicks - ElapsedTicks));
            GameLoop();
        }

        while (true)
        {
            gameLoopTicks.Restart();

            if (Console.KeyAvailable == true)
            {
                 char KeyPressed = Console.ReadKey().KeyChar;
            }
            UpdateGame();
            if (Draw == true)
            {
                Render();
            }
            ElapsedTicks = gameLoopTicks.ElapsedTicks;
            if (ElapsedTicks > FrameTimeTicks)
            {
                Draw = false;
            }
            else
            {
                Draw = true;
                await PauseGameLoop();
            }
        }

Task.Delay MSDN, await MSDN. Am I tackling this problem the wrong way ? This post is why I chose this method. Thanks for the help!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

How do keep the program from exiting when there's an active async function running?

You GameLoop is async but your Main function is not. By default, starting a Task starts it on pooled thread. The minimal changes to avoid exiting Main are to return a Task and wait on it in Main:

static void Main() { 
   GameLoop().Wait();
}

public static async Task GameLoop() {
      .. same as before ..
}

A bigger issue will be that the resolution of the timer in Task.Delay is (system-dependent) probably only around 15ms, so you're not going to get very stable framerates. In fact, the resolution of sleeping a thread in general is going to be too low.

How to get consistent sleep times if Task.Delay isn't high-resolution enough?

For most consistent results, You'll want to look into implementing a fixed-timestep game loop where you just render as fast as possible (or perhaps spinwait any extra time away) or use VSync to naturally limit your loop speed.

The Go-to article on how to implement a fixed-timestep game loop has been Fix your Timestep, and there have been plenty of previous fixed-timestep questions asked here.

We take a page from the standard fixed-timestep pattern and wrap a main loop around a time-delta generating function. When that delta goes above our desired frameTime we call Update(). The rest of the loop consists of either a Thread.Sleep(1) if your platform has a high-resolution sleep timer or an empty loop while you're waiting for enough time to pass, to get a spinwait. In theory, you could use unsafe / unmanaged code to call the equivalent of _mm_pause to reduce your power consumption in the spinwait, but that's outside the scope of this answer.

using System;

class Program {
    public Program(double fps) {
        DesiredFPS = fps;
        frameTime = TimeSpan.FromSeconds(1.0 / fps);
    }
    double DesiredFPS;
    TimeSpan frameTime;
    // counts how many times we've looped while waiting
    int spinCounter = 0;
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

    // this is the function we run at the desired FPS
    void Update() {
        Console.WriteLine("{0}   {1}", spinCounter, sw.Elapsed);
        spinCounter = 0;
    }

    void MainLoop() {
      sw.Start();
      var last = sw.Elapsed;
      var update_time = new TimeSpan(0);
      while (true) {
        var delta = sw.Elapsed - last;
        last += delta;
        update_time += delta;
        while (update_time > frameTime) {
            update_time -= frameTime;
            Update();
        }
        spinCounter++;
        // On some systems, this returns in 1 millisecond
        // on others, it may return in much higher.
        // If so, you should just comment this out, and let the main loop
        // spin to wait out the timer.
        System.Threading.Thread.Sleep(1);
      }
    }

    static void Main() { new Program(30.0).MainLoop(); }
}
\$\endgroup\$
8
  • \$\begingroup\$ So the fact that the input values to Task.Delay is in the format TimeSpan ticks doesn't change the resolution of Task.Delay? I don't think console apps support vsync? \$\endgroup\$
    – Trr1ppy
    Commented Nov 3, 2017 at 1:30
  • \$\begingroup\$ Oh, if it's completely a console app, I suspect you will probably need to spinwait. msdn.microsoft.com/en-us/library/system.threading.spinwait.aspx \$\endgroup\$
    – Jimmy
    Commented Nov 3, 2017 at 1:33
  • \$\begingroup\$ Either way, the Delay/Sleep stuff is very platform dependent -- (on my machine, Thread.Sleep(1) has roughtly 1-2 millisecond response, for example. But the docs don't guarantee that holds true for all systems) \$\endgroup\$
    – Jimmy
    Commented Nov 3, 2017 at 1:41
  • \$\begingroup\$ Wouldn't the fact the Thread.Sleep(1) locks UI, eliminate that as a possibility or will the sleep time be small enough that it doesn't matter? \$\endgroup\$
    – Trr1ppy
    Commented Nov 3, 2017 at 9:37
  • \$\begingroup\$ Correction SpinWait method actually uses Thread.Sleep so I think the simplest option is to use Thread.Sleep(TimeSpan.FromTicks(FrameTimeTicks) - TimeSpan.FromTicks(gameLoopTicks.ElapsedTicks)); \$\endgroup\$
    – Trr1ppy
    Commented Nov 3, 2017 at 12:57

You must log in to answer this question.

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