CC Blog Projects Research & Design Hub

Video Game Inspired by The Legend of Zelda

Implemented on a Raspberry Pi Pico MCU

The Legend of Zelda is a classic video game from the 1980s. In this article, three Cornell University students describe how they created a video game that emulates the aesthetic and gameplay of the original Zelda game, complete with animated movements, and custom VGA graphics and sound effects, using an RP2040.


  • How to build a video game
  • RP2040 chip
  • Raspberry Pi Pico MCU
  • Raspberry Pi | www.raspberrypi.com

The Legend of Zelda (“Zelda”) is a series of action-adventure video games originally published by Nintendo in the 1980s; it is considered one of the greatest or most influential video games of all time, and the progenitor of modern action role-playing video games [1]. Using a Raspberry Pi Pico development board, featuring the RP2040 chip, we implemented a Zelda-style video game as a final project for Cornell University’s ECE 4760 course: Digital Systems Design Using Microcontrollers. This project-based microcontrollers design course concludes with a 4 week independent design project.

https://youtu.be/m-djPeuXcxA

Our game emulates the aesthetic and gameplay of the original Zelda game, complete with animated movements, custom VGA graphics and sound effects, and a nine-screen-long demonstration level. Players control their avatars’ movements and interactions with a keyboard. Each player has to navigate the level that can only be won by finding a weapon, solving a puzzle, and defeating enemies.

When the game starts, the player is presented with a set of instructions describing how to control the avatar. A story unfolds as the player moves through the game. The first screen of the level contains a bed and sign telling the player how to pick up gems and to find a weapon. The next screen contains a sword that the player must collect before continuing to areas containing enemies. After collecting the sword, the players are able to clear a screen of monsters, only to find they cannot pass without paying a villager a considerable sum of gems. They must solve a puzzle so they can collect sufficient gems to continue. After paying the villager, the player must defeat a “boss fight,” consisting of two stronger enemies, to access the end screen.

HARDWARE OVERVIEW

The Raspberry Pi Pico connects to three external devices: a VGA screen to display the game, a speaker, and a UART channel to a serial client.

The VGA screen connects to the RP2040 via six GPIO ports, which govern the timing and color of the display. The HSYNC and VSYNC lines are driven by a custom VGA driver developed by course staff for our class, but we modified the color connections to the RP2040 to allow for use of 4-bit color. This gave us access to a greater suite of colors for our designs. The fourth bit doubles the resolution of the GREEN line, so while each of the RED and BLUE VGA lines have a 330Ω resistor in series (which, in addition to an internal 70Ω resistor, pulls the color line to a safe voltage), an R2R DAC (digital-to-analog converter) is required for the GREEN VGA line, shown in Figure 1.

FIGURE 1 
Breadboard diagram of connections to Raspberry Pi Pico
FIGURE 1
Breadboard diagram of connections to Raspberry Pi Pico

We generated sounds by means of the MCP4822, a 12-bit DAC with which we communicate through an SPI channel. For the speaker circuit, the following four signals were connected from the serial peripheral on the RP2040 to the DAC IC: serial clock, serial data, latching input, and chip select. Other I/O for the DAC IC included power and ground, and two analog output channels (identified by A and B). Each of these output channels could be wired to a 3.5mm headphone jack receptacle, which allowed us to connect a speaker system to the DAC. A breadboarding diagram and a full schematic of the hardware, are shown in Figure 1 and Figure 2, respectively.

FIGURE 2 
Full schematic
FIGURE 2
Full schematic
PLOTTING TO VGA SCREEN

The main constraint with the project was the limited RAM for displaying VGA frames. Any asset or level design needed to be coded in flash, but also needed to be written to the VGA screen with minimal RAM utilization. These asset and level designs needed to be detailed to replicate the original NES (Nintendo Entertainment System) game. This meant that each tile would be 16 x 16 blocks, and each level would contain 16 x 11 tiles. To make the game more visually appealing, each block would be 2 x 2 pixels on the VGA screen, as shown in Figure 3.

FIGURE 3 
A indicates one pixel, B indicates one block, C indicates one tile
FIGURE 3
A indicates one pixel, B indicates one block, C indicates one tile

Writing to the VGA screen needs to be fast and smooth, so the framework of the VGA driver developed by V. Hunter Adams and Bruce Land [2] is used as a starting point. The driver interacts with the VGA outputs through a PIO state machine configured on the Pico to send data at the frequency required by the VGA screen, and a DMA channel is responsible for moving data to the screen. The data is encoded in a byte array of length 153,600 (half the number of pixels on the screen). Each byte in the array holds color information for two adjacent pixels.

Updating VGA pixel memory one pixel at a time would be far too slow, and the delays and stutters of the background would be significantly more noticeable, so multiple pixels needed to be changed at one time. The designs for each tile are stored in flash as hex values, doubled across each column, but computationally duplicated across two rows to create a 2 x 2 block. Each tile is a two dimensional long array. A pointer traverses the array, copying memory appropriately to where they are supposed to appear on the screen (Figure 4).

FIGURE 4 
VGA plotting software diagram
FIGURE 4
VGA plotting software diagram

To write the main character, enemies, or entities that sit “on top” of the background, “transparent blocks” need to be considered. The designs need to be hard coded, but there is no way to hard code transparency, since every hex value corresponds to a color, and even if a value were to be assigned, the computation for looking for that value would be too expensive. The alternative was to use a “mask” for every transparent design that specified which tiles were “transparent” and which were not. This mask is compared against the existing tile and inverted to compare against the design, to determine which bits need to be from the background and which from the design. The result is copied to the VGA screen (Figure 5).

FIGURE 5 
Example mask and how it overlays with a character
FIGURE 5
Example mask and how it overlays with a character

To store the tiles and use them in a clean way, the pointer to the start of the design or mask array is in an array, where the array stores the design and mask for the character in any state it might assume. This means that in its current state, it stores the two frames for the walking animation in any direction and the attacking frames. These are then used through an enum to index into the array. However, for characters such as enemies, there will be multiple enemy types, each of which has a direction design. This means we need a pointer to the aforementioned array of pointers, to specify which enemy is being drawn. These are indexed and read through pointer arithmetic, taking advantage of the fact that arrays store memory contiguously. This was implemented to be as conservative with flash memory and RAM as possible, as well as the flexibility of changing, adding, and deleting designs.

GAME ENGINE

The game engine is designed to emulate the original Legend of Zelda. The RP2040 is dual core, so the Protothreads library [3] [4] is integrated for thread management. This library is specifically designed to be lightweight for memory-constrained systems. Since the user plays the game through a serial monitor, an entire core was used to read the input of the player. The other core available is used for game logic and graphics. Another thread on that core is used for periodic events. The game is designed such that the serial thread sets flags for the intended actions of the player and small commands, while the other core is responsible for any major computation.

Periodic events include cooldowns for attacking, when to move after attacking, updating enemy movements, handling how fast the game processes serial inputs, and how long to draw things like the death cloud for when enemies die. The graphics thread is responsible for drawing the background, what is “on top” of the background, the interface, and anything else that needs to appear on the screen. Notably, this does not include computation of new screen changes or erasing the previous screen contents.

This game engine design prioritizes player experience. A game that is unresponsive or feels inconsistent can ruin the entire game experience, regardless of how much fun it is. This design decision was informed by making sure the players have agency over their characters and can smoothly handle the characters within the constraints of the actual game.

Game Logic: The major implementations of game logic came in the form of obeying terrain, “hitbox/hurtbox” detection (discussed below), and interacting with the environment.

Since there are technically no set boundaries on which areas are and are not walkable, the character’s movement restrictions cannot be generated from the screen design. The terrain collision is stored through a field that contains information on whether the character can move into a new global tile or not in each of the cardinal directions. An additional mask is required to determine which tiles are traversable and not traversable. The terrain collision value is generated by checking this mask with the character’s current position, and also when the character collides with entities such as signs and boulders.

In gaming, hitboxes and hurtboxes are collision checks that let you determine when objects contact or overlap with one another—in our case, when a character gets hit. The hitbox is the “attack,” and will damage a character when it intersects with the character’s hurtbox. Every time the game state thread is updated, a hitbox/hurtbox detection is run for all enemies on the screen; this indicates in which direction the main character has been hit, if applicable. Examples of hitboxes and hurtboxes are shown in Figure 6.

FIGURE 6a
Examples of hitboxes and hurtboxes
FIGURE 6a
Examples of hitboxes and hurtboxes
FIGURE 6b
Main character tile
FIGURE 6b
Main character tile

To allow the player to react accordingly, the main character gets pushed back a certain number of blocks in the opposite direction from which it was hit. To detect the player attacking the enemy, it uses the location and size of the sword to check against every enemy, and if any are alive and the rectangles intersect, it will return the index of the enemy. A health index in the number of hearts is maintained for each enemy, which is decremented upon hit. Since enemies don’t have a weapon, their hitbox and hurtbox depend on the size field of their character. The main character’s entire model is the hurtbox, and the hitbox is a disjointed sword that adjusts its hurtbox size depending on which direction the player attacks.

Enemy pathfinding is partly random, but generally moves toward the player. The distance between the enemy and the main character determines how likely it is that the enemy will move in that direction toward the player, whenever the enemy does move. This means that if the enemy is close on the y-axis but distant on the x-axis, it is much more likely for the enemy to move toward the player by moving closer in the x-axis direction.

There are two ways to interact with the environment: running over passive objects or interacting with active objects. Passive objects are items such as gems and hearts, which don’t require the player to interact, and accept touch with the character. Active objects—such as boulders, signs, and villagers—all need to interact with the state of the game when the player chooses to interact with them. Upon each update of the game state, the thread checks for passive objects. The bounding boxes of the character and any object are used to evaluate contact. This is also used to determine the ability for the player to interact with nearby objects, for example pushing a boulder.

VISUAL ASSET DESIGN & IMPLEMENTATION

Designing the visuals of the game includes both tiles (16 blocks x 16 blocks) and levels (11 tiles x 16 tiles). We did not directly use the assets from the original Zelda game; however, many of our designs were inspired by Zelda or other video games. Designs are hard coded into flash memory that can be directly fed to the VGA driver. There are essentially two steps to each element: 1) visually lay out the desired appearance of the tile or level; and 2) encode the design into the game program.

Tiles can be designed visually using a spreadsheet program (we used Google Sheets) adjusted such that the width of the columns and the height of the rows are the same (that is, each block is a cell of the spreadsheet). It is helpful to enter the approximate hex value for the 16 possible colors of the VGA screen as custom colors, to best approximate what the design will look like on the screen. Then one can block off 16×16 squares of cells using border outlines, and fill in the background colors of cells to visualize the desired tile design. The main character tile is shown in Figure 5.

To convert tile designs into code, we developed a Python script. The user inputs the design by the corresponding number of the color of each block (as defined by the VGA driver), and outputs the properly formatted 16×2 array of long longs, which can be copied and pasted into the assets.h header file in which all designs are stored. This is essentially a task in string manipulation. It was instrumental to our efficiency, however, because the tile design used by the VGA driver needs to be in terms of pixels rather than blocks.

The script also generates the transparency mask used for characters and entities that do not take up the entire 16×16 tile, such that one should be able to see the background “through” parts of the tile. The tile design Python script makes the designer’s life significantly easier, because it transforms an easy-to-visualize block of colors (coded as numbers) into arrays that are nearly illegibly manipulated, to be compatible with the VGA driver.

Level design differs from tile design, in that each element is a tile design or monotone color rather than a colored pixel. This makes it significantly more difficult to visualize a level design accurately using the spreadsheet technique. It was helpful to lay out screens on an 11×16 grid of cells and to rely on “average” color of the desired tile design for each tile of the screen. Even so, level design required a lot more testing on the VGA screen than tile design did. Each screen design has three parts: an array indicating which design should appear on each tile, an array of interactables, and a wall mask.

Similarly, we developed a Python script to more easily encode the design, but it only formatted the tile designs and monotone tiles (part 1). Parts 2 and 3 were written manually, because it was easy enough not to require automation. Enough time was spent iterating on screen designs that it was possible to memorize the encodings of respective colors and interactables simply by working through the code.

The demo level includes nine screens (listed in no particular order):

  1. Wakeup screen
  2. Hallway directing player toward the weapon
  3. Allows player to pick up the weapon, enabling the player to fight enemies
  4. Contains enemies the player must fight, and a “villager” that the player must pay before proceeding through the level
  5. “Boss fight” in which the player must fight two spider enemies with > 1 HP (hit points)
  6. Hallway leading to the puzzle cave
  7. Puzzle the player must solve, to get gems to pay the villager
  8. Reward for solving the puzzle (gems)
  9. End screen

As far as relative placement of the screens and traversal through screens within the level, the game was implemented with screen transition “portals” located on the edge of the screen (top, bottom, left, or right). All screens were spatially arranged with respect to each other. Although we modularized the development of each screen, they were stored so that upon standing on a transition-enabled tile, the character simply walked in the respective direction onto the next screen. For example, walking onto a portal on the left side of the screen would take the player to the screen positioned left of the current one.

SOUND EFFECTS

All sound generation for the game comprises two distinct groups: sound effects and background music. We used a 2-channel DAC to produce analog signals that would ultimately be connected to external speakers with a 3.5mm audio jack. Both the sound effects and background music run simultaneously on separate channels of the DAC for the sake of simplicity.

We created our own sound effects for this game. Most of these sound effects were played on a guitar, drum or household item, and were recorded on a laptop. These small (1s) sound bites were recorded on Audacity, then exported as signed 16-bit PCM .wav files. Because a .wav file is simply an array of values corresponding to amplitudes of the sound output at each sampling time, we knew we could store these values in array-form. We sampled these sound effects at 8kHz to save space.

We developed a Python script to process the .wav files into scaled arrays that could be inserted into a header file. By declaring these arrays as constant, they could be stored in flash memory. This way, any time we wanted to play a sound effect, we could configure a DMA channel to iterate through the array and send each array value to the serial output buffer. An example of a sound effect is shown in Figure 7.

FIGURE 7
Example of a sound effect
FIGURE 7
Example of a sound effect

The other half of our sound generation, background music, is synthesized with Direct Digital Synthesis (DDS). For this synthesis, we developed a lookup table that closely resembles the sound wave produced by the synthesizers in the Nintendo Entertainment System. We selected a duet from Musescore.com . This arrangement contains both 16th notes and triplets, so 12 subdivisions per beat are required to keep perfect time. Therefore, we created an array to contain each note in the song, with a secondary array to contain the length of each particular note. We wanted two independent parts to be played in the interrupt. To allow for two-part music (duet style) while still only using one channel of the DAC, the calculated DAC outputs for each part are averaged. We found that this was almost always perfect, except for when each part was simultaneously playing the same note.

CONCLUSION

We found the implementation of game logic to be much more difficult than anticipated. For example, we had planned to implement the game in a fully tile-based manner, but quickly realized that for the characters’ movement on the VGA display to appear smooth enough, they could not jump from one tile to the next. Thus, we also needed some extent of control between tiles. We also did not anticipate how many corner cases we would have to deal with throughout the game logic. Instead, we had predicted running into issues with running out of space on the Pico, either for storing designs and sound effects or for running the game code. Neither of these issues ended up being a problem.

As a result of developing the game program from very strong fundamentals and prioritizing organization and good coding practices, we were able to add features such as interactability, additional enemies, and walking animation frames for the main character, more easily than if we had taken a shorter-term approach to the game engine development. Specifically, we implemented the puzzle and the villager with whom the player must interact within the last few days of the project.

A video demonstration of our project is available on YouTube [5], and the code for this project is available at Circuit Cellar’s Article Materials and Resources webpage. 

RESOURCES
Raspberry Pi | www.raspberrypi.com

REFERENCES
[1] Wikipedia: The Legend of Zelda (Video Game): https://en.wikipedia.org/wiki/The_Legend_of_Zelda_(video_game)
[2] Framework for VGA driver developed by V. Hunter Adams and Bruce Land: https://vanhunteradams.com/Pico/VGA/VGA.html
[3] Adam Dunkels, “Protothreads”: https://dunkels.com/adam/pt/
[4} Cornell University, “ ECE4760, ProtoThreads, PIC32MX250F128B”: https://people.ece.cornell.edu/land/courses/ece4760/PIC32/index_Protothreads.html
[5] Video demonstration of the completed project: https://www.youtube.com/watch?v=4r80cfx1C0o

SOURCES
Adams, “ECE 4760/5730, Cornell University,” https://ece4760.github.io/, 2023
Raspberry Pi Ltd, “RP2040 Datasheet,” https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf 2023
Raspberry Pi Ltd, “Raspberry Pi Pico Datasheet,” https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf 2023

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MAY 2024 #406 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

— ADVERTISMENT—

Advertise Here

Sponsor this Article

Tangia Sun (tts47@cornell.edu) is a Masters of Engineering student at Cornell University studying Electrical and Computer Engineering graduating in Dec of 2023. He is interested in PCB design and analog circuit design, and likes to prototype and code.

Emily Speckhals (ems395@cornell.edu) is a current Senior studying Electrical and Computer Engineering at Cornell University. She is interested in the integration of electrical and mechanical systems.

Max Klugherz (mck65@cornell.edu) is a current Senior and incoming Master of Engineering student in Electrical and Computer Engineering at Cornell University. He is expected to complete both degrees by December 2024. After he graduates, he plans to pursue a career in mixed signal design for avionics systems.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Video Game Inspired by The Legend of Zelda

by Tangia Sun, Emily Speckhals, and Max Klugherz time to read: 14 min