Console Games – Snake – Part 3 (Introducing a game timer)

The console snake game is progressing well. Based on where we got to on the last post, we had a game where the snake itself was behaving more or less as expected. The next task is to plant some food. In order to plant the food, we’re going to need a game timer.

What is a game timer?

It’s important to remember here that we’re using this as a teaching device, so trying to introduce something like a System.Threading timer is not going to work because it’s complicated to explain; additionally, one thing that I’ve learned from the small amount of game development that I’ve done is that the more control you have over your threads, the better. Since we already have a game loop, let’s just use that. We currently have a function to accept user input and a function to update the screen; this time we need a function to update the game variables:

        private static DateTime nextUpdate = DateTime.MinValue;
        private static bool UpdateGame()
        {
            if (DateTime.Now < nextUpdate) return false;

            nextUpdate = DateTime.Now.AddMilliseconds(500);
            return true;
        }

Notice that we have an update variable to store the next update, and return a flag where we do update. The Main function would handle this like so:

        static void Main(string[] args)
        {
            Console.CursorVisible = false;
            DrawScreen();
            while (true)
            {
                if (AcceptInput() || UpdateGame())
                    DrawScreen();                
            }
        }

So far, nothing concrete has changed. Let’s use our new function to add some `food`. This is actually quite involved, because we need to translate Position to use a class, rather than a struct; here’s why:

        private static DateTime nextUpdate = DateTime.MinValue;
        private static Position _foodPosition = null;
        private static Random _rnd = new Random();
        private static bool UpdateGame()
        {
            if (DateTime.Now < nextUpdate) return false;

            if (_foodPosition == null)
            {
                _foodPosition = new Position()
                {
                    left = _rnd.Next(Console.WindowWidth),
                    top = _rnd.Next(Console.WindowHeight)
                };
            }

            nextUpdate = DateTime.Now.AddMilliseconds(500);
            return true;
        }

We need to be able to signify that the food is nowhere (at the start, and after it’s eaten). I tried to avoid bringing in classes at this stage, because they add complexity to an already complicated change; however, this seemed the cleanest and easiest solution at this stage.

There’s some other changes to allow for the change to a class from a struct:

        private static bool AcceptInput()
        {
            if (!Console.KeyAvailable)
                return false;

            ConsoleKeyInfo key = Console.ReadKey();

            Position currentPos;
            if (points.Count != 0)
                currentPos = new Position() { left = points.Last().left, top = points.Last().top };
            else
                currentPos = GetStartPosition();

            switch (key.Key)
            {
                case ConsoleKey.LeftArrow:
                    currentPos.left--;
                    break;
                case ConsoleKey.RightArrow:
                    currentPos.left++;
                    break;
                case ConsoleKey.UpArrow:
                    currentPos.top--;
                    break;
                case ConsoleKey.DownArrow:
                    currentPos.top++;
                    break;

            }

            points.Add(currentPos);
            CleanUp();

            return true;
        }

This is because structs are immutable; meaning that we can take one, change it and add it to a collection without issue; but do that with a class and it changes the copied class.

We need to change the DrawScreen method to display the `food`:

        private static void DrawScreen()
        {
            Console.Clear();
            foreach (var point in points)
            {
                Console.SetCursorPosition(point.left, point.top);
                Console.Write('*');
            }

            if (_foodPosition != null)
            {
                Console.SetCursorPosition(_foodPosition.left, _foodPosition.top);
                Console.Write('X');
            }
        }

Finally, the snake now needs to move based on the game timer. First, refactor the section of `AcceptInput` that actually moves the snake:

        private static bool AcceptInput()
        {
            if (!Console.KeyAvailable)
                return false;

            ConsoleKeyInfo key = Console.ReadKey();

            Move(key);

            return true;
        }

        private static void Move(ConsoleKeyInfo key)
        {
            Position currentPos;
            if (points.Count != 0)
                currentPos = new Position() { left = points.Last().left, top = points.Last().top };
            else
                currentPos = GetStartPosition();

            switch (key.Key)
            {
                case ConsoleKey.LeftArrow:
                    currentPos.left--;
                    break;
                case ConsoleKey.RightArrow:
                    currentPos.left++;
                    break;
                case ConsoleKey.UpArrow:
                    currentPos.top--;
                    break;
                case ConsoleKey.DownArrow:
                    currentPos.top++;
                    break;

            }

            points.Add(currentPos);
            CleanUp();
        }

Next, we’ll just cache the key input instead of actually moving on keypress:

        static ConsoleKeyInfo _lastKey;
        private static bool AcceptInput()
        {
            if (!Console.KeyAvailable)
                return false;

            _lastKey = Console.ReadKey();

            return true;
        }

And then handle it in the UpdateGame() method:

        private static bool UpdateGame()
        {
            if (DateTime.Now < nextUpdate) return false;

            if (_foodPosition == null)
            {
                _foodPosition = new Position()
                {
                    left = _rnd.Next(Console.WindowWidth),
                    top = _rnd.Next(Console.WindowHeight)
                };
            }

            Move(_lastKey);

            nextUpdate = DateTime.Now.AddMilliseconds(500);
            return true;
        }

Next time, we’ll manage eating the food and collision detection.

GitHub

For anyone following these posts, I’ve uploaded the code so far to GitHub:

Git Hub Repository

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.