Crowd City on Pico¶

Zhanqiu Hu (zh338), Zehua Pan (zp74)

Table of contents¶

  1. Introduction
  2. High-level Design
  3. Program/Hardware Design
  4. Results
  5. Conclusion
  6. Appendix
In [ ]:
from IPython.display import YouTubeVideo
YouTubeVideo("https://www.youtube.com/watch?v=m4_iMseZTsA&list=PLDqMkB5cbBA6AwYC_DElkDStUdOsTuIL7&index=38", width=800, height=500)
Out[ ]:

Introduction ¶

This project implements a two-player video game through the combination of hardware and software based on RP2040 microcontroller.

From the developer’s perspective, this project is an embedded video game project designed for entertainment. In lab 2 of course 5730, the team learned the concept of boids and their behavior. During the process of coming up with ideas for the final project, the team found an interesting online game called Crowd City. Inspired by these two, the team decided to create a user-controllable video game based on what we learned in this course.

From the user’s perspective, the players could control and play the game using the two joysticks. The user interface is shown on the VGA screen. After choosing the game time on the initial interface, the players could start playing the game. During the game, each player could control a leading boid on the screen to move and infect other free boids. Those infected free boids would become members of the player’s group and the player with a larger amount of members at the end is the winner. The player should move its leading boid smartly to enlarge its group as fast as possible. Whenever the group size of one player increases and exceeds the amount of another group, a warning sound would be generated. If the time for the current game is less than five seconds, a timing sound would keep alarming until the game ends. Finally, the game ends and the winner would be shown on the screen.

High-level design ¶

The high-level design for the hardware part is shown as the diagram in the following figure. As one can see, the core of this project is Raspberry Pi Pico development board with RP2040 microcontroller. Other hardwares are outfitted on the breadboard with physical wire. On the top-left corner, the diagram shows the part about generating sound. The team first connects Pico to the DAC converter and then connect the DAC converter to the audio socket. By this way, the digital signal generated by the Pico could be converted into analog signal and further human-detectable sound. Finally, the signal is played by the speakers connected to the audio socket. On the top-right corner, the diagram shows the part about display. In order to display the user interface of this game, the team simply needs to connect the Pico to a VGA screen. On the bottom-left corner, the diagram shows how the Pico is programmed. In this project, the team makes use of Mac laptop to write the code and program into the Pico. On the bottom-right corner, the diagram shows how the users could interact with this system. As one can see, the team first connects one joystick to the Pico directly and then insert a MUX between the Pico and the second joystick. This is because one joystick needs to use two ADC channels. Since the Pico only has three usable ADC channels, the second joystick can’t be connected to the Pico directly. Instead, the team makes use of a Mux to switch the two inputs from the second joystick and output one signal to the Pico. Since the switching delay is so small such that the user couldn’t tell the difference, this design is acceptable.

For the software part, the team defines the concepts and some initial rules for the game as the following.

Basic concepts:

  • Boid: moving unit on the screen, use the same concept as in lab 2 of the course.
  • Leading boid: the boid that could be controlled by the user via joystick, bigger than other boids and has its own color.
  • Free boid: the boid that moves freely and randomly with white color.
  • Group: some organized boids moving together led by the leading boid and they have the same color as the leading boid.
  • Infection: a boid is absorbed into one group compulsively.

Infection rules:

  • When the leading boid is close to other free boids, the free boids are infected by the leading boid, and their colors will be changed to the same as the leading boid. In the code, this is implemented by the free boid side. That is, when a free boid sees a leading boid in its infected range, it changes its color. In the case it sees two leading boids, it chooses the one with a smaller distance.
  • The infected range is the same as the protected range of the Separation algorithm in lab 2 of the course. After that, they will follow the leading boid.
  • The leading boids couldn’t be infected! So the team only focuses on the update of the free boids.

Following rules:

  • Non-leading boids in the group will follow the leading boid of that group through some organic mechanism (like what we did in lab 2). For example, there will be some matching, alignment, and separation effects. We will introduce some new rules for boids in the group to align and catch up with the leading boid.

Battle rules:

  • When two (or more) different groups meet together, the boids in the group with a smaller amount will be infected by another one. But it can flee as fast as possible. Notice that the member instead of the leading boid in a specific group could also affect members in other groups.

Unfollowing rules:

  • An infected boid has a low probability to unfollow the current leading boid (configured by own bias). Also, it is possible for boids at the margins of the group to unfollow the leading boid. For example, if the leading boid is not in the visual range of the boid, boid will unfollow the leading boid, but might still have the tendency to follow the group.

Other rules:

  • Use of colors to indicate boids: we will use white pixels to indicate free boids, green to indicate one crowd, and red to indicate the other crowd. We will also add some additional notations to indicate which two boids are leading boids.
  • Timing: Each game has time limit. It could also be user input.

Program/hardware design ¶

The hardware used for this project is listed as below.

  • Raspberry Pi Pico with dual-core RP2040 (# 7 in figure )
  • DAC converter (# 3 in figure )
  • 3.5mm audio socket (# 2 in figure)
  • Two speakers
  • VGA Screen (connected cable is # 1 in figure )
  • Mac Laptop
  • 8 choose 1 MUX, Model CD4051BE (# 5 in figure )
  • Two joysticks (# 4 in figure )
  • UART
  • Button (# 6 in figure )

The hardware setup and connection is shown in figures below. Note that the VDD of Joystick 2 is also connected to the Pico 3.3V output.

In addition, since this is the first time the team tries to make use of a MUX, the team meets a lot of issues when debugging the hardware part related to the MUX. The most important one is that the team forgets to connect the VEE and INH PIN of the MUX to the ground at first. As a result, the team observes a strange bug in the system. Specifically speaking, when using one channel input from the joystick, the output signal to the Pico is correct. But once we connect two channel inputs from the joystick and switch between them, the output signal would become some unexpected results. This is really hard to debug since the MUX does work when there’s only one channel input and the software part has nothing to do with it. The team finally figured out the bug by checking the circuit example in the MUX datasheet carefully.

Hardware setup


Connections

Sound effect algorithm¶

In order to increase the user experience, the team adds some sound effects to the system. The sound generation algorithm is based on the direct digital synthesis of lab 1 of this course. There are three possible sound effects generated during the game. When the size of one group exceeds the size of another group by a certain amount, the Pico would generate a beep sound. Two different frequencies are designed for the two cases: the red group size is larger than the green group size or the green group size is larger than the red group size. In addition, when the remained time for the current game is less than 5 seconds, a continuous alarm sound would be generated to warn the players. The technical details come from the algorithm and logic of lab 1. The major difference is that in this project, the team only uses core 1 to generate the sound. This is because core 0 is dedicated to controlling the boids in order to maximize performance and the number of boids. Since only core 1 is used for sound generation, the system could only generate one kind of sound at a specific moment. The sound selection is controlled by the global variables in the code.

Joystick control algorithm¶

This project requires the players to use joysticks to interact with the system. There are only two joysticks connected to the system and each player uses one of them to control the leading boid. One of the joysticks is the master joystick and only this joystick could be used to control the game mode selection and game reset. When using the joystick to play, the player needs to hold the joystick in the correct direction such that the text on the joystick could be read normally. In this way, when the player moves the joystick to the left, the leading boid would move to the left on the screen. During the initial user interface on the screen, the player could use the master joystick to choose the time of the game. Once the time is confirmed, the player could press the master joystick in the middle and the game would start. After the game is over, the player could press the master joystick in the middle again to reset the game.

Before we dive into the algorithm, one should know what kind of data the joystick generates and send to the Pico. The joystick has two outputs: VRx and VRy, which correspond to the current position of the top of the joystick. The joystick uses 12 bits to encode the x and y values, which means the range of the value for x and y is [0, 4095]. When the joystick is in the normal position (not moved by the player), the output value of VRx and VRy should be around 2048. When moving the joystick horizontally, the VRx would change accordingly. When moving the joystick vertically, a similar thing happens to VRy.

From the previous sections, we know that the first joystick is connected to the Pico directly through two ADC channels. Therefore, when reading the data from the first joystick, the team only needs to collect the x and y values from the “adc_read()” function. However, when reading the data from the second joystick, the team needs to enable the switching of the MUX such that the Pico could get the x and y values. Specifically, the MUX has three control inputs A, B, and C. They correspond to the selection bits and output the data from one of the input channels. For example, when A, B, and C correspond to high, low, and low. The selection bits are 001 (CBA) and input channel 1 would be selected. In this project, we only have two inputs from the second joystick and they are connected to input channel 0 and input channel 1 separately. To enable the switching, the team only needs to use one bit. Therefore, control inputs B and C are wired to the ground. When control input A is low, input channel 0 would be selected, otherwise, channel 1 would be selected. The team makes use of GPIO 22 to control the status of control input A. Namely, when GPIO 22 is set to high, control input A is high, and the same for the case of low. In this way, the team could control the receiving data through the setting of GPIO 22 in the code. In addition, the x and y values might oscillate a little bit even though no one moves the joystick. To stabilize the data, the team shifts the data by 4 bits in the code and uses the new data for other algorithms.

Game logics¶

Our game consists of three main states: START, PLAY, and END. We have a global variable curr_state to store the current state.

At START state, the program draws the following display onto the VGA screen. Player can move the joystick left or right to select options (time limit for the game). The options (from left to right) are denoted by integers 0, 1, 2, and 3, and the default option is 30s. We keep track of player’s current selection using a global variable. Each time the program register a movement to the left, we set current option to be: curr_opt = (curr_opt + 3) % 4; each time the program register a movement to the right, we set current option to be curr_opt = (curr_opt + 1) % 4.

After selection, the player will be able to click the button on the joystick to start the game. When the program detects a button click, it sets the current state to be PLAY, and performs some initialization.

START state

Initializing PLAY state¶

The following image shows the PLAY state. The rectangular boxes and the circle are obstacles, and scores and remaining time are printed on two of the rectangular obstacles. The other cyan rectangular obstacle moves up and down throughout the game. During state change, we first initialize the global variables used in the PLAY state, including

  • 4 variables that store positions and velocities in x- and y- directions of the 2 leading boids (randomly initialize, and make sure the positions are not in obstacle and the speeds are between max and min speed)
  • 4 arrays that store the positions and velocities in x- and y- directions of the boids (randomly initialize, and make sure the positions are not in obstacle and the speeds are between max and min speed)
  • 1 array that contains 2 counters that keep track of the size of the group (initialize to 0)
  • 1 array that store the status of boids: whether they are free, green, or red (initialize all boids to be free boids)
  • 2 variables that store the current position and moving direction (up or down) of the moving rectangular obstacle
  • Arrays that store the positions and widths and heights (or radius) of the obstacles (initialized to pre-determined values)

We also clear drawings from the START state, and draw the initial positions of obstacles and boids onto VGA screen.

PLAY state

Boid movements¶

At each frame, we loop through each boid and update their velocities for them to behave correspondingly to the rules we described in the first section.

If the boid is free,

  • If it is within the protected/infection range of a leading boid, it will be infected and become green or red. If it is within the protected/infection range of both leading boids, it will be infected by the one with a greater group size.
  • Otherwise, the boid stay as free boid, and with a 1% probability (if rand() % 100 == 1), we update its velocity by adding randomly generated values to its current velocity and do not update their velocities otherwise.

If a boid is green or red, we loop through every other boid:

  • If the other boid is free and it is within our protected/range, we can infect them
  • If the other boid has the same color as us, we will try to keep away from them following some logic (specified in the following code snippet)
  • If the other boid is from the other group, we will infect them if our group size is greater than theirs and will be infected by them if our group size is smaller than theirs

The following code snippets shows how we update each boid based on others. In particular, when calculating the relative distance between two boids, we defined new macros called real_x_dist and real_y_dist. This is because our game supports top/bottom wrapping and left/right wrapping. This means there are four direct paths between two boids, depending on whether we choose the path that goes across the edge of the screen. Therefore, we define the real distance between two boids to be the shortest path of the four. We also have a helper function set_boid_state that takes in the index of a boid and the state we want to set it to. The function also updates the counters to reflect the change in group size due to infection, stealing, and unfollowing.

Loop: {  fix15 close_dx = 0;
   fix15 close_dy = 0;
   // For each boid except the current boid, update the variables accordingly
   for (int i = 0; i < NUM_BOID; i++){
     if (i == boid_id) continue;
     // Compute difference
     fix15 dx = real_x_dist(*x - boid_free_x_array[i]);
     fix15 dy = real_y_dist(*y - boid_free_y_array[i]);
     int i_state = boid_free_state_array[i];
     // Check if it is in visual range
       if (abs(dx) < INFECTION_RANGE && abs(dy) < INFECTION_RANGE){
         if (i_state == -1) { // when i is free, member boids can infect free boids
           set_boid_state(i, boid_state);
         } else if (i_state != boid_state) { // both boids have color
           if (member_counts[i_state] > member_counts[boid_state]){
               set_boid_state(boid_id, i_state);// current boid with boid_id is infected
             } else if (member_counts[i_state] < member_counts[boid_state]){
               // boid i is infected
               set_boid_state(i, boid_state);
           }
         }
         close_dx += dx;
         close_dy += dy;
   }   
 }
 // Add the avoidance contribution to velocity
 *vx = * vx + multfix15(close_dx, AVOID_FACTOR);
 *vy = * vy + multfix15(close_dy, AVOID_FACTOR);

We also want member boids to follow their leading boids. For each member in the group, if the boid can “see” the leading boid (i.e. within visual range), then we update its velocity as follows

*vx = *vx + (-dx0) * CENTERING_FACTOR)+ (boid0_vx - *vx) * MATCHING_FACTOR;
*vy = (*vy + (-dy0) * CENTERING_FACTOR +(boid0_vy - *vy) * MATCHING_FACTOR;
Last but not least, we update the position of each boid based on their velocity, then redraw them on the VGA screen with their new color.

Moving obstacle¶

We update the y position of the obstacle to reflect changes in the height of the obstacle. Recall that we have a global variable that stores the direction of movements for the moving obstacle. For the obstacle to “move” up, we erase (blacken) the bottom line of pixels and draw a cyan line of pixels of the same width above the top line of pixels, and similarly for downward movement. The obstacle starts with upward movements, and when the position of the box hits the top limit (predefined), we flipped the direction variable and start moving down. Similarly, when the position of the box hits the bottom limit (predefined), we flipped the direction variable and start moving up.

Boids’ interaction with the obstacles¶

For stationary obstacles: When a boid moves closer enough to a boundary (distance to the boundary is within some pre-defined margin), it loses all velocities in the direction that’s perpendicular to the boundary and will not enter the obstacle. To prevent the boids from getting stuck in one place (i.e. having 0 speed), we assign a small velocity to it if both vx and vy are 0. For moving obstacles: When a boid moves closer enough to a boundary, it will bounce off (i.e. the velocity in the direction perpendicular to the boundary will flip sign).

END state¶

When the time exceeds the time limit, we set the current state to END state. And display end-of-game messages on VGA based on the three situations: red wins, green wins, or tie.

At END state, player can restart the game by clicking the joystick button. The program will set the current state to be START state, re-initialize the selected option to be the default option (30s), erase previous drawings, and draw prompts and option boxes to VGA screen.

END state

Results of the design ¶

In our video game, we keep our update logic to be within the time constraint so that we can guarantee 30 frames per second and keep the animation smooth. We spread workloads by using both core 1 and core 0. For example, velocity and position updates are done on core 0, and core 1 displays real-time messages (such as sizes of boid groups and remaining time) on the screen and generates sound effects. To make sure there isn’t data racing, core 0 reads and modifies global variables while core 1 only reads these data.

As described in the hardware section, we use a MUX to select and then read ADC input signals from one of the joysticks. This requires us to wait for the correct signals to be selected before we can read them. We used an oscilloscope to verify the wait time we choose is sufficient. We also verified that this wait time is short enough so that when we move the joystick, we don’t notice any lag.
At the START state, we tuned the time interval between two joystick registration, so that when we move the joystick left or right, the selected option box changes smoothly but not too quickly. We asked our friends to play the game and fixed some issues based on their advice. For example, we changed the color and size of some objects displayed on the VGA and added the arrow next to the member count.

We make sure our design is safe by using 3.3V output on the Pico board to power all of our hardware components, instead of using any external voltage course. This eliminates the risk of accidentally burning the Pico board with high voltage. For the components to function as expected, we also checked all components in our system to make sure they are properly grounded.

Conclusions ¶

Overall, our design meets our expectations. The movements of boids and their interaction matches the rules we specified when we started this project. We added different obstacles to increase the complexity of this video game. We also implemented the sound effects and starting and ending states to make the game more playable. We found the video game to be very interesting, our friends also had fun playing it! We used the starter codes for lab 1 (for sound effects) and lab 2 (for displaying boids on VGA), and we also designed our following and avoiding logic based on lab 3 instructions.

Appendix: ¶

Permissions¶

"The group approves this report for inclusion on the course website."

"The group approves the video for inclusion on the course youtube channel."

Cost list¶

Name #Item Price link
Raspberry Pi Pico 1 $9.99 link
Joystick 2 $6.29 link
CD4051BE MUX 1 $0.79 link
Breadboard 1 $4.20 link

Contribution¶

Zehua Pan(zp74): partial hardware setup and debugging (e.g. sound generation part, VGA and UART debugging, general design), sound effect algorithm, joystick algorithm, basic boid algorithms (e.g. initial moving, position and velocity update, infection).

Zhanqiu Hu(zh338): partial hardware setup and debugging (e.g MUX connection and debugging, joystick wiring), joystick update, advanced boid algorithms (e.g. layout design, adding obstacles, boids colliding onto obstacles, three game states, dynamic text update, unfollowing rules, bouncing rules, and battle rules).

References¶

ECE 4760 Lab 1: https://vanhunteradams.com/Pico/Cricket/Crickets.html

ECE 4760 Lab 2: https://vanhunteradams.com/Pico/Animal_Movement/Animal_Movement.html

Original Crowd City game: https://crowdcity.io/