Skip to content

Software Development

Samuel Asebrook edited this page May 18, 2024 · 2 revisions

Software Overview

This project was fulfilled using a parallel approach; as hardware was decided on and developed, as was the software to tie the entire system together. The result was a cohesive program that features maximum compatibility and expert accuracy. Changes were made throughout the process, but the structure of the code retained its form and evolved.

Hardware Initialization and Operation

As discussed in the Hardware Development section of the Wiki, the hardware truly is what makes all of this possible. There were multiple components that needed to be coded into functionality.

Code for Start/End Components

Since the optical sensor that activates the scoring system and the limit switch that termites it communicates through signal wiring, setting them up was very straightforward. To start, before the setup loop, the pin for the communication from the optical sensor was established:

const int opticalSensorPin = A0;

And then the same was done with the pin for the communication from the limit switch:

const int limitSwitchPin = 2;

And then within the setup loop, their roles on the Arduino Mega were established:

void setup() {

...

pinMode(opticalSensorPin, INPUT);

pinMode(limitSwitchPin, INPUT_PULLUP);

}

Their respective activations either start or terminate the scoring logic, which will be explained shortly.

Code For Screens

While the differences in the code will be explained shortly, it is necessary to mention the code that assembled the screens and allowed for them to communicate their fun messages and score information. Originally, the LCD I2C used was initialized using the LiquidCrystal_I2C library:

LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 16, 2);

And then fully compiled in the setup loop with a message to the player to get them excited to play:

void setup() {

lcd.init();

lcd.backlight();

lcd.setCursor(0, 0);

lcd.print("Hell Yeah!");

lcd.setCursor(0, 1);

lcd.print("Pinball!");

...

}

Later on in the code, both when game-play is started and game-play is concluded, the screen is used to convey information. For example:

lcd.clear();

lcd.setCursor(0, 0);

lcd.print("Score: ");

lcd.print(score);

lcd.setCursor(0, 1);

lcd.print("High: ");

lcd.print(highScore);

As will be discussed shortly, the code was transitioned from LCD I2C to Adafruit SSD1306 OLED panels to fix voltage constraints.

Score-Keeping

For the pinball game in its entirety, score is calculated off of two factors:

  • Time spent actively playing

  • Amount of obstacles struck

When earlier versions of the code were pushed, time spent playing was the only factor in deciding a player's score, which was strictly for development purposes as striking obstacles was always intended to contribute to the score as well.

Score Contributions From Time Spent Playing

When the optical sensor that determines if the launch arm has been pulled is activated, the system enters a state that is controlled by a boolean variable for whether or not the game has started. Such a state begins a timer that measures how long is spent playing the game:

unsigned long startTime;

unsigned long elapsedTime;

bool gameStarted = false;

...

if (opticalSensorState == HIGH && !gameStarted) {

// The launcher has been pulled back, start the game

startTime = millis();

gameStarted = true;

display.clearDisplay();

display.setTextSize(2);

display.setTextColor(SSD1306_WHITE);

display.setCursor(0,0);

display.println("Good Luck!")

display.setCursor(0,16);

display.println("You Will Need It!");

display.display();

delay(1000);

}

Once the limit switch that detects when the ball leaves the play field is activated, the system enters a state that is controlled by the same boolean variable for whether or not the game has started. Such a state terminates the timer that measures how long is spent playing the game, calculates the difference, adds to the score using a ratio of 10 points for every second, and displays both the player's score from that round and the high score for the game at that time:

if (gameStarted && limitSwitchState == LOW) {

// The limit switch has been pressed, end the game

elapsedTime = millis() - startTime;

gameStarted = false;

// Calculate the score

score = score + (elapsedTime / 100);

if (score > highScore) {

highScore = score;

}

display.clearDisplay();

display.setTextSize(2);

display.setTextColor(SSD1306_WHITE);

display.setCursor(0,0);

display.print("Score: ");

display.println(score);

display.setCursor(0,16);

display.print("High: ");

display.println(highScore);

...

}

Once the scores are properly displayed, the score is reset to 0, and the score multiplier is reset to 1.

Score Contributions From Obstacles Struck

As discussed in the Hardware Development section of the Wiki, striking obstacles contributes to a player's score. There are four obstacles, each with a "health" that is equal to four strikes. The first strike on an obstacle contributes 100 points to the player's score, the second strike contributes 200 points, the third strike contributes 300 points, and the fourth strike contributes 1000 points. If the player manages to fully deplete the "health" of every obstacle on the play field, then all obstacles' "health" resets and score contributions resulting from collisions double. This detection, state, and calculation routine is managed by a boolean that alternates from false to true and back on repeat and an if statement to determine if a collision has occurred (such code was not my responsibility, my responsibility was to contribute to the player's score accordingly):

if ((state_1 == 1 && state1_moved == false) || (state_2 == 1 && state2_moved == false) || (state_3 == 1 && state3_moved == false) || (state_4 == 1 && state4_moved == false) ) {

score = score + (multiplier * 100);

if (state_1 == 1) {

state1_moved = true;

} else if (state_2 == 1) {

state2_moved = true;

} else if (state_3 == 1) {

state3_moved = true;

} else if (state_4 == 1) {

state4_moved = true;

}

}

The alternating boolean variable allows for the state machine to determine if the obstacle's health was affected and the system did not know. If this is the case, then the score is manipulated accordingly and the boolean is switched to prevent continuous score addition:

if ((state_1 == 2 && state1_moved == true) || (state_2 == 2 && state2_moved == true) || (state_3 == 2 && state3_moved == true) || (state_4 == 2 && state4_moved == true) ) {

score = score + (multiplier * 200);

if (state_1 == 2) {

state1_moved = false;

} else if (state_2 == 2) {

state2_moved = false;

} else if (state_3 == 2) {

state3_moved = false;

} else if (state_4 == 2) {

state4_moved = false;

}

}

This alternation continues until each obstacle has been collided with four times. When this occurs, as described earlier, the "health" of every obstacle is reset and score contributions due to collisions are doubled:

if (state_1 == 4 && state_2 == 4 && state_3 == 4 && state_4 == 4) {

state_1 == 0;

state_2 == 0;

state_3 == 0;

state_4 == 0;

multiplier = multiplier * 2;

}

Once the game is terminated, the score from the time spent playing is added to the score accumulated from colliding with obstacles in the play field.

Optimizations

Throughout the coding and development process, a few optimizations were made to better allow the code to do its job, which also results in a more positive player experience.

Score Calculation and Printing

In early pushes, there were two issues with scores. First, there was a bug that resulted in the high score not always being properly displayed. Second, there was an uber-inflation of scores that was occurring. The bug was fixed in a two-step method. First, a function was generated to scroll text from right to left across the screen so that longer numbers could be properly displayed for the user:

void scrollText(int row, String message, int delayTime) {

message = message + " ";

for (int i = 0; i < message.length(); i++) {

lcd.setCursor(0, row);

lcd.print(message.substring(i));

delay(delayTime);

}

}

Second, the message that was used to display the high score was changed from "High Score: " to "High: ", which allowed for more place-values to be displayed for the high score, subsequently decreasing the need for the function. However, it remained implemented as a backup:

if (String(highScore).length() >= 11) {

scrollText(1, " High: " + String(highScore), 500);

}

The uber-inflated scores were dealt with by reducing the ratio of seconds spent playing to points earned to 10 points per second instead of the original 100 points per second.

Screen Types

Originally, this system was coded to utilize LCD I2C to display information. I kept running into an issue when connecting the scoring system to the Arduino Mega that controls the entire system that the screen would not always be able to fully display information. This was the result of a voltage issue, so we changed the hardware and the code from LCD I2C to Adafruit SSD1306 OLED panels. This resulted in an ability to display whatever information necessary and not have to stress voltage constraints within the system.

Game-Start Detection

Original design plans (as outlined in group Wiki) utilized optical sensors to start the game. Through actual building and implementation of the entire system, it was found to be more practical to utilize a limit switch to conduct this action instead.