One of the best classic games from all time, created from scratch in full C# Console text based.
A couple of weeks ago I decided to start a new project, something that I could use to learn new things, and share my entire journey also while practicing my (basic) English writing skills. So, this article will cover the entire project, starting from the concept to the complete running game, and also as a bonus I’ll share the source code with you, so you can play and add your own fun inside this amazing project.
As first steps, I tried to materialize my ideas thinking abou the most important mechanics that this project should have, then I came with:
- 2D Interface (Text Only)
- Entities (Character, Ghosts, etc.)
- Controls (Character movement)
- Collision detection (ex: When a ghost hits the player)
- 2D Colored Interface
- Music & Sounds
Starting from scratch with a new completely empty C# Console project, I started testing some features that I was curious to figure out how would I make these things work.
As a first try, I started with .NET Core, trying to implement a more universal approach, so my game could run on Linux too.
Trying to implement a simple render method using Console.WriteLine() and a 2d character array was a nice progress. While implementing this, I discovered a simple way to initialize a 2d array in C# with Multidimensional Arrays which actually makes everything easier. Example of use:
int[,] array = new int[4, 2];
After some time writing to the console, I figured out some additional properties that made everything a bit more professional, like hiding the cursor and the scroll bar. And this is my result writing a 2D array where each line represents the Y axis and each character on that line represents the X axis.
So far so cool (maybe not yet), I decided to move forward and implement some basic player input, I came really fast with a major problem: Console inputs with .NET Core are very limited (obviously). When I tried to get the keyboard pressed keys with Console.ReadKey() the input I receive is different than expected, if the user just presses a key, it works normally, but if the user keeps holding a key, this method will receive different triggers telling that the user has released and pressed again that key without the user actually doing it.
I already was expecting these framework limitations but tried to see how far could I go, and that was it, then I changed the project from .NET Core to .NET Framework, so with this I was sure that I’d be able to implement (almost) everything that came to my mind like colors, music and some other stuff.
This was my result controlling my ‘0’ player after changing from Console.ReadKey() to Keyboard.IsKeyDown(Key.Up)
A fluid control experience while holding keys.
Now with a working base for rendering and controls it was time to define the game map with the player, objects and ghosts. I decided that the map should be easily edited and with that in mind I implemented a Scene class that was responsible for loading a .txt map file and store the game state with the current 2d grid state, positions and entities.
After defining some game constants, our base map.txt looks like this:
Of course, some implementations were still missing, like game fruits and other logic, but for this step it was going good enough.
So, for now the logic was still basic, load the text file and set each position of our grid (2d array) to the respective character in the text, then it’s time to implement something a little bit more complex. Let’s think about the game movements focusing on three objects, the player, score and ghost, when the player moves towards a score position, after he exits that position what character are we going to set in that position? It might look obvious, an empty space, for this case it works but what if the ghost is moving towards a score position and after it leaves that position? A general approach wouldn’t solve this, instead we need to store which object was in that position before, then we will create some ‘layers’ in each position and with that we can have a ordered list / sequence of objects that are in the same position and we’ll display only the top layer of each position and when anything moves we just move that top layer position in front of another position and automatically the previous position will display the correct character.
For this logic the LinkedList is the perfect choice to these ‘layers’ approach, I’ll not write about this data structure in this post but I strongly recommend that you read about if you’re not familiar with it.
Then our 2d char grid became a grid of LinkedLists and I created a base class (EntityBase) for every entity in the map, then the map loading sequence was the following:
- Fill every position of the grid with a LinkedList with only one entity: an empty space.
- Load the map.txt and add each character in top of the empty space in that position.
Called Tick inspired on Minecraft architecture, the tick is our main game loop that’s called N times a second and every tick updates the game state.
The s.Tick() method executes an update in every game entity and each entity decides what it’ll do (move, wait, etc), this could be a simple game loop solution as it makes the game have something near to 5ticks/s but only ignoring the loop execution time. Considering that at every single loop execution, the necessary time to finish all processing may vary, then our base isn’t going to work the same every time and at every environment, for this we need to implement a better approach, something that will take in consideration every tick execution time.
With this we can actually be more accurate as we try to make every tick last near the ‘delay’ variable. Using 500ms as delay we have something near to 2ticks/s.
Every Entity (Player, ghost, wall, space, score etc.) extends EntityBase and the EntityBase reserves an abstract method called Update() that every entity can override to make some action/movement within every game loop. Inspired on Unity MonoBehavior Update().
It was time to improve our graphics (or texts if you prefer), first trying to add some color property to each Entity object made the entire scene a bit more elegant (less boring).
YES, a small piece of life coming from nothing showed on these simple colors.
But that graphics weren’t looking ‘user-friendly’ then I started thinking about what could make this interface more beautiful, the first thing I tried was scaling, printing empty spaces after each pixel, what actually worked but created new problems.
The interface is now bigger but is not exactly what I expected, after some (long) adjusts trying to make the colors seamless between two pixels, made everything looks better. For this logic in each new generated scaled pixel, I compare the previous and next pixel’s color and if they are the same type and color we just print the scaled (empty space) with the same color in the middle, using this in both axis X and Y.
Challenges, games are based on challenges and until now our ghosts only know how to move randomly around the map, this logic works and it’s very simple but lets try to implement something better.
I’ll not focus on replicating the exact same behavior that the original game has, but instead use it’s core concepts and implement some features.
I modified the logic, now the ghosts can see objects near them, starting with a preset of the view distance of 5, they can see objects in each of their 4 directions (up, down, left and right), this means they can see everything that’s at the maximum of 5 pixels of distance and they can’t see trough other collision-enabled objects like a wall or other ghost.
The full logic is this:
- The ghost will look into all 4 directions and analyze everything that’s around him.
- If it sees the player, he’ll walk into the player direction.
- If it can’t see the player, he’ll get all the available directions to go.
- It memorize where it came from, if there is more than one direction available, it ignores the direction where it came and use others instead, it only will go back if the direction that it came from is the only available direction.
- If there are multiple available directions after all filters, it chooses one randomly.
- If no direction is available, it doesn’t move and keeps idle.
After adding some scene transitions between the start and dying, and adding some other essentials, this was my final result:
It was better than I could imagine, of course there is some features that’s still missing, logic, musics, effects and animation, maybe a better GUI with score count, but the next features I’ll let up to you decide and implement by your own, so this is my invite for you to play the game and edit the source code to help me make it better.
Also there is a lot of other structures that I implemented on this project and I’m not mentioning here tring to keep focusing just on the most important features, so it will be nice if you view the code itself to see everything that it does.
If you want to edit the map, download the game and edit the file map.txt
Download Project: https://github.com/fabio-stein/PacManConsole
Thanks for reading, if there’s something I missed or you want to contact me: