Wednesday, October 21, 2026

A sample game: making Pac-Man from scratch

        With working graphics, sound, and a fast Basic interpreter, I needed a real demonstration program to show what the system could do. I decided to re-create a version of Pac-Man, similar to the original arcade game but with a few tweaks and modifications.


        First, I set about re-creating the game map, using one of the tile engines. The original playfield graphics were easy to find online, already in the form of 8x8 tiles which my tile engine code could easily work with. My tile engine only needed a few tiles to build up the entire maze, as the per-tile flip and rotate feature greatly reduced the amount of raw graphics needed.

 The dozen or so map tiles needed, in a simple tile editor program I created for this project.

        The shape of the map would not be the same, as the original Pac-Man game had a vertically oriented screen, while I was working with a conventional horizontally oriented screen. Rather than try to use the original playfield map, I decided to just create a new horizontally oriented map for my game. And to be extra fancy, I made the map slightly different every level with a bit of procedural generation. Most of the playfield map would stay the same, but I added a few dozen spots which change with each level. In all there would be several thousand slightly different playfields which the player might encounter while playing the game. The base maze outline was actually saved in a binary file on disk and loaded into memory as needed, as was the bitmap graphics for the tiles. These could have been stored in the Basic program itself in data statements, but that would have made the Basic program listing much longer.



One of many procedurally generated map screens

        After drawing in the walls of the playfield maze and filling in the level-specific custom details, the code places the four power pellets and then floods the remaining corridor areas with the regular pellets. This does mean that the number of pellets that need to be eaten varies slightly from level to level. It also avoids the level 256 kill screen present in the original arcade game.


        Next, I needed to create the sprites. All of the sprites in this game are 16x16 pixel, 256 color mode sprites. 16K of sprite graphics space gives me 64 different images to use, which was just barely enough for this game.

All of the sprite graphics needed for the game

        I did take advantage of the flip and rotate capabilities in my sprite engine to avoid needing graphics for each different direction of Pac-Man and the ghosts. I also used the sprite animation to cycle between sprite images to animate the ghosts. Later I added an optional 16 color sprite mode with per-sprite palettes, which could have been used to save memory by using the same basic sprite graphics for all 4 ghosts, and also increased the available sprite graphics space to 64K, but I didn't end up needing that for this project.

        Movement of Pac-Man unfortunately had to be done with the keyboard, since I have yet to add a joystick port to this machine. It’s good enough for demonstration purposes. I ended up coding a general movement routine that applies to all movable objects in the game which locks them to moving only along lines between the centers of tile spots and through open spaces in the maze, with some special logic for using the tunnels off the sides of the map. Pac-Man moves according to these limits, with a target direction value determined by the keyboard inputs.

 Moving around the maze and eating pellets.

        The ghosts are also locked to moving along open spaces in the maze, with some special case logic for entering and leaving the ghost house in the center. They all have a target location which they try to move towards. Initially, after leaving the ghost house, they are all in “scatter mode”, where each one moves towards a different corner of the maze. After some time in scatter mode, they switch to chase mode. In this mode each ghost has a slightly different behavior, having a target point which will be the player, a point in front of the player, a point behind them, or alternating between the player’s location and the far corner of the map. This is the same chase AI as in the original arcade game. After the player eats a power pellet, the ghosts change appearance and then attempt to move directly away from the player if possible. Finally, after a ghost has been “eaten”, its target location will be set to the center of the maze to return it to the ghost house.

The game gets much harder once the ghosts have been added.

        I also added a moving bonus item that bounces as it travels from one side of the arena to another, as was seen in Ms. Pac-Man. It just seemed more fun than just having it appear in the middle of the arena.
To recreate the sound of the original game, I found a You Tube video of a long gameplay with good audio, fed the audio into spectrum analyzer software, and then spent a while working out the exact frequency and timing of the various sounds. The cross-channel modulation features of my audio generation code were very useful here, allowing me to recreate the background siren sound, the rising whoops when the player eats a ghost or bonus item, and other complex sound effects with just a few lines of Basic. Some of the sound effects don’t exactly match the arcade game original, as the original arcade game used a custom sound generator chip with complex wavetables that I couldn’t easily reproduce. The original “waka” sound from eating a pellet is especially difficult to reproduce, and my code doesn’t get it exactly, but it works well enough for demonstration purposes.

Apologies for the handheld camera work. I haven't managed to get the video capture device to work with this setup yet.

        This game is missing several features of the original arcade game. There are no cutscenes. There’s no attract mode, running the program starts a new game instantly. And getting a game over simply locks the game up entirely, forcing the user to exit and then run the program again.


        It’s a pretty fun game. I find it actually fairly difficult, but I’m not sure if that’s due to my reflexes being poor, or if I’ve set the speed on the ghosts to be too high. The main thing I’m proud of is that the somewhat complex game logic runs smoothly at 60hz, despite still being essentially interpreted Basic code.


        The Basic source code is available here, but obviously this is only going to run on my own custom hardware, so it’s provided purely for reference purposes.


        While writing this code the main problem I ran into was the limited memory available for sprites. 16K is really not much space for bitmaps, especially when they’re being stored in full 256 color mode. I did later implement an option for 16 color mode to effectively double the space available for sprites, and also later added some options to potentially increase the available sprite bitmap space to as much as 64K by disabling the other graphics engines and using their memory for sprite bitmaps instead. Ultimately I’m limited by the small amount of memory available on the FPGA, and my choice to try and implement this graphics engine with a single chip solution. Adding an external memory chip would fix the limited memory availability, of course, but that would be an entirely different design than what I’m going for.


Sound generation

        With the graphics engine coming along well, I turned to the second major task this FPGA would need to do: audio generation. The typical method to get audio out of a retrocomputer project like this would be to use something like a AY-3-8910 sound synthesis chip, but I didn’t have any of those on hand and wanted to try making my own audio generator from scratch. The preferred method with modern sound generation is to just play back digitized sound clips, but I didn’t have enough block RAM left over on the FPGA for that, and it would put too much load on the external CPU anyway. Instead, I decided that I wanted to implement a fairly standard wavetable synthesis system, inspired by the AY-3-8910, the Commodore SID, and other classic sound generators.

      Those early classic sound chips were generally good for 3 channels of sound at most, with completely separate sound generation circuitry for each channel. I decided for a more ambitious design, a synthesizer capable of handing up to 16 channels simultaneously. To avoid having to have an unreasonable amount of logic for each channel, the audio engine would operate serially, cycling through the 16 channels and performing all the math required for each one at a time.

Audio engine block diagram

        The audio engine starts with a memory interface block, which loads all of the registers required for a channel from the audio control RAM. Each channel has an associated 32 byte section of the control ram, which is loaded into temporary registers which control the audio engine. After the required math has been run for that channel, 9 bytes will be written back to the audio RAM, containing data that needs to be updated with each cycle. As all 16 channels are processed, right and left channel output values are added to a cumulative sum, which is then sent to the I2S audio output block after all 16 channels are done. The entire cycle repeats on a 48khz clock.

        Each channel starts with a 24 bit phase register, which is updated by a 24 bit frequency control register to set the note frequency. 24 bits of frequency control allows a wide range of frequencies for each channel, giving a maximum frequency of 12mhz and a minimum of somewhere around 1/700hz. Being able to go to frequencies that low might seem pointless, but I wanted the channels to be also used as low-frequency oscillators for cross-channel modulation purposes. It also allows a full 9 octave note range with good precision.

        There is a cross-channel modulation function, which allows each channel's frequency, amplitude, or duty cycle to be adjusted by the output from another channel. This allows the user to set up complex effects such as sirens, warbling sounds, randomly varying notes, rising/falling noise to simulate waves or wind, etc., all without the external CPU having to do anything other than the initial register configuration. The resulting system has a lot of flexibility, and the 16 channels give a lot of spare channels to use as oscillators.

        Each channel can be set to output random noise, square waves, triangle/saw waves, sine waves, or one of four custom waveforms. The triangle/saw tooth wave generation logic supports smoothly transition between falling sawtooth, triangle, and rising sawtooth waves with a bit of clever math. The noise, square, triangle and saw waves are generated mathematically on the fly, while the sine wave and custom waveforms are stored in small lookup tables in memory. Each channel also has an independent ADSR envelope generator, which optionally shapes the volume output for each channel’s audio output.

        The left and right channel outputs from each channel are all summed together, and then output on an I2S port after all 16 channels have been updated. An external amplifier takes the I2S input and drives a speaker. I had originally experimented with a R2R DAC, but it didn’t work well. The I2S amp seems to be a cleaner solution anyway.

Devkits now with added I2S amp and speaker.
(and also a USB hub, 8 digit LED display, and battery backed RTC chip) 

        While I am not an audio expert by any means, the resulting sound synthesis seems good to me. Quite capable of generating the types of sound effects that you’d get out of a mid 80’s game system. I am sure that someone with more musical aptitude than I could do a lot with this audio generator.