Makowe przygody z elektroniką [#6]
W ramach podsumowania dotychczas zdobytej wiedzy tym razem stworzymy układ, który oprócz praktycznego wykorzystania wielu elementów zapewni nam sporo frajdy będąc równocześnie wciągającą grą zręcznościową. To najwyższy czas, aby pobawić się elektroniką, którą poznajemy.

Na początek skupmy się na cyfrowym pierwowzorze, który będziemy chcieli odtworzyć w naszym układzie. Mowa o grze Ready Steady Bang, która polega na pojedynku na czas reakcji pomiędzy dwójką graczy. Kto pierwszy wystrzeli z wirtualnego rewolweru po usłyszeniu sygnału zdobywa punkt.
Pojedyncza runda rozgrywa się w mgnieniu oka, ale potrafi wciągnąć ze względu na chęć doskonalenia techniki. W końcu chyba każdy chciałby wykazać się refleksem niczym kowboje na dzikim zachodzie.
Mając założenia dla naszej gry spróbujmy przełożyć je na świat elektroniki. Z całą pewnością będziemy potrzebować dwóch przycisków stykowych (po jednym dla każdego gracza), głośnika wydającego dźwięk startu rundy, wyświetlacza do prezentacji wyników oraz oczywiście mikrokontrolera, który zajmie się stosownymi obliczeniami czasu. Warto dodać również przyciski odpowiadające za przejście do kolejnej rundy i resetu do stanu pierwotnego.
Potrzebne materiały
- płytka Arduino (w przykładzie wykorzystam UNO)
- kabel USB do połączenia Arduino z Makiem
- wyświetlacz LCD (zgodny z HD44780)
- rezystor 220Ω
- rezystor 10kΩ
- 4x potencjometr 10kΩ
- 4x przełącznik stykowy
- głośnik piezoelektryczny (tzw. piezo)
- 2x płytka stykowa (opcjonalnie można zmieścić się też na jednej, ale kosztem komfortu grania)
- kabelki połączeniowe
Połączenie
Wyświetlacz
- pin 1 (VSS) wyświetlacza LCD do GND Arduino
- pin 2 (VDD) wyświetlacza LCD do 5V Arduino
- pin 3 (V0) wyświetlacza LCD do wyjścia (środkowego pinu) potencjometru 10kΩ
- pin 4 (RS) wyświetlacza LCD do D12 Arduino
- pin 5 (RW) wyświetlacza LCD do GND Arduino
- pin 6 (E) wyświetlacza LCD do D11 Arduino
- pin 7 (D0) wyświetlacza LCD bez połączenia
- pin 8 (D1) wyświetlacza LCD bez połączenia
- pin 9 (D2) wyświetlacza LCD bez połączenia
- pin 10 (D3) wyświetlacza LCD bez połączenia
- pin 11 (D4) wyświetlacza LCD do D5 Arduino
- pin 12 (D5) wyświetlacza LCD do D4 Arduino
- pin 13 (D6) wyświetlacza LCD do D3 Arduino
- pin 14 (D7) wyświetlacza LCD do D2 Arduino
- pin 15 (A) wyświetlacza LCD poprzez rezystor 220Ω do 5V Arduino
- pin 16 (K) wyświetlacza LCD do GND Arduino
Plansza do gry
- Gracz A: jedną stronę przełącznika stykowego podłączamy do 5V Arduino, a drugą poprzez rezystor 10kΩ do GND Arduino (masy) oraz bezpośrednio do do D6 Arduino
- Gracz B: jedną stronę przełącznika stykowego podłączamy do 5V Arduino, a drugą poprzez rezystor 10kΩ do GND Arduino (masy) oraz bezpośrednio do do D7 Arduino
- Przycisk DALEJ: jedną stronę przełącznika stykowego podłączamy do 5V Arduino, a drugą poprzez rezystor 10kΩ do GND Arduino (masy) oraz bezpośrednio do do D8 Arduino
- Przycisk RESET: jedną stronę przełącznika stykowego podłączamy do 5V Arduino, a drugą poprzez rezystor 10kΩ do GND Arduino (masy) oraz bezpośrednio do do D9 Arduino
Głośnik piezo
- nóżkę „+” do D10 Arduino
- nóżkę „-” do GND Arduino
Kod źródłowy
Starałem się możliwie szczegółowo komentować poszczególne linie kodu tak, aby całość była zrozumiała:
//Dołączamy niezbędne biblioteki: | |
#include <Chrono.h> //Chrono - odpowiedzialna za mierzenie czasu (http://github.com/SofaPirate/Chrono) | |
#include <LiquidCrystal.h> //LiquidCrystal - odpowiedzialna za obsługę wyświetlacza | |
int playerAButton = 6; //pin dla przełącznika gracza A | |
int playerBButton = 7; //pin dla przełącznika gracza B | |
int nextButton = 8; //pin dla przełącznika DALEJ | |
int resetButton = 9; //pin dla przełącznika RESET | |
int buzzer = 10; //pin dla głośniczka piezo | |
int playerAstartLives = 5; //ilość "żyć" startowych dla gracza A | |
int playerBstartLives = 5; //ilość "żyć" startowych dla gracza B | |
int playerALives = playerAstartLives; //przypisanie ilości startowych "żyć" gracza A do zmiennej, która będzie modyfikowana podczas gry | |
int playerBLives = playerBstartLives; //przypisanie ilości startowych "żyć" gracza B do zmiennej, która będzie modyfikowana podczas gry | |
int delayCorrection = 6; //korekcja w ms pomiędzy odczytem przycisku gracza A i B (wartość określona testowo, można modyfikować wedle potrzeby) | |
float playerAScore = 0.0; //zmienna przechowująca wynik czasowy dla gracza A | |
float playerBScore = 0.0; //zmienna przechowująca wynik czasowy dla gracza B | |
bool playerAStop = false; //zmienna przechowująca informacje o zakończeniu rundy przez gracza A | |
bool playerBStop = false; //zmienna przechowująca informacje o zakończeniu rundy przez gracza B | |
int randomMaxDelay = 3000; //zmienna przechowująca wartość maksymalnego losowego interwału czasowego do sygnału BANG (wyrażona ms, dodawana do standardowych 2s) | |
//Tworzymy instancje obiektów biblioteki Chrono | |
Chrono playerATimer; //czas dla gracza A | |
Chrono playerBTimer; //czas dla gracza B | |
//Podajemy numery cyfrowych gniazd Arduino do których podłączyliśmy wyświetlacz | |
LiquidCrystal lcd(12, 11, 5, 4, 3, 2); | |
//Deklarujemy znaki odpowiedialne za prezentację ilości trafień (1 to piksel zapełniony, 0 to piksel pusty) | |
byte full[8] = { | |
B11111, | |
B11111, | |
B11111, | |
B11111, | |
B11111, | |
B11111, | |
B11111, | |
B11111, | |
}; | |
byte empty[8] = { | |
B11111, | |
B10001, | |
B10001, | |
B10001, | |
B10001, | |
B10001, | |
B10001, | |
B11111, | |
}; | |
void setup() | |
{ | |
Serial.begin(9600); //uruchamiamy połączenie serialowe na potrzeby testowania gry | |
//ustawiamy piny Arduino skojarzone z przełącznikami stykowymi jako wejściowe: | |
pinMode(playerAButton, INPUT); | |
pinMode(playerBButton, INPUT); | |
pinMode(nextButton, INPUT); | |
pinMode(resetButton, INPUT); | |
lcd.createChar(0, full); //przygotowanie utworzonego znaku "pełnego trafienia" | |
lcd.createChar(1, empty); //przygotowanie utworzonego znaku "pustego trafienia" | |
lcd.begin(16, 2); //ustawiamy ilości kolumn i lini naszego wyświetlacza LCD | |
startText(); //wywołujemy naszą funkcję odpowiedzialną za wyświetlenie tesktu startowego | |
} | |
void loop() | |
{ | |
//ustawiamy zmienne przechowujące stan przełączników stykowych: | |
int playerAButtonState = digitalRead(playerAButton); | |
int playerBButtonState = digitalRead(playerBButton); | |
int nextButtonState = digitalRead(nextButton); | |
int resetButtonState = digitalRead(resetButton); | |
//instrukcja wywoływana po wciśnięciu przycisku gracza A | |
if (playerAButtonState == 1) { | |
while (digitalRead(playerAButton)); //zabezpieczenie przed trzymaniem wciśniętego przycisku (pętla będzie wykonywała się tak długo, aż przycisk nie zostanie zwolniony) | |
stopChronoA(); //wywołanie funkcji odpowiedzialnej za zatrzymanie pomiaru dla gracza A | |
} | |
//instrukcja wywoływana po wciśnięciu przycisku gracza B | |
if (playerBButtonState == 1) { | |
while (digitalRead(playerBButton)); //zabezpieczenie przed trzymaniem wciśniętego przycisku (pętla będzie wykonywała się tak długo, aż przycisk nie zostanie zwolniony) | |
stopChronoB(); //wywołanie funkcji odpowiedzialnej za zatrzymanie pomiaru dla gracza B | |
} | |
//instrukcja na wypadek wciśnięcia przycisku DALEJ: | |
if (nextButtonState == 1) { | |
while (digitalRead(nextButton)); //zabezpieczenie przed trzymaniem wciśniętego przycisku (pętla będzie wykonywała się tak długo, aż przycisk nie zostanie zwolniony) | |
Serial.println(); | |
showIntro(); //wywołanie funkcji odpowiedzialej za sekwencję startową | |
startChrono(); //wywołanie funkcji odpowiedzialnej za rozpoczęcie odliczania | |
} | |
//instrukcja na wypadek wciśnięcia przycisku RESET | |
if (resetButtonState == 1) { | |
while (digitalRead(resetButton)); //zabezpieczenie przed trzymaniem wciśniętego przycisku (pętla będzie wykonywała się tak długo, aż przycisk nie zostanie zwolniony) | |
resetPlay(); //wywołanie funkcji odpowiedzialnej za zresetowanie gry | |
} | |
//instrukcja wywoływana gdy gracze skończą turę: | |
if (playerAStop == true & playerBStop == true) { | |
showResult(); //wywołanie funkcji odpowiedzialnej za prezetnację wników | |
playerAStop = false; //wyzerowanie flagi zatrzymania | |
playerBStop = false; //wyzerowanie flagi zatrzymania | |
} | |
} | |
//funkcja odpowiedzialna za wyświetlenie sekwencji startowej: | |
void showIntro() { | |
lcd.clear(); //wyczyszczenie zawartości wyświetlacza | |
//instrukcja wywoływana, gdy któryś z graczy osiągnie 0 w ilości "żyć" | |
if (playerALives == 0 | playerBLives == 0) { | |
theEnd(); //wywołanie funkcji zakończenia gry | |
} | |
else { | |
Serial.println("-- NA MIEJSCA --"); | |
lcd.setCursor(0, 0); | |
lcd.print("-- NA MIEJSCA --"); | |
delay(200); | |
tone(buzzer, 1000, 400); // Send 1KHz sound signal... | |
lcd.setCursor(0, 1); | |
lcd.print("A:"); | |
for (int i = 0; i < playerALives; i++) { | |
lcd.write(byte(0)); | |
} | |
for (int i = 0; i < (5 - playerALives); i++) { | |
lcd.write(byte(1)); | |
} | |
lcd.print(" B:"); | |
for (int i = 0; i < playerBLives; i++) { | |
lcd.write(byte(0)); | |
} | |
for (int i = 0; i < (5 - playerBLives); i++) { | |
lcd.write(byte(1)); | |
} | |
delay(1500); | |
tone(buzzer, 1000, 300); // Send 1KHz sound signal... | |
Serial.println("---- GOTOWI ----"); | |
lcd.setCursor(0, 0); | |
lcd.print("---- GOTOWI ----"); | |
delay(2000); | |
int randomDelay = random(randomMaxDelay); | |
delay(randomDelay); | |
Serial.print("(Wartosc losowo doliczonego czasu: "); | |
Serial.print(randomDelay); | |
Serial.println("ms)"); | |
tone(buzzer, 1000, 200); // Send 1KHz sound signal... | |
Serial.println("----- BANG -----"); | |
lcd.setCursor(0, 0); | |
lcd.print("----- BANG -----"); | |
} | |
} | |
//funkcja odpowiedzialna za wyświetlenie tekstu startowego: | |
void startText() { | |
Serial.println("MyApple BANG!"); | |
lcd.setCursor(2, 0); // ustawiamy kursor w 3 kolumnie i 1 lini (linie numerowane są od 0) | |
lcd.print("MyApple BANG!"); //wysyłamy tekst na wyświetlacz | |
Serial.println("Nacisnij DALEJ"); | |
lcd.setCursor(1, 1); // ustawiamy kursor w 2 kolumnie i 2 lini (linie numerowane są od 0) | |
lcd.print("Nacisnij DALEJ"); //wysyłamy tekst na wyświetlacz | |
} | |
//funkcja rozpoczynająca odliczanie | |
void startChrono() { | |
playerAScore = 0.0; | |
playerBScore = 0.0; | |
playerAStop = false; | |
playerBStop = false; | |
playerATimer.restart(); //restartujemy (wznawiamy) licznik dla gracza A | |
playerBTimer.restart(); //restartujemy (wznawiamy) licznik dla gracza B | |
} | |
//funkcja wywoływana po naciśnięciu przycisku gracza A | |
void stopChronoA() { | |
playerATimer.stop(); //zatrzymujemy licznik dla gracza A | |
playerAScore = playerATimer.elapsed() / 1000.00; //konwertujemy wynik do z ms do s | |
Serial.print("Wynik gracza A: "); | |
Serial.print(playerAScore); | |
Serial.print("s ("); | |
Serial.print(playerATimer.elapsed()); | |
Serial.println("ms)"); | |
lcd.setCursor(0, 0); | |
lcd.print("Gracz A: "); | |
lcd.print(playerAScore); | |
lcd.print("s "); | |
playerAStop = true; | |
} | |
//funkcja wywoływana po naciśnięciu przycisku gracza B | |
void stopChronoB() { | |
playerBTimer.stop(); //zatrzymujemy licznik dla gracza A | |
playerBScore = (playerBTimer.elapsed() - delayCorrection) / 1000.00; //konwertujemy wynik do z ms do s odejmując wartość korekty | |
Serial.print("Wynik gracza B: "); | |
Serial.print(playerBScore); | |
Serial.print("s ("); | |
Serial.print(playerBTimer.elapsed()); | |
Serial.println("ms)"); | |
lcd.setCursor(0, 1); | |
lcd.print("Gracz B: "); | |
lcd.print(playerBScore); | |
lcd.print("s "); | |
playerBStop = true; | |
} | |
//funkcja odpowiedzialna za wyświetlenie wyników | |
void showResult() { | |
lcd.setCursor(0, 0); | |
lcd.print("Gracz A: "); | |
lcd.print(playerAScore); | |
lcd.print("s "); | |
lcd.setCursor(0, 1); | |
lcd.print("Gracz B: "); | |
lcd.print(playerBScore); | |
lcd.print("s "); | |
if (playerAScore < playerBScore) { | |
lcd.setCursor(15, 0); | |
lcd.write(byte(0)); | |
lcd.setCursor(15, 1); | |
lcd.write(byte(1)); | |
playerBLives--; //odejmujemy "życie" gracza B | |
} | |
else if (playerBScore < playerAScore) { | |
lcd.setCursor(15, 0); | |
lcd.write(byte(1)); | |
lcd.setCursor(15, 1); | |
lcd.write(byte(0)); | |
playerALives--; //odejmujemy "życie" gracza A | |
} | |
//instrukcja dla awaryjnej sytuacji w której gracze zremisują: | |
else { | |
lcd.setCursor(15, 0); | |
lcd.write(byte(0)); | |
lcd.setCursor(15, 1); | |
lcd.write(byte(0)); | |
playerALives--; //odejmujemy "życie" gracza A | |
playerBLives--; //odejmujemy "życie" gracza B | |
} | |
} | |
//funkcja odpowiedzialna za reset rozgrywki do punktu startowego | |
void resetPlay() { | |
lcd.clear(); | |
playerALives = playerAstartLives; | |
playerBLives = playerBstartLives; | |
playerATimer.stop(); | |
playerBTimer.stop(); | |
playerAStop = false; | |
playerBStop = false; | |
startText(); | |
} | |
//funkcja odpowiedzialna za wyświetlenie podsumowania rozgrywki | |
void theEnd() { | |
lcd.clear(); | |
lcd.setCursor(0, 0); | |
lcd.print("Zwyciezca "); | |
if (playerALives == 0) { | |
lcd.print("B("); | |
lcd.print(playerBLives); | |
lcd.print(":0)"); | |
} | |
else { | |
lcd.print("A("); | |
lcd.print(playerALives); | |
lcd.print(":0)"); | |
} | |
lcd.setCursor(1, 1); | |
lcd.print("Nacisnij RESET"); | |
} |
Bardziej zaawansowani elektronicy oraz programiści z pewnością będą w stanie zoptymalizować powyższy kod dodając obsługę przerwań lub zabezpieczając poszczególne etapy przed niespodziewanym naciśnięciem przycisku. Ja starałem się napisać go możliwie prostym językiem tak, aby jego poziom był adekwatny do aktualnie poznanej wiedzy w ramach cyklu „Makowe przygody z elektroniką”.
Bibliografia:
Strona biblioteki LiquidCrystal
Gra Ready Steady Bang w AppStore