TFTanks

Introduction

TFTanks is a two player game in which small artillery tanks shoot projectile shells at each other across the TFT display screen. The game board consists of a randomly generated terrain landscape with sharp hills and valleys that players can destroy incrementally with their shells. The game is turn based, where players use sliders to line up their shot angle and amount of power and are allowed a limited number of steps left and right each turn. The objective is to hit the other player three times to decrease the player’s health to zero and win the game. Our purpose was to create a fun game for all ages reminiscent of those we played on CoolMathGames.com!

High Level Design

Project Idea

All three of us have fond memories of teachers bringing out the laptop cart in middle school and playing Adobe Flash Games on CoolMathGames.com and AddictingGames.com. One of such games involved two tanks shooting projectiles back and forth at each other, which we emulated in our game TFTanks. This gameplay has gone under multiple names over the years, such as Scorched Earth for the IBM PC in 1991, Atomic Tanks, and more recently Pocket Tanks available on the Google Play and Apple App Store.

Background Math

The main computations of our project occurred when we were animating the projectile motion of the shell. We achieved a 30fps frame rate by having the animation thread run every 33 milliseconds (30 frames per second = 33 ms per frame).

When in the middle of a fire event, meaning that a projectile should be moving across the screen, we performed incremental velocity calculations. The x and y velocity was set by the power slider of the user when they first clicked fire. Every frame, the y velocity is changed by the acceleration due to gravity, g, and the x velocity remains constant. The x and y positions are then updated to be their current position plus this incremental velocity to simulate projectile motion.

We also had to perform various cosine and sine calculations for drawing the cannon based on the user’s fire angle. The cannon is drawn as a rectangle, with start points at the peak vertex of the tank triangle, and end points at x = vertex_x + 15*cos(fire angle) and y = vertex_y – 15*sin(fire angle) where 15 is the length of the tank. Please note that because the (0,0) origin is at the top left of the TFT display screen, many updates to y variable seem unintuitive like subtracting instead of adding to go upwards.

Logical Structure

We implemented TFTanks with C code for the PIC32 microcontroller. We used the protothreads library to implement a cooperative multithreading system that governed the majority of our code, in which we had threads that yielded until user input and yielded periodically, for example drawing the animation of a shell across the screen every 33ms. We used the TFT LCD display in conjunction with a graphics library to draw and display the tanks and gameboard. User input is taken through the python interface which communicates with the PIC32 through a serial link. The python was written using pySimpleGUI library. We also generated events based on the PIC32 code such as a decrease in health and sent this back to the interface to be displayed.

Hardware/Software Trade Offs

Because we viewed the TFT display through a zoom call and the lab computer’s camera that was fairly zoomed out, we had limitations on what colors were clearly visible. We initially desired our land to be green and the two tanks players to be red and blue, but we found through testing that blue land and white tanks were the most clearly visible. We also achieved a 30fps frame rate without dropping any frames and this looked smooth on the TFT display, so we were not limited by the processing power of the PIC32. We did not observe any clear latency issues besides the internet occasionally dropping out and our zoom view of the TFT being delayed.

Standards and Patents

The main standards that apply to this project are communication standards. Serial communication from the Python interface to the PIC follows the RS-232 standard. Communication from the PIC to the TFT follows the SPI standard.

Because there are numerous versions of this turn-based artillery game, we do not believe we are infringing on copyright or trademarks specific to any one version of this tanks game.

We attribute intellectual property to Bruce Land and Hunter Adams for the remote interface setup as well as to the authors of the various libraries we used.

Program & Hardware Design

The above picture documents the software flow of our game. Two threads govern most of the functionality of TFTanks: the serial thread and the animate thread. The serial thread captures the user input from the python interface. When the user slides the fire angle slider or presses the move right button, for example, python writes a string via serial to the PIC32 that encodes which slider was moved, the slider ID number, and the slider value (credit: Bruce Land/Hunter Adams). The animate thread runs every 33ms corresponding to a 30fps frame rate. Once the user has aligned their shot and presses fire, the animate thread governs the projectile motion of the shell, the animation of an explosion if the other player is hit, the calling of the terrain destruction, and the game over scenarios. Below is an explanation of the main aspects of our code.

See Appendix for full source code.

The Tanks and Cannons

Each tank is represented by a tank struct. Below is a diagram of the points on the tank that we stored. We also stored the angle of the cannon, the width of the tank (20 pixels), the height of the tank (10 pixels), the health (a number 0-3), and the shell_vx and shell_vy for incremental velocity calculations in projectile motion.

Initializing the tank is done with init_tank and is called on a new game and after a restart. This method, for example, sets the tank y position to be one higher than the land position at x, and sets the shell x position to be x+15cos(angle) where 15 is the length of the cannon represented by a rectangle.

To draw the tank we call tft_fillTriangle, a built-in function of the TFT graphics library. We input the 3 vertices of the tank based on the pixels we stored as seen in the diagram (for example the left vertex of the tank is at x position (x-tank width/2)). To draw the cannon, we call the tft_drawLine() function, which takes in the start and end of the rectangle above.

To move the tank, we call the function move_step(). This method undraws the previous tank and cannon by calling tft_fillTriangle() and tft_drawLine() with the current variables stored in the tank struct and with the color black, matching our background. We also redraw the land in the immediate area of the current tank position (between the left and right vertices of the triangle) by looping through the land[] array and redrawing the vertical lines (see Game Board). We then increment or decrement the x position of the tank based on the direction of the step as input to the move step function, and do extra checks to make sure the new position is not off the TFT display, the left tank is not to the right of the right tank, and the right tank is not to the left of the left tank (they cannot pass each other). We then call tft_fillTriangle() and tft_drawLine() with the new x position and the color white.

To aim the cannon, we call the function aim_cannon(). This function simply redraws over the previous cannon line with black, and then redraws in white the new cannon based on the angle that is input to the function. It updates the shell_x and shell_y variables to be at the tip of the new cannon position.

The Game Board and Starting a Game

The game board is randomly generated on a new game. On the very first run of the game a start screen is displayed with the title “TFTanks” and waits for the start button to be pressed.

This is accomplished by setting the flag start_screen=1 in main, representing that we should be paused on the start display. In the animate thread when start_screen==1 the beginning text is displayed and the thread yields until the start_game flag is equal to 1. At this point, we only want to listen to the input from the start button. We accomplish ignoring the other user input by having the slider thread continue to yield when start_screen=1 and have the other buttons ignore their input when start_screen==1. When the start button is pressed start_game=1 and the animate thread is able to continue**. We then set the seed of the random number generator with the protothreads function PT_GET_TIME(). Because we can be sitting on the start screen for a variable amount of time and thus exiting the yield at a random point due to the user pressing start, the PT_GET_TIME() serves as a sufficiently random seed for the random number generator.

With the random number generator seeded, we can then generate the terrain for the board. The land is drawn by drawing 320 vertical lines across the width of the TFT display in the method tft_draw_level(). We store the top pixel of the land in integer array land[] so that we can draw lines from the bottom of the TFT display up to these individual points. We can index into land[] based on the x position we want to modify on the screen.

We populate array land[] with the method rand_land_create(). This method randomly generates a new map by generating the height of the land at each x position of the TFT. It begins on the left edge of the TFT at x = 0 and stores a land height value in land[0]. The following process is then repeated until the display is full of land. First, x is incremented by 30 pixels. Second, a random value is generated, scaled to be between 0 and 1, and multiplied by a maximum allowable land height. This value is the land height, or y-coordinate, at the current x-coordinate. Third, the line segment equation between the current and previous point is computed, and at each intermediate x-coordinate, the corresponding y-coordinate is computed and stored as the land height. Then x is incremented again, and the process repeats.

Terrain is destroyed when the shell hits the ground and not another player. This is done in the method destroy_land(). The x position of the destroy spot is a parameter, and 10 pixels to the left and right of the position are destroyed. This is done by decrementing the land[] at each index within that 21 pixel range by a pixel amount scaled by the power the user fired the shot with (power is set by the power slider and is global). This scale was experimentally determined to be (power/9)*30. We undraw and redraw the vertical line corresponding to that land index.

**Note that a similar mechanic controls when a new game is pressed after a player wins. The animate thread sits on a game over screen yielding until new_game=1, which is set by a new game button press. A restart_game() method is called which sets all flags to their starting state and initializes the tanks and game board.

The Game Itself : The Animate Thread and the Slider/Button Threads

The animate thread is scheduled every 33ms, however it does not perform any work unless start_screen==1 (see Starting a Game), fire==1 (corresponding to a shell in the air), explosion==1 (corresponding to an explosion animation in progress), or one of the player’s healths is equal to 0 (the game over conditions). At all other times, the slider and button threads are responding to user input on the python interface.

Which tank the buttons and sliders move is governed by a global turn variable that is either 1 (left tank, player 1) or 2 (right tank, player 2). The button thread yields until new_button==1, meaning the serial thread received a button press, and fire and explosion are both equal to zero, meaning we are not in the middle of a projectile or explosion animation, in which case the buttons should not affect the screen. Under these conditions, the button press is decoded by its button ID into the proper action. If the button is the left or right arrow buttons, as long as the player has steps remaining this turn, the move_step() function is called with the appropriate direction and tank (based on turn). The steps remaining this turn are kept track of in global steps variable, and it is set back to 5 every new turn. The start game and new game buttons set the flags start_game=1 and new_game=1 respectively (see Starting a Game and footnote). The fire button sets the shell vx and vy to be power*cos(angle) and -power*sin(angle), where power is set globally by the power slider and the initial angle of the projectile is set based on the cannon angle. It then sets fire=1, so that upon the next entry to the animate thread the projectile will start moving across the screen.

The sliders thread also yields on the same conditions as the button thread, and the serial thread signals a new slider event to the thread by setting new_slider=1. If the slider that was moved was the power slider, the global power variable is updated to be 9*slider_value/100, where 9 is our max power allowed and the slider value is a percentage out of 100. If the angle slider was moved, then the new angle is saved and sent, along with the correct tank based on the turn, to the aim cannon function.

We will now discuss the different cases in which the animate thread is running (for start_screen==1, see Starting a Game).

If fire==1, this means we are in the middle of animating the projectile to fly across the screen. We start by redrawing both tanks’ cannons in the same spot, in case the projectile location is inside one and clipping it. We then undraw the previous shell’s position, by using the method tft_fillCircle() with radius 2, color black, and position shell x and shell y. We then perform the incremental velocity and position updates based on projectile motion physics (see Background Math in High Level Design). We then call tft_fillCircle() with the new shell position and white color, as long as the shell x and y position is not negative or off the screen. We then call method collision() which checks if the shell position is inside the hit box of either tank. If it is inside the hit box, we set explosion equal to 1.

If explosion==1 when we run the animate thread, this means we are in the middle of animating the explosion of the tank. This is done by drawing concentric circles with increasing radius (adding .5 every 33ms). Once the size of the circle is radius 20, we redraw the land in the immediate area around the explosion, redraw both tanks and cannons in case they got covered by the explosion radius, and set explosion back to zero.

For the game over conditions, either tank 1’s health is 0 or tank 2’s health is 0 (and explosion==0, meaning we are done animating it). We draw to screen “GAME OVER” as well as who won the game. We then yield the animate thread until new_game==1, which similar to starting a game, will wait for the “New Game” button press which sets new_game equal to 1. We then call restart_game() which sets all flags to their starting state and re-initializes the tanks and game board. We do not re-seed the random number generator but still randomly generate a new map by calling rand_land_create().

The User Interface

Users interact with the game via a remote desktop connection to a computer in Phillips Hall. The user opens up the computer’s camera app, and an external camera is pointed at the TFT display. The user then runs the C code on the PIC32 and Python code on the computer also via remote desktop. When playing the game, the Python GUI below is used.

The top two sliders on the interface are for setting the projectile power and angle. The two arrows are for moving the tank left and right, and the fire button fires the projectile. The “start” button allows users to start the very first game when they are on the start screen. The new game button allows users to start another game after one game ends. The user interacts with all of these items, and when any user input occurs, the inputs are sent from the Python to the PIC32 and handled by the serial thread. The “Serial data to PIC” and “Serial data from PIC” windows were left in for testing purposes. The data from the PIC allows us to verify that the serial line is connected.

Only one player can interact with the interface at a time depending on whose turn it is, and this is displayed at the bottom as “player turn”. Other feedback displayed on the GUI is the number of remaining steps a player has for their current turn and the current health of each player. These components of the GUI are created by sending information from the PIC32 back to the Python interface. printf() statements in the C code send updated health, turn, and steps data to the Python as needed. They are encoded as a string containing an event number and new value that is decoded by the Python into which variable should be updated on the GUI.

What Didn’t Work

When we initially did terrain destruction, we tried to make the destruction area a circle rather than a flat decrease of nearby terrain heights. This would allow chunks of land to be removed such that there would be two land heights at the same x position (creating floating land above other land). This required significant change to our code, as our land array that determines where the tank is drawn only stores one y position corresponding to the top of the land. Fixing this would involve storing the land array as a 2D list, with valid and invalid y positions at each x.

We also considered drawing the tanks such that their orientation changed as the slope of the land changed. We decided it was better to keep the tanks perfectly horizontal at all times. This made the code much simpler and we liked the look of the tanks moving up the mountains in this fashion. Due to the random generation of the land and the potentially steep mountain peaks, we thought it would look silly for the tank to be driving almost straight up a mountain at a really steep angle. Implementing this feature would have required doing expensive matrix transformations on the coordinates of the tank triangle and cannon. Also, having sharp corners in the terrain as opposed to smooth, continuous terrain would have made the tanks’ orientation undefined at these points. However, our terrain generation algorithm always creates sharp corners, so this would have needed to be changed.

Hardware Description

In this lab, we used the built in hardware described in the diagram above. The only notable sections of the SECABB that were used were the PIC32 and the TFT display, which were connected through a single SPI channel. Also notable was the PIC32 and the Python interface, connected over UART. Finally, we used the PicKit3 programmer to load our code onto the PIC32.

Results

Our final product functions as we expected, is complete, and does not contain any bugs. It supports two players and is turn-based. It randomly generates terrain, and players can destroy terrain by shooting it. Players can aim their cannons and shoot with variable power; this power determines how much terrain is destroyed. A collision animation displays when a player is hit. Player movement is also limited to 5 movements per turn, and each player gets 3 health per game. All controls are accessible via the Python GUI, and player health and step information is displayed there as well. When one game ends, another game can be started by pressing a new game button.

Speed of Execution

Code speed was critical for this lab, because frame updates must occur at a rate of 30 FPS, or every 33.33 ms. If the frame rate drops below this threshold, the game will start to lag and look choppy to the user. The clock rate of the CPU is 40000000 clock cycles per second. This means that between each frame update there are (40000000 cycles/sec)*(1/30 sec) = 1333333 (1.3 million) cycles. Therefore, all computations between frame updates must occur in less than 1.3 million cycles. This is a generous number of cycles that is fairly manageable to achieve at all times. Nevertheless, keeping this limit in mind and practicing efficient coding is still important.

The primary method of speeding up the code was using the _Accum data type. The _Accum data type is 32 bits, but it has a fixed decimal point unlike the float data type. Having a fixed decimal point makes arithmetic with _Accums significantly faster than floating point arithmetic. These differences are shown below.

Between each frame update, few computations are taking place when there are no active projectiles. The user may aim the cannon or move the tank which will generate several computations, but they are not too intensive. For this reason, the tank’s x and y positions, cannon angle, and health are all ints since they do not need to be heavily optimized.

On the contrary, the projectile computations are far more intensive. Thus, the shell’s x and y positions and velocities are stored using the _Accum data type to save cycles on the computations. The power is also stored as an _Accum. The TFT is small, so any small errors due to fixed point arithmetic are not noticeable. As the chart shows, _Accums save 58 cycles for additions and 27 cycles for multiplications. They require 5 more cycles for divisions.

The critical path between frame updates is within the animate thread and can be approximated as follows: 1) detect an active projectile. 2) redraw the tank cannons. 3) erase the projectile and compute its new position and velocity. 4) check if the projectile is on the screen and redraw it. 5) check for a collision. 6) detect a collision with the land. 7) destroy the land. 8) redraw the tanks.

To ensure the code speed is sufficient, it must be shown that this critical path executes before the next frame update. Among the steps listed above, the most computationally intensive steps are 2, 3, 4, 7, and 8.

Step 2 consists of two TFT function calls. Each TFT function call requires SPI communication and requires approximately 200 cycles to complete. Therefore, step 2 requires about 400 cycles.

Step 3 consists of one TFT function call and three fixed-point additions. This corresponds to 200 + 3*2 = 206 cycles using the _Accum data type. Using floats would have required 3*58 = 174 additional cycles.

Step 4 consists of one TFT function call and requires about 200 cycles.

Step 7 consists of 40 TFT function calls, 20 fixed point multiplications, and 20 fixed point divisions. This requires 200*40 + 20*28 + 20*145= 11460 cycles. Using floats would have required 20*27 – 20*5 = 440 additional cycles.

Step 8 consists primarily of 8 TFT function calls, corresponding to 1600 cycles.

The results of this analysis are summarized below:

Note that this chart gives only an approximate calculation of the worst-case number of cycles between frame updates. Nevertheless, the calculation clearly shows that meeting the frame update deadline is not an issue since 13,866 cycles is far less than the 1.3 million cycles available. However, adding new features could very rapidly increase the number of cycles used due to nested loops, so having an existing infrastructure that contains _Accums is still important.

Since the game is very interactive, it is also important that the game quickly responds to button presses and slider movements. It indeed does, because the button and slider threads each get an opportunity to run between every frame update since there are so many cycles to spare. At 30 FPS, this makes user inputs appear to happen nearly instantaneously. Thus the concurrent thread implementation works very well.

Accuracy

A few accuracy concerns had to be addressed when designing the game. First of all, the game is played over remote desktop connection, meaning some lag is always present. For this reason, the game was designed such that small latency does not matter. By making the game turn-based with simple controls and no timing-dependent components, any momentary lag (whether large or small) does not adversely affect play.

The other accuracy concern was that to draw to the TFT display, the code must specify which pixel (or pixels) to draw to. The pixels must be specified as ints. However, when doing computations to simulate projectile motion, the calculations frequently end up generating floats and/or _Accums instead of ints. In our first implementation, we stored the shell’s x and y position as an int since its position had to be mapped to a pixel as an int. However, this made the projectile motion look unrealistic, because repeatedly casting and saving the position as an int caused a lot of round-off error over time. To solve this problem, the shell’s x and y positions were stored as _Accums instead of ints. These values were kept as _Accums at all times to prevent any round-off errors from happening. When writing to the TFT display, the _Accums were casted to ints in the function arguments, but they were not saved as ints in the tank struct. This way, the projectile motion calculations did not round-off to ints between each frame update. The graphics looked much more realistic and accurate after making this change. _Accums have 16 decimal places and were accurate enough for our purposes. Floats would have produced essentially the same graphics, but at the cost of more cycles.

Safety

This project does not have any major safety concerns, besides being dangerously fun. The project is entirely remote from the user, so the user can not be harmed in any way by playing it. In the lab, the project just consists of the PIC32 and TFT. There are not any moving parts or components that could harm anyone nearby.

Interference with other designs

Our project consists solely of software controlling the TFT display. It does not make noise, generate RF signals, etc. Therefore, there is no aspect of our project that would interfere with other projects in the lab.

Usability

The game was designed to be as user friendly as possible. The GUI has a simple, straightforward design with large, readable text and high-contrast colors. Similarly, the TFT display is simple and does not have any small text. People who are colorblind can still play the game since the only colors used on the TFT display are blue, black, and white. There are currently no sounds associated with the game, and even if there were, hearing these sounds would not be necessary to play. Blindness is the only condition that would absolutely prevent an individual from playing the game.

Conclusions

Meeting Expectations

Initially, we were daunted by how we were going to be able to draw these game elements on the TFT display, given how primitive and often close to the hardware the drawing functions were. However, once we became accustomed to the inverted y axis coordinate system, we were able to draw the game without any significant roadblocks in the creation process.

In our initial plan of the project, we set our first playable release as the two tanks firing projectiles at each other on a flat plane. Beyond this stable version of the game, we considered random terrain generation and terrain manipulation as stretch goals, among other stretch goals like variable weapon types or increased player movement options. We were excited to meet those first two stretch goals although they came with a few changes, which we liked for the final version of our game even though they did not match our original vision.

When creating sloped, randomized terrain for the game, we initially expected that we would have to do some complicated matrix transformations on the pixels for drawing the tanks to rotate them, but this prevented us from having discontinuous terrain that broke this transformation (since we used slope calculations). We decided to discard it because we preferred having discontinuous terrain segments and this also allowed us to have terrain destruction that created discontinuous patches in the ground, which we felt added more to the movement dynamics and added more interesting gameplay possibilities. Scaling terrain destruction with the projectile velocity also contributed to this decision.

If we made this project a second time, an aspiration would be to have a more accessible user interface, maybe utilizing the keyboard instead of elements in the python interface with clicks. In addition, having more robust terrain generation techniques for specific geological formations would be an interesting addition, as well as having different terrain manipulation methods other than the current methods that take scaled chunks out of the ground based on firepower.

Standards

Given the simplistic hardware approach to our project, the only applicable standards were the communication standards: RS-232 for UART communication between the python GUI and PIC32, and SPI for communication between the TFT display and the PIC32. Our design performed well and within the bounds of these standards with no conformity issues arising in our implementation.

Intellectual Property

The conception of our project was based on the Pocket Tanks artillery game from Blitwise productions, as well as numerous other turn-based artillery games dating back to the 1991 Scorched Earth by Microstar. However, the only imported elements of those games into our game was the genre of static artillery games, and we used no source code or intellectual property of any published videogame. Thus, there are no copyright issues with our game, nor are there any patent possibilities due to the nature of this game’s concept being already established.

As mentioned previously, we attribute intellectual property to Bruce Land and Hunter Adams for the remote interface setup and the serial threads setup, as well as to the authors of the various libraries we used.

Ethics

With our game not interacting with the public in any way or including any hardware elements that create a risk of injury, as well as holding paramount effective and cooperative teamwork in our development process, our decisions in this project follow the IEEE Code of Ethics.

In terms of safety, it is safe.

Schematic

Schmatic of Big Board used to connect hardware peripherals.

Source: TFTanks


About The Author

Muhammad Bilal

I am a highly skilled and motivated individual with a Master's degree in Computer Science. I have extensive experience in technical writing and a deep understanding of SEO practices.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.