So far, there have been nine installments to this rambling series on creating a Pac-Man style game for an Arduino using the TVout video library. During the course of this discussion, many small things have changed so I thought it might be a good time to reboot the series. Here is a brief updated overview of the process so far.
0. Inspiration and Evolution
One night, two weekends ago, I decided to try the TVout video library for Arduino. I cut up an old A/V RCA cable and used two resistors to connect it to my UNO and soon was running the TVout demo. I next created a small program to draw a line, square, and circle. Then I decided to make the circle bounce around the screen like a ball, followed by bouncing multiple balls. I soon replaced the circle with a bitmap graphic so I could bounce a different shape, and ended up making that shape a Pac-Man which I could steer around the screen, avoiding the balls. Somehow this led me to decide to create a full-on Pac-Man style game.
1. Creating the Maze
I would be using the default TVout video resolution of 120×96. The original Pac-Man resolution is 224×288 and uses a monitor that is turned sideways (portrait mode) so it is taller than it is wide. If I just scaled that screen size down proportionally, it would become 75×96 and make for a very small maze.
I decided to take the approach used by most home versions that would be played on traditional monitors (landscape) and crop off the top and bottom area (score, lives left, level) so only the maze would be left. The maze alone was 224×248.
I began the process of drawing out the maze, pixel by pixel, on top of this scaled down graphic. As I did this, I had to adjust my maze size a bit so everything would line up. Some later research revealed that the original Pac-Man screen was divided up in to 8×8 tiles of 28 across and 36 down. (Or, when the top and bottom where cropped, 28×31.) In order to get 31 tiles vertically out of the TVout’s 96 pixel tall resolution, I did some simple math and divided the height of the TVout screen (96 pixels) by the number of tiles I wanted to display (31). 96/31=3.096. It seemed I would need to round to 3×3 tiles.
Going across the screen (28 tiles) at three pixels each was 84 (3*28=84). Going down the screen (31 tiles) at three pixels each was 93 (3*31=93). My scaled down Arduino maze would be 84×93, and that would allow it to be the same number of tiles as the original, just using smaller tiles.
I used a 3×3 grid to draw out the full maze, and took the liberty of making the side walls a bit wider than they really should be to make them look better. (If you look at the thick walls around the ghost house in the center, that is how the entire outer wall should be for the scaled down lines to be the same dimensions. That is the only thing about this maze recreation that isn’t dimensionally accurate.)
I used a website to convert my graphic file in to C data statements that could be displayed by the TVout bitmap library function.
I next created the ghost and Pac-Man character sprites to use in the game. The original arcade sprites were just under two 8×8 tiles wide and tall (less than 16×16 pixels) and would fit in the maze corridors. Since my corridors were now 5 pixels tall or wide (just under two 3×3 tiles) it seemed to work out perfectly to make my game characters 5×5. I designed Pac-Man with three frames (mouth closed, partially open and fully open) just like the arcade, and created versions for all four directions. For the ghosts, they had two frames of animation, and a version for all four directions (with the eyes facing that way). I drew these in a graphics program first, then hand entered them as data in the C source code.
2. Moving – some new stuff here!
Moving through the maze was a challenge. Originally I tried to detect walls by looking for set pixels, but I kept running in to places where the characters would get stuck at the rounded corners. After I learned about how Pac-Man used the tile system, that provided a nice and simple solution: I created a binary map that represented all of the tiles on the screen (28×31) and put a 1 where a wall would be and a 0 where a hall/path would be.
Once I had this, I worked out a system which would track which tile a sprite was in, and check for tiles around it to see if it could go in that direction. This is the part of the project that has evolved the most over the past week as I learned that tiles were also used for how the ghosts knew where to move (they head towards a specific tile), and how collisions between ghosts and Pac-Man were done.
Pac-Man considered a sprite to be in whatever tile the center of the sprite was in. Because of this, I would be tracking the X/Y coordinate of each object based on its center. In a 5×5 sprite, the center would be at (3,3) if we used base-1 and started with the top left pixel at (1,1) and the bottom right pixel at (5,5). In order to allow sprites to move smoothly, I would also track an X/Y offset within pixel. In the diagram to the right, the top ghost appears at the center of, for example, tile (10,10) with an X offset of 0. As it moves to the right (second ghost), it would still be in tile (10,10) but would have an X offset of 1. As it moves to the right again (third ghost), its center would now have it be in tile (11,10) with an X offset of -1 from that tile. Lastly (four ghost), it would be in tile (11,10) with an offset of 0.
I would need, at the very least, for each sprite to contain something like:
uint8_t x,y; int8_t xOffset, yOffset;
To figure out where on the screen to draw the sprite, I would take the tile coordinate, and multiple it by the size (3 pixels tall or wide), and then add the offset. In the above example, tile (10,10) with an X offset of 0 (in the center) would correspond to screen pixel (30,30). As the ghost moved to the right, it would be at (31,30) and (32,30). I also needed to add a few pixels to X and Y to compensate for the maze bitmap being a bit wider than the tiles under it. (NOTE: This is simplified. In the actual code, I also add half the width of the tile to keep the X/Y positions representing the center.)
To keep from having to do this kind of math (MAZEX+x*3+xOffset) every time an object was displayed, I thought I might also track the screen position separately:
uint8_t xScreen, yScreen;
I would need to do some testing and see if the overhead of the formula to calculate screen position every time was better than tracking two separate X/Y coordinates (one for tile, one for screen). Therefore, this approach is still something that could change. It would make the code seem simpler, but perhaps at a cost of extra CPU cycles needed (versus just having a variable around).
Objects also need to know what direction they were headed in. Initially, since I had started with a bouncing ball demo, I gave each object a directional offset variable that could either be +1, -1 or 0 depending on if the object was moving right/up, left/down or stopped. I was calling them “mx” and “my” (movement X, movement Y) and to move, I would just do something like:
x = x + mx; y = y + my;
As I got further in to development, I decided that since no object ever moved diagonally (thus, no mx=1 at the same time my=1), using two variables was not needed. Instead, I could just track the current direction with one, that would be set to UP, LEFT, DOWN or RIGHT:
And, because ghosts look one tile ahead to decide where they will turn when they get there, and because Pac-Man allows the player to push the joystick in a different direction as he is moving and instantly be ready to turn there, I added:
When I share the updated code, this will make more sense, and the new code should also be much simpler.
3. Ghost A.I.
With the basics of a maze and ability to move around it figured out, I then moved on to the logic of the ghosts. Pac-Man ghosts basically just try to move towards a target tile. That tile either causes them to scatter to a corner, or to try to track down Pac-Man, based on the mode the game is in.
To achieve this, I created some functions that let the ghost determine what it’s next direction should be at every intersection. It does this by comparing the tiles it could turn to and seeing which one is the closest to the target tile. Once I had targeting working, the only thing left to do would be add code to change the tiles as the game modes changed (from scattering to the corner, to chasing Pac-Man, to running away frightened after an energizer pellet was eaten).
There are also some special rules that needed to be taken care of, like the four passageways that ghosts are never allowed to turn up, and handling the side tunnels.
4. Dealing with Dots
A big portion of the project, currently left untouched, is figuring out how to handle all the dots on the screen. I have previously shared my idea on how this will be done, but I have yet to write any code to test this idea (this will be next). Once this is done, there will be very little left to do for the “engine” of this game to function.
5. Miscellaneous TO DO
The final part of this project will include coding some specific routines to handle things like:
- Scoring as Pac-Man eats dots.
- Collision detection between Pac-Man and Ghosts.
- Changing mode to FRIGHTENED when Pac-Man eats an energizer pellet.
- Displaying bonus fruit at appropriate times and scoring if eaten.
- Altering speed of Ghosts and Pac-Man based on levels and mode. Also, ghosts slow down in the tunnels, Pac-Man is slower while eating dots, and Pac-Man can slightly cut corners when turning.
- Intro screen, intermissions and high scores.
- …and probably many other things I haven’t even thought about yet. For instance, just yesterday, I saw a “cut scene” between levels that I had never seen before. I will have to find videos of all of them to try to recreate them.
And with that out of the way, it’s time to resume this project reboot, and share the current state of the code.
To be continued…