Introduction: Runner Game in VHDL
We are EE/CPE students at Cal Poly, San Luis Obispo, in a digital design class taught by Professor Andrew Danowitz. For our final project we were tasked with creating an electronic game using the Nexys 3 FPGA board. We decided to create an endless runner game written in VHDL.
In this instructable we will introduce our design for this game along with the VHDL modules we wrote. This is intended to be an "Endless Runner" game. There are four lanes with blue box obstacles moving from right to left on the monitor. These obstacles will move toward the player icon, a white box. The player's job is to dodge the obstacles by moving up and down by pressing the top and bottom buttons on the FPGA board. The player will have three lives before the game’s over. If the player gets hit by an obstacle the player will lose a life. Once the player gets hit three times the player icon will turn red and the game must be reset. We included player score and lives remaining outputs on the Nexys 3 board itself (not on screen, as that would have been significantly more difficult).
This tutorial is intended for readers who have a bit of prior knowledge of VHDL and the use of field-programmable gate arrays. We wrote our modules using ISE Design Suite 14.7.
We hope this will be an interesting and informative tutorial for other students and hobbyists. We’ve included all the VHDL modules we used to create the game at the end of this tutorial. The modules contain comments throughout, which should hopefully answer most questions about how the components of our game operate and interact with each other.
Please keep in mind that we’re merely students just learning how to use these tools. This was our first true digital design project, and it’s far from perfect. You’ll likely find redundant signals and inefficient processes in our VHDL modules. Still, we think this could help give others ideas for their own projects. All modules that we obtained from other sources are credited at the end.
If you use any portion of our modules in your own projects, we only ask that you please credit us or the other creators.
Here we go!
The first large chunk of VHDL that we implemented was the game logic. In this module, within the entity we created outputs for the four obstacle locations, the score, lives left, and the game over state. From that we added in a clock with the desired frequency of 8Hz. The constants we used to create that frequency are integer number and represents our desired frequency of game logic updates. Next, we generated a random sequence for the obstacles to be displayed. By use of an LFSR we were able to deal with a pseudo-random number generator to generate the obstacles in the game. Obst_out is the output seen and that output is sent to the pick_a_lane module which decides which of the four lanes the obstacle will go too.
The next part is deciding whether or not the player was hit by an obstacle or not. By use of signals we were able to keep track of where the players and obstacles were at all times. Through this we were also able to keep track of how many hits the player has made with the obstacle before reaching the game over state. Another clock was implemented specifically for the "game logic" of the circuit. It is much slower than the other clock signals because it determines how frequently the objects on the screen are updated and how quickly the obstacles move across the screen. Once we had the "game logic" clock put in, we were able to create a global reset. This reset signal is linked to all components of the "game logic" and when the signal is high, it resets all elements of the game. Next, we instantiated the four SRs for the four lanes. The obst_in inputs to the SRs are linked to arbitrarily selected indices of the LFSR's stored 32-bit binary number. The outputs of these SRs are linked to the ourputs of the game_mechanics module. Once that four SRs were instantiated we put in one LFSR that is used to generate new obstacles in the four lanes in a pseudo-random fashion. Then a process is used to generate our desired "game clock" with a frequency of 8Hz and the signal enable_game_update will only be high 16 times per second. Next, through a series of if/else statements, a process was created to determine the movement of the player. The player is only allowed to move up or down between lanes. It runs whenever a change in player input is detected because that is when the user wants to move their character. After the player_movement process was implemented, a hit detection process was needed. This process is responsible for checking to see if the player hits an obstacle when the obstacle enters the "player zone". Along with this hit_detection process, a process called scoring_system was put into action through an if/else statement that increments once per second. Lastly, a process called check_for_game_over was necessary to complete the game logic. Using if/else statements and utilizing the global_reset process, we were able to check if the lose_state was high or low. If lives_left equals zero, then "Game Over".
Step 2: Lane Instantiation
In this step of our VHDL module we needed to create the different lanes for the obstacles to be present in. We utilized game_clock and obst_in from the previous "game logic" module to become the inputs in this new entity. Also using dead and reset as inputs, an output will be present called lane_out. Within the architecture of this module a beginning lane with no obstacles 32 bits wide is used.
A process called "shift" is then implemented with a sensitivity list including game_clock, obst_in, dead, and reset. Through an if/else statement it is determined whether or not to shift the obstacle. However, if reset or dead are high, then the lane will go back to the original state of all 0's for the 32 bits.
Step 3: Game Obstacles
For this game to be playable and fun, it needs obstacles.To have obstacles appear in the lanes we have just created, we implemented two different signals inside of the architecture of LFSR. We have first the signal called "lfsr" which is a 32 bit binary number with only a single one in a random place inside the string. The next signal we have is called "lfsr_next" which is again a 32 bit binary number consisting of only a single one placed in the same positions as the one in "lfsr".
Once we declared those signals, we created a process called lfsr_seq with a sensitivity list consisting of game_clock and reset. An if/else statement then reads in to determine if "dead" is high and, if so, lfsr is assigned a 32 bit binary number consisting of only zeros. Lfsr will only shift when the game_clock is on the rising edge.
Finally, there is a combinational logic process called lfsr_comb that will create a pseudo-random sequence of obstacles. In this process an XOR gate is put anywhere in the 32 bit binary number. This XOR serves to create the pseudo-random sequence of numbers.
Now we have obstacles!
Step 4: Lane Selector
Now that we have the ability to create obstacles to appear on the screen, we need to tell them where to go. We first set outputs in the entity called pick_a_lane. These outputs are obst_to_lane_1, obst_to_lane_2, obst_to_lane_3, and obst_to_lane_4. We created two signals, lfsr and lfsr_next, that are 16 bit binary numbers with three arbitrarily placed ones. After these signals are created we made a process called lfsr_seq with only game_clock in the sensitivity list. The lfsr will get lfsr_next only on the rising edge of clock. Next we have a large if/else statement that chooses one of the lanes to get the obstacles based on the decimal value of lfsr. If obst_in from the obst_generator LFSR is not one, then no obstacles are generated. After the if statement, we again have a process called lfsr_comb with an XOR statement to psuedo-randomize the sequence.
Step 5: Score Display
From a VHDL module written by Bryan Mealy, we were able to display a score counter on the seven-segment display on the FPGA board. This module takes an eight bit number and turns it into a decimal number able to be shown on the seven-segment display. The score increments one point per second and stops counting once the player reaches the "Game Over" state.
(Again the VHDL module for this section is provided at the end of this tutorial)
Step 6: Pixel Clock
There are many different clock signals in this project. One of the main clocks was the pixel_clock. We needed a clock to be outputted at 25.185Hz. We used the core generator included in the ISE Design Suite to create a clock for us. We entered our desired clock frequency of 25.185Hz and used the VHDL created as our pixel_clock.
Step 7: VGA Sync Module
This game would be a lot cooler if you could actually see it, so our next step was to have it displayed on a VGA monitor. To get all of the game logic and such displayed on screen you need a module to send all of the information from the FPGA board, through the VGA port, and to the monitor. By use of VHDL provided by Digilent, we were able to complete this task and have the game displayed beautifully on a monitor screen.
(Again, the VHDL module is provided at the end of this tutorial)
Step 8: Graphics
Now that we have figured out how to get the obstacles and the player drawn on the screen, we need to tell them where to go. With inputs of pixel_clk, player_loc, obst_locs_1, obst_locs_2, obst_locs_3, and obst_locs_4 and an output called rgb we are able to begin this task. We had to choose how large we wanted all of the boxes on the screen to be and where exactly we wanted them. First we had to create two signals, draw_plyr and color_selector. These signals will help to draw the boxes. Then we need to convert 11-bit inputs, hcount and vcount, into integers. We do this because it will make it easier to designate locations on the screen. We then wrote a very large OR statement with 32 options for each of the four lanes.
We also needed to tell when to draw the player. We told it to draw the player (draw_plyr) only when it is high again using an OR statement to tell it where on the screen to be drawn.
Lastly for our graphics we needed to give all of the boxes their designated colors. We decided the obstacles to be blue and the player to be white. The player is white during game play. When the lose_state is achieved, the player will turn red implying "Game Over".
Step 9: Top Level
The final part of this project is the top level wiring. We have written eight separate modules and had to find a way to connect them all together. We begun with our top_level entity with inputs of Clk and player_input and outputs of rgb, HS, HV, LD7, LD6, LD5, segs, and AN. The first component, game_logic, is implemented with clk and player_input as inputs. We decided p_i button to be "up", the p_i button to be "down", and the p_i button to be "confirm/select".
Next, we need an external signal for current_player_location and this goes to another module that will draw the player on the screen. Another external signal is needed for locations of obstacles that will be drawn by another module.
Components are necessary for all modules previously written to wire everything together. A component for the VGA monitor is needed as well with inputs of rst and pixel_clk and outputs of HS, VS, hcount, vcount, and blank. A component for the clk_wiz_v3_6 is put into action with an input of clk_in1 and an output of clk_out1. We also need a component for graphics. This component has inputs of pixel_clk, player_loc, obst_locs_1, obst_locs_2, obst_locs_3, obst_locs_4, dead, hcount, vcount and an output of rgb.
We also need a component to display the player's score on the seven-segment display on the Nexys 3 board. This component called sseg_dec was written and provided by Bryan Mealy and is again provided at the end of this tutorial.
We then need to instantiate 15 signals. These signals are: pixel_clk, reset, score, top_lives_left, top_level_dead, player_loc, obstacles_to_draw_1, obstacles_to_draw_2, obstacles_to_draw_3, obstacles_to_draw_4, horizcount, verticount, blank, sign, and valid. The signal "reset" is a global reset and, when high, will set all components back to their initial state. The internal signals "score", "top_lives_left" and "top_level_dead" come out of the game_logic module and go into the graphics module which then decides which pixels will be which colors every time the screen refreshes (60 times per second). The signal "blank" will always be zero because we are not using it in our game design; it is part of the standard Diligent VGA controller. Lastly, the signals "sign" and "valid" are signals for the sseg_dec module and are only initialized here to satisfy the port requirements for the pre-built sseg_dec VHDL module.
Using these signals, we assign pixel_clock to be the clk_wiz_v3_6, game_mechanics to game_logic, sync_module to vga_controller_640_60, draw_the_boxes to graphics, and score_disp to sseg_dec.
The last touch we made was to display the lives left for the player on the LEDs on the Nexys 3 board. This was done by a process called lives_on_LEDs with only top_lives_left in the sensitivity list. Using an if/else statement it is determined how many lights to display on the LEDs until no lights are on meaning "Game Over".
Step 10: Credits and VHD Source Files
We hope you found this tutorial helpful. If anything is unclear or you have more questions about our design process, please leave a comment or send us a message.
We'd like to credit the following people for their work that we used in our project:
-The vga_controller_640_60 module was written by Ulrich Zultán for Digilent, Inc., and is available for download under the title "VGA controller reference design" on the following page: Nexys™2 Spartan-3E FPGA Board
-We also made use of the "Master UCF File for Nexys3", which is provided here: Nexys™3 Spartan-6 FPGA Board
-The sseg_dec (seven-segment display decoder) module was written by Professor Bryan J. Mealy and provided to us by our professor.
-The idea for our game_freq_gen process was provided (unknowingly) by StackExchange user Martin Thompson on this forum thread: How to divide 50MHz down to 2Hz in VHDL on Xilinx FPGA
Attached to this Instructable, we have provided you with the individual .vhd files we wrote for reference. Additionally, we include a complete project folder that should allow you to run the game on a Nexys 3 board with minimal set-up (provided you have already downloaded and installed ISE Design Suite 14.7 as well as Adept).
One last thing we've attached is a .zip file containing only one file, a higher-quality photo of the drawn top-level block diagram, which will hopefully allow you to see a non-lossy copy of the picture. (Its quality degraded significantly upon uploading to Instructables the normal way).