Program Design

The program for this game was largely inspired by the boids program from Lab 2. From the beginning, the code was designed to operate on both cores of the Pico, with the finite state machine thread running in core 0 and the player control thread running in core 1. We also configured GPIO 4 to be the start button that was pulled up by default, GPIO 18 to be the move left control, and GPIO 19 to be the move right control. We then instantiated various macros for fix15 calculations in the same way that they were included in lab 2, the global variables necessary for the threads, and the inclusion of header files containing the pixel color arrays of each sprite.

A handful of helper functions were created to supplement the threads.  One important helper function was drawSprite, which drew sprites on the VGA display at a specified vertex (x, y) by taking the color array of the sprite to be drawn as an input. 

Helper Function for Drawing a Sprite

The way the function completed the above task was by iteratively calling the function drawPixel from the VGA graphics helper file in a nested for loop. Each pixel called by the nested loop was colored with the corresponding element in the color array. This way, each sprite was drawn pixel by pixel, and this worked because the color arrays were flattened arrays that already preserved the row order and thus only needed to have their pointers incremented every iteration. Another useful helper function was random_int, which generated a random integer within a specified range of values. Finally, the functions startScreen and gameOver were made to display the beginning screen and the game over screen, respectively, which corresponded to state changes that are described later in this section.

The player thread handled the animation updates for the player avatar in accordance with the player movements. 

Code for Changing Avatar Position Using Player Input

Since the avatar should only appear in the start screen and during the actual game, the thread first checks if the state is 0 or 1 before drawing the Newton. The position of the Newton is updated at a constant velocity such that if GPIO pin 18 goes low, then that means that the player has moved their hand above the left phototransistor and the Newton should move to the left; and if GPIO pin 19 goes low, then the right phototransistor has been covered and the Newton should move to the right. The Newton is redrawn at the new position afterwards (with a brief delay to moderate the speed of the avatar), and the thread is delayed before restarting to account for the frame rate spare time.

An important aspect of the player thread is the hitbox checker. This algorithm compares the current position of the avatar with the current position of the projectile to see if the two have made contact in any way. Since the positions are only the top left corners of each sprite, it was necessary to also make sure that the pixels adjacent to the top left vertex of each sprite were not in contact with any of each other. If they were, then the state was changed to 2, which replaced the level with the game over screen before automatically returning to the start screen.

The finite state machine thread handled most of the state changes that occurred internally throughout the game. 

Finite State Machine Diagram

At the beginning of the game, the state is initially 0 and draws the start screen while waiting for the player to press the start button. To check if the start button was pressed or not, the FSM thread continuously checks if GPIO 4 is low (since it was pulled up and would go low if the button connected to it was pressed). If the button is pressed, then the thread goes into an empty while loop until the button is unpressed, after which the state changes to 1.

At state 1, the actual level is drawn. First, the five apple enemies are drawn and their positions stored in enemy arrays. Then, the thread enters a while loop that continuously updates the projectiles shot by the enemies. The way the loop works is that if the projectile is determined to not exist using a toggle variable, then the projectile is initially drawn to be shot from a randomly chosen enemy. The projectile’s position is then updated every frame to account for its vertical movement down until it exits the screen’s dimensions, after which the projectile is considered non-existent again. The FSM thread was also delayed before restarting in accordance with the frame rate spare time.

One aspect of the program that was difficult to code was getting the state to change to the game over state if the player avatar hit a projectile. The reason was because we were initially getting an error where if the player hit a projectile, then the avatar stopped moving and was unable to be controlled further, but the projectiles continued to spawn and the game over screen was not drawn. We deduced from this error that the state was successfully changed to 2 and the player animation thread successfully exited its while loop as a result (which is why the avatar stopped moving), but the state change was not reflected in the FSM thread for some reason. We initially tried various solutions such as creating two separate sets of frame rate and timing variables for the two threads and manually changing the state in both threads, because we thought that the problem was that the threads were not synchronized and did not share global program context. However, these changes did not resolve the problem. Eventually, we fixed the bug by completely exiting the FSM thread with PT_EXIT(pt) if the player hit a projectile. This caused the FSM thread to turn off after a projectile hit which allowed the game over screen to manifest.