9

I am trying to build an interface for a game. The game runs for 1 minute. The GetStop method stops after 60 sec game. The play method starts the game and the quit method quit the game. Now ideally what I want is when I quit the game after 30 seconds, the timer should get reset and on click of the Play button, the timer should again run for 1 minute. So that the next game gets to run for 1 minute. If I press Quit button then again, the timer should be reset for the next game.

However, there seems to be a certain issue in my code. Whenever I execute quit method the timer seems to be saved at that state. So, If I quit a race in 30 seconds then the next race will last for only 30 seconds. If I quit a race in 50 seconds, the next race will last only 10 seconds. Ideally, the timer should get reset but it is not getting reset.

I am out of ideas here. Can anyone please provide some suggestions??

private async Task GetStop(CancellationToken token)
{ 
    await Task.Run(async () =>
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(TimeSpan.FromSeconds(60), token);

        token.ThrowIfCancellationRequested();
        if (!token.IsCancellationRequested)
        {
            sendMessage((byte)ACMessage.AC_ESCAPE); 
        }
    }, token);
}

public async void Play()
{         
        sendMessage((byte)ACMessage.AC_START_RACE); 
        _cts.Cancel();

        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
        _cts = new CancellationTokenSource(); 
        await GetStop(_cts.Token);
   }

public void Quit()
{
        _cts.Cancel();
        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
    //
}
AndrejH
  • 2,028
  • 1
  • 11
  • 23
David Silwal
  • 581
  • 5
  • 18
  • 4
    I don't know if I understood your question right, but maybe you think that cancelling a `Task` stops the process from running. This is not true. Only the task itself as a wrapper for the process will stop waiting for it and the rest of your code continues including the process 'inside' the task. – Silvermind Jul 02 '18 at 07:22
  • 2
    Your code can only stop where you let it. So, for example, if you put in a 5 minute sleep, that sleep will continue.. What you need is your game loop to have time as part of its conditions to update/process.. Instead of calling "getstop", at the start set a variable to now+ and then check now < timespan – BugFinder Jul 02 '18 at 07:29
  • Thank you , could you please send me an example or reference link to follow? – David Silwal Jul 02 '18 at 11:57
  • You will probably not be happy with a solution to your question here if you handle your game loop with Tasks. Have a look at [this description](https://stackoverflow.com/questions/17440555/using-timer-and-game-loop/17440807#17440807) of how a game loop normally works. – nvoigt Aug 13 '18 at 08:27

2 Answers2

2

I can see that your code may throw exceptions at several places. If you are catching and ignoring all exceptions, you may not be able to see the reason why the time, cancellation token and tasks are not working correctly.

At a first moment, I could identify the following:

private async Task GetStop(CancellationToken token)
{ 
    await Task.Run(async () =>
    {
        // I think you don't need to throw here
        token.ThrowIfCancellationRequested();

        // this will throw an Exception when cancelled
        await Task.Delay(TimeSpan.FromSeconds(60), token); 

        // again, I think you don't need to throw here
        token.ThrowIfCancellationRequested();

        if (!token.IsCancellationRequested)
        {
            sendMessage((byte)ACMessage.AC_ESCAPE); 
        }
    }, token);
}

public async void Play()
{         
        sendMessage((byte)ACMessage.AC_START_RACE); 

        // at some scenarios this may be null
        _cts.Cancel();

        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
        _cts = new CancellationTokenSource(); 
        await GetStop(_cts.Token);
   }

public void Quit()
{
        _cts.Cancel();
        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
}

I created a Console application, did some small modifications, and here it seems to work just fine. Please take a look:

public static class Program
{
    public static void Main(string[] args)
    {
        var game = new Game();

        game.Play();
        Task.Delay(5000).Wait();
        game.Quit();

        game.Play();
        Task.Delay(15000).Wait();
        game.Quit();

        game.Play();
        Task.Delay(65000).Wait();

        Console.WriteLine("Main thread finished");
        Console.ReadKey();

        // Output:
        //
        // Start race (-00:00:00.0050018)
        // Quit called (00:00:05.0163131)
        // Timeout (00:00:05.0564685)
        // Start race (00:00:05.0569656)
        // Quit called (00:00:20.0585092)
        // Timeout (00:00:20.1025051)
        // Start race (00:00:20.1030095)
        // Escape (00:01:20.1052507)
        // Main thread finished
    }
}

internal class Game
{
    private CancellationTokenSource _cts;

    // this is just to keep track of the behavior, should be removed
    private DateTime? _first;
    private DateTime First
    {
        get
        {
            if (!_first.HasValue) _first = DateTime.Now;
            return _first.Value;
        }
    }


    private async Task GetStop(CancellationToken token)
    {
        await Task.Run(async () =>
        {
            try
            {
                // we expect an exception here, if it is cancelled
                await Task.Delay(TimeSpan.FromSeconds(60), token);
            }
            catch (Exception)
            {
                Console.WriteLine("Timeout ({0})", DateTime.Now.Subtract(First));
            }

            if (!token.IsCancellationRequested)
            {
                Console.WriteLine("Escape ({0})", DateTime.Now.Subtract(First));
            }
        }, token);
    }

    public async void Play()
    {
        Console.WriteLine("Start race ({0})", DateTime.Now.Subtract(First));

        CancelAndDisposeCts();

        _cts = new CancellationTokenSource();
        await GetStop(_cts.Token);
    }

    public void Quit()
    {
        Console.WriteLine("Quit called ({0})", DateTime.Now.Subtract(First));
        CancelAndDisposeCts();
    }

    private void CancelAndDisposeCts()
    {
        // avoid copy/paste for the same behavior
        if (_cts == null) return;

        _cts.Cancel();
        _cts.Dispose();
        _cts = null;
    }
}

I would also suggest to take a look on System.Threading.Timer, maybe if can be useful for some scenarios...

Good luck with your game!

Anderson Rancan
  • 366
  • 5
  • 13
1

For my own purposes I created a wrapper called CancellableTask which might help you achieve what you want. You can create the task using by passing a delegate as a parameter to the constructor, then you can Run it with delay or without. It can be Canceled at any time, either during the delay or while it's running.

Here's the class:

public class CancellableTask
    {
        private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        private Task cancellationTask = null;
        private Action<Task> method;
        private int delayMilis;

        public bool Delayed { get; private set; }

        public TaskStatus TaskStatus => cancellationTask.Status;

        public CancellableTask(Action<Task> task)
        {
            method = task;
        }

        public bool Cancel()
        {
            if (cancellationTask != null && (cancellationTask.Status == TaskStatus.Running || cancellationTask.Status == TaskStatus.WaitingForActivation))
            {
                cancellationTokenSource.Cancel();
                cancellationTokenSource.Dispose();
                cancellationTokenSource = new CancellationTokenSource();
                return true;
            }
            return false;
        }

        public void Run()
        {
            Delayed = false;
            StartTask();
        }

        public void Run(int delayMiliseconds)
        {
            if(delayMiliseconds < 0)
                throw new ArgumentOutOfRangeException();

            Delayed = true;
            delayMilis = delayMiliseconds;
            StartDelayedTask();
        }

        private void DelayedTask(int delay)
        {
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            try
            {
                cancellationTask =
                    Task.
                        Delay(TimeSpan.FromMilliseconds(delay), cancellationToken).
                        ContinueWith(method, cancellationToken);

                while (true)
                {
                    if (cancellationTask.IsCompleted)
                        break;

                    if (cancellationToken.IsCancellationRequested)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                //handle exception
                return;
            }

        }

        private void NormalTask()
        {
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            try
            {
                cancellationTask =
                    Task.Run(() => method, cancellationToken);

                while (true)
                {
                    if (cancellationTask.IsCompleted)
                        break;

                    if (cancellationToken.IsCancellationRequested)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                //handle exception
                return;
            }
        }

        private void StartTask()
        {
            Task.Run(() => NormalTask());
        }

        private void StartDelayedTask()
        {
            Task.Run(() => DelayedTask(delayMilis));
        }

    }

And it can be used like this:

var task = new CancellableTask(delegate
            {
               DoSomething(); // your function to execute
            });
task.Run(); // without delay
task.Run(5000); // with delay in miliseconds
task.Cancel(); // cancelling the task
AndrejH
  • 2,028
  • 1
  • 11
  • 23