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
\$\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 Nov 3 '17 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 Nov 3 '17 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 Nov 3 '17 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 Nov 3 '17 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 Nov 3 '17 at 12:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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