[176] Pong na oscyloskopie cz. 2

02.png

Powróćmy do naszego Ponga i uczyńmy go ciekawszym. Zaczniemy od wpływu miejsca spotkania piłki z paletką na kąt jej odbicia. Jeśli piłka trafi w jej środek – odbije się poziomo. Im miejsce spotkania piłki będzie dalsze od środka, tym większy będzie kąt odbicia. Pozwoli nam to zgrabnie podkręcać piłkę i w końcu ogrywać komputer.

#include "pwm.h"                            // Biblioteka PWM dla architektury Uno R4
const byte pot = A1;                        // Port potencjometru sterującego prawą paletką (gracz).
PwmOut pwmX(10);                            // Obiekt sterujący sprzętowym PWM dla pinu 10 (oś X).
PwmOut pwmY(9);                             // Obiekt sterujący sprzętowym PWM dla pinu 9 (oś Y).
const byte paddleLength = 8;                // Połowa szerokości paletki.
const byte paddleTop = 100 - paddleLength;  // Górna granica ruchu środka paletki.
byte paddleLCenterY = 50;                   // Środek lewej paletki (komputera).
byte paddleRCenterY = 50;                   // Środek prawej paletki (gracza).
int ballX = 50;                             // Pozycja X piłki.
int ballY = 50;                             // Pozycja Y piłki.
int ballXSpeed = 2;                         // Prędkość pozioma piłki i jej kierunek.
int ballYSpeed = 1;                         // Prędkość pionowa piłki i jej kierunek.
unsigned long prevMillis = 0;               // Czas poprzedniego stanu zegara systemowego.
const unsigned long refresh = 20;           // Czas odświeżania logiki gry w ms.

void setup() {
  pwmX.begin(480000.0f, 0.0f);  // Częstotliwość pracy X = 480 kHz = 100 kroków.
  pwmY.begin(480000.0f, 0.0f);  // Częstotliwość pracy Y = 480 kHz = 100 kroków.
}
void loop() {
  unsigned long currentMillis = millis();       // Sczytaj czas systemowy.
  if (currentMillis - prevMillis >= refresh) {  // Jeśli minął czas trwania jednej klatki gry...
    prevMillis = currentMillis;                 // Przenieś sczytany czas systemowy do zmiennej do porównań.

    paddleRCenterY = map(analogRead(pot), 0, 1023, paddleLength, paddleTop);  // Oblicz położenie środka prawej paletki (gracza).

    ballX += ballXSpeed;  // Wyznacz nowy adres X piłki.
    ballY += ballYSpeed;  // Wyznacz nowy adres Y piłki.

    if (ballY > paddleLCenterY && paddleLCenterY < paddleTop) {            // Jeśli piłka znajduje się powyżej środka lewej paletki...
      paddleLCenterY++;                                                    // Przesuń lewą paletkę (komputera) w górę.
    } else if (ballY < paddleLCenterY && paddleLCenterY > paddleLength) {  // Jeśli piłka znajduje się poniżej środka lewej paletki...
      paddleLCenterY--;                                                    // Przesuń lewą paletkę (komputera) w dół.
    }
    if (ballY >= 100) {          // Jeśli piłka osiągnęła górną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w dół.
      ballY = 100;               // W razie przekroczenia zakresu ustaw piłkę na górze ekranu
    } else if (ballY <= 0) {     // Jeśli piłka osiągnęła dolną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w górę.
      ballY = 0;                 // W razie przekroczenia zakresu ustaw piłkę na dole ekranu.
    }
    if (ballXSpeed < 0 && ballX <= 0 && abs(ballY - paddleLCenterY) <= paddleLength) {           // Odbij piłkę jeśli...
      ballYSpeed = (ballY - paddleLCenterY) / 2;                                                 // Wyznacz nowy kąt z przesunięcia od środka paletki komputera.
      ballXSpeed = -ballXSpeed;                                                                  // Zmień kierunek jej poruszania.
    } else if (ballXSpeed > 0 && ballX >= 100 && abs(ballY - paddleRCenterY) <= paddleLength) {  // Odbij piłkę jeśli...
      ballYSpeed = (ballY - paddleRCenterY) / 2;                                                 // Wyznacz nowy kąt z przesunięcia od środka paletki gracza.
      ballXSpeed = -ballXSpeed;                                                                  // Zmień kierunek jej poruszania.
    }
    if (ballX < 0 || ballX > 100) {  // Jeśli piłka nie została przechwycona paletką (znalazła się poza ekranem w osi X)...
      ballYSpeed = 1;                // Zresetuj prędkość pionową do wartości bazowej.
      ballX = 50;                    // Centruj jej ustawienie.
      ballY = 50;
    }
  }
  drawPaddle(0, paddleLCenterY);    // Rysuj lewą paletkę (komputera).
  drawPaddle(100, paddleRCenterY);  // Rysuj prawą paletkę (gracza).

  pwmX.pulse_perc(ballX);   // Wyślij adres X pozycji piłki.
  pwmY.pulse_perc(ballY);   // Wyślij adres Y pozycji piłki.
  delayMicroseconds(1000);  // Wprowadź opóźnienie dające wyraźny rysunek piłki.
}
void drawPaddle(byte positionX, byte centerY) {  // Rysowanie paletki.
  byte beginY = centerY - paddleLength;          // Oblicz adres Y początku paletki.
  byte endY = centerY + paddleLength;            // Oblicz adres Y końca paletki.
  pwmX.pulse_perc(positionX);                    // Wyślij adres X początku paletki.
  for (byte n = beginY; n <= endY; n++) {        // Rysuj paletkę, dobierz liczbę kroków do czytelności.
    pwmY.pulse_perc(n);                          // Stawiaj kolejne kropki na osi Y.
    delayMicroseconds(1000);                     // Wprowadź opóźnienie dające wyraźny rysunek paletki.
  }
}

Cała modyfikacja sprowadzi się do trzech wpisów: w procedurze analizy zderzeń piłki z paletkami dopiszemy linie, które zmienią wartość zmiennej odpowiedzialnej za ruch piłki w pionie. Współczynnik ballYSpeed określi stopień zmiany kąta odbicia i możemy go dobrać do preferencji własnych. Dodatkowo po stracie punktu będziemy resetować tę wartość. I to wszystko, teraz gra stała się ciekawsza – można próbować pierwszych strategii. Dopiszmy kolejną funkcję: teraz piłka przyspieszy po dziesiątym odbiciu.

#include "pwm.h"                            // Biblioteka PWM dla architektury Uno R4
const byte pot = A1;                        // Port potencjometru sterującego prawą paletką (gracz).
PwmOut pwmX(10);                            // Obiekt sterujący sprzętowym PWM dla pinu 10 (oś X).
PwmOut pwmY(9);                             // Obiekt sterujący sprzętowym PWM dla pinu 9 (oś Y).
const byte paddleLength = 8;                // Połowa szerokości paletki.
const byte paddleTop = 100 - paddleLength;  // Górna granica ruchu środka paletki.
byte paddleLCenterY = 50;                   // Środek lewej paletki (komputera).
byte paddleRCenterY = 50;                   // Środek prawej paletki (gracza).
int ballX = 50;                             // Pozycja X piłki.
int ballY = 50;                             // Pozycja Y piłki.
int ballXSpeed = 2;                         // Prędkość pozioma piłki i jej kierunek.
int ballYSpeed = 1;                         // Prędkość pionowa piłki i jej kierunek.
unsigned long prevMillis = 0;               // Czas poprzedniego stanu zegara systemowego.
const unsigned long refresh = 20;           // Czas odświeżania logiki gry w ms.
int ballCount = 0;                          // Licznik odbić piłki od paletek.

void setup() {
  pwmX.begin(480000.0f, 0.0f);  // Częstotliwość pracy X = 480 kHz = 100 kroków.
  pwmY.begin(480000.0f, 0.0f);  // Częstotliwość pracy Y = 480 kHz = 100 kroków.
}
void loop() {
  unsigned long currentMillis = millis();       // Sczytaj czas systemowy.
  if (currentMillis - prevMillis >= refresh) {  // Jeśli minął czas trwania jednej klatki gry...
    prevMillis = currentMillis;                 // Przenieś sczytany czas systemowy do zmiennej do porównań.

    paddleRCenterY = map(analogRead(pot), 0, 1023, paddleLength, paddleTop);  // Oblicz położenie środka prawej paletki (gracza).

    ballX += ballXSpeed;  // Wyznacz nowy adres X piłki.
    ballY += ballYSpeed;  // Wyznacz nowy adres Y piłki.

    if (ballY > paddleLCenterY && paddleLCenterY < paddleTop) {            // Jeśli piłka znajduje się powyżej środka lewej paletki...
      paddleLCenterY++;                                                    // Przesuń lewą paletkę (komputera) w górę.
    } else if (ballY < paddleLCenterY && paddleLCenterY > paddleLength) {  // Jeśli piłka znajduje się poniżej środka lewej paletki...
      paddleLCenterY--;                                                    // Przesuń lewą paletkę (komputera) w dół.
    }
    if (ballY >= 100) {          // Jeśli piłka osiągnęła górną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w dół.
      ballY = 100;               // W razie przekroczenia zakresu ustaw piłkę na górze ekranu
    } else if (ballY <= 0) {     // Jeśli piłka osiągnęła dolną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w górę.
      ballY = 0;                 // W razie przekroczenia zakresu ustaw piłkę na dole ekranu.
    }
    if (ballXSpeed < 0 && ballX <= 2 && abs(ballY - paddleLCenterY) <= paddleLength) {          // Odbij piłkę jeśli...
      ballYSpeed = (ballY - paddleLCenterY) / 2;                                                // Wyznacz nowy kąt z przesunięcia od środka paletki komputera.
      hitBall();                                                                                // Dokończ działania w podprogramie.
    } else if (ballXSpeed > 0 && ballX >= 98 && abs(ballY - paddleRCenterY) <= paddleLength) {  // Odbij piłkę jeśli...
      ballYSpeed = (ballY - paddleRCenterY) / 2;                                                // Wyznacz nowy kąt z przesunięcia od środka paletki gracza.
      hitBall();                                                                                // Dokończ działania w podprogramie.
    }
    if (ballX < 0 || ballX > 100) {       // Jeśli piłka nie została przechwycona paletką (znalazła się poza ekranem w osi X)...
      ballYSpeed = 1;                     // Zresetuj prędkość pionową.
      ballXSpeed = (ballX < 0) ? -2 : 2;  // Zresetuj prędkość poziomą i kierunek do przegranego.
      ballCount = 0;                      // Zresetuj licznik odbić piłki dla nowej rundy.
      ballX = 50;                         // Centruj jej ustawienie.
      ballY = 50;
    }
  }
  drawPaddle(0, paddleLCenterY);    // Rysuj lewą paletkę (komputera).
  drawPaddle(100, paddleRCenterY);  // Rysuj prawą paletkę (gracza).

  pwmX.pulse_perc(ballX);   // Wyślij adres X pozycji piłki.
  pwmY.pulse_perc(ballY);   // Wyślij adres Y pozycji piłki.
  delayMicroseconds(1000);  // Wprowadź opóźnienie dające wyraźny rysunek piłki.
}
void drawPaddle(byte positionX, byte centerY) {  // Rysowanie paletki.
  byte beginY = centerY - paddleLength;          // Oblicz adres Y początku paletki.
  byte endY = centerY + paddleLength;            // Oblicz adres Y końca paletki.
  pwmX.pulse_perc(positionX);                    // Wyślij adres X początku paletki.
  for (byte n = beginY; n <= endY; n++) {        // Rysuj paletkę, dobierz liczbę kroków do czytelności.
    pwmY.pulse_perc(n);                          // Stawiaj kolejne kropki na osi Y.
    delayMicroseconds(1000);                     // Wprowadź opóźnienie dające wyraźny rysunek paletki.
  }
}
void hitBall() {
  ballXSpeed = -ballXSpeed;              // Zmień kierunek poruszania piłki.
  ballCount++;                           // Zwiększ licznik odbić.
  if (ballCount == 10) ballXSpeed *= 2;  // Jeśli to dziesiąte odbicie, podwój szybkość poruszania się piłki.
}

Dla ułatwienia przyspieszy raz, dwukrotnie, ale oczywiście można tę opcję wprowadzać płynnie i bez ograniczeń. Powołamy do życia licznik odbić piłki ballCount. Po każdym odbiciu będzie zwiększał wartość i z chwilą, gdy osiągnie dziesięć, zmienna ballXSpeed – szybkości poruszania się piłki w poziomie podwoi się, czyniąc grę trudniejszą. Po utracie punktu z powrotem powróci do wartości domyślnej. Dodatkowo wprowadziłem instrukcję, która rozpocznie kolejny set w kierunku przegranego: wartość startowa będzie ujemna bądź dodatnia w zależności od tego, w którym kierunku zdążała piłka przed utratą punktu.

No, można już pograć naprawdę, ale przydałoby się, by komputer wykazał jakiś pierwiastek humanizmu. Skupimy się nad trzema elementami: nadamy komputerowemu partnerowi trochę cech ludzkich, zajmiemy się grafiką i sprawimy, że Pong przemówi. Gdy partnerem w tego typu grach był komputer, szybko pojawiała się sztampa i powtarzalność. Najlepsi fachowcy od pisania takich aplikacji potrafili jednak wycisnąć z maszyny cechy człowiecze, a najlepiej użyć do tego funkcji losowych. Niech czasem robot się potknie, pomyli, ale też niech trochę uśmiechnie się do niego szczęście. Cóż, bierzmy się więc do pracy.

#include "pwm.h"                            // Biblioteka PWM dla architektury Uno R4
const byte pot = A1;                        // Port potencjometru sterującego prawą paletką (gracz).
PwmOut pwmX(10);                            // Obiekt sterujący sprzętowym PWM dla pinu 10 (oś X).
PwmOut pwmY(9);                             // Obiekt sterujący sprzętowym PWM dla pinu 9 (oś Y).
const byte paddleLength = 8;                // Połowa szerokości paletki.
const byte paddleTop = 100 - paddleLength;  // Górna granica ruchu środka paletki.
byte paddleLCenterY = 50;                   // Środek lewej paletki (komputera).
byte paddleRCenterY = 50;                   // Środek prawej paletki (gracza).
int ballX = 50;                             // Pozycja X piłki.
int ballY = 50;                             // Pozycja Y piłki.
int ballXSpeed = 2;                         // Prędkość pozioma piłki i jej kierunek.
int ballYSpeed = 1;                         // Prędkość pionowa piłki i jej kierunek.
unsigned long prevMillis = 0;               // Czas poprzedniego stanu zegara systemowego.
const unsigned long refresh = 20;           // Czas odświeżania logiki gry w ms.
int ballCount = 0;                          // Licznik odbić piłki od paletek.

void setup() {
  pwmX.begin(480000.0f, 0.0f);  // Częstotliwość pracy X = 480 kHz = 100 kroków.
  pwmY.begin(480000.0f, 0.0f);  // Częstotliwość pracy Y = 480 kHz = 100 kroków.
  randomSeed(analogRead(A0));   // Inicjalizacja generatora losowego szumem z pinu A0.
}
void loop() {
  unsigned long currentMillis = millis();                                     // Sczytaj czas systemowy.
  if (currentMillis - prevMillis >= refresh) {                                // Jeśli minął czas trwania jednej klatki gry...
    prevMillis = currentMillis;                                               // Przenieś sczytany czas systemowy do zmiennej do porównań.
    paddleRCenterY = map(analogRead(pot), 0, 1023, paddleLength, paddleTop);  // Oblicz położenie środka prawej paletki (gracza).
    ballX += ballXSpeed;                                                      // Wyznacz nowy adres X piłki.
    ballY += ballYSpeed;                                                      // Wyznacz nowy adres Y piłki.

    if (ballY > paddleLCenterY) {                                        // Jeśli piłka znajduje się powyżej środka lewej paletki...
      paddleLCenterY += random(1, 3);                                    // Przesuń lewą paletkę (komputera) w górę.
      if (paddleLCenterY > paddleTop) paddleLCenterY = paddleTop;        // Zabezpiecz się na wypadek przekroczenia górnego zakresu ekranu.
    } else if (ballY < paddleLCenterY) {                                 // Jeśli piłka znajduje się poniżej środka lewej paletki...
      paddleLCenterY -= random(1, 3);                                    // Przesuń lewą paletkę (komputera) w dół.
      if (paddleLCenterY < paddleLength) paddleLCenterY = paddleLength;  // Zabezpiecz się na wypadek przekroczenia dolnego zakresu ekranu (chroni przed znikaniem).
    }
    if (ballY >= 100) {          // Jeśli piłka osiągnęła górną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w dół.
      ballY = 100;               // W razie przekroczenia zakresu ustaw piłkę na górze ekranu
    } else if (ballY <= 0) {     // Jeśli piłka osiągnęła dolną krawędź...
      ballYSpeed = -ballYSpeed;  // Zmień kierunek poruszania się piłki w górę.
      ballY = 0;                 // W razie przekroczenia zakresu ustaw piłkę na dole ekranu.
    }
    if (ballXSpeed < 0 && ballX <= 2 && abs(ballY - paddleLCenterY) <= paddleLength) {          // Odbij piłkę jeśli leci w lewo i trafia w komputer...
      ballYSpeed = (ballY - paddleLCenterY + random(-4, 5)) / 2;                                // Wyznacz nowy kąt z przesunięcia od środka paletki komputera.
      hitBall();                                                                                // Dokończ działania w podprogramie.
    } else if (ballXSpeed > 0 && ballX >= 98 && abs(ballY - paddleRCenterY) <= paddleLength) {  // Odbij piłkę jeśli leci w prawo i trafia w gracza...
      ballYSpeed = (ballY - paddleRCenterY) / 2;                                                // Wyznacz nowy kąt z przesunięcia od środka paletki gracza.
      hitBall();                                                                                // Dokończ działania w podprogramie.
    }
    if (ballX < 0 || ballX > 100) {  // Jeśli piłka nie została przechwycona paletką (znalazła się poza ekranem w osi X)...
      ballYSpeed = random(-3, 4);    // Losuj kąt rzutu.
      ballCount = 0;                 // Zresetuj licznik odbić piłki dla nowej rundy.
      if (ballX < 0) {               // Jeśli wygrał gracz po prawej stronie...
        ballX = 98;                  // Ustaw piłkę przed paletką gracza.
        ballY = paddleRCenterY;      // Piłka startuje z wysokości środka paletki gracza.
        ballXSpeed = -2;             // Skieruj piłkę w lewo.
      } else {                       // Jeśli wygrał komputer po lewej stronie...
        ballX = 2;                   // Ustaw piłkę przed paletką komputera.
        ballY = paddleLCenterY;      // Piłka startuje z wysokości środka paletki komputera.
        ballXSpeed = 2;              // Skieruj piłkę w prawo.
      }
    }
  }
  drawPaddle(0, paddleLCenterY);    // Rysuj lewą paletkę (komputera).
  drawPaddle(100, paddleRCenterY);  // Rysuj prawą paletkę (gracza).
  pwmX.pulse_perc(ballX);           // Wyślij adres X pozycji piłki.
  pwmY.pulse_perc(ballY);           // Wyślij adres Y pozycji piłki.
  delayMicroseconds(1000);          // Wprowadź opóźnienie dające wyraźny rysunek piłki.
}
void drawPaddle(byte positionX, byte centerY) {  // Rysowanie paletki.
  int beginY = centerY - paddleLength;           // Oblicz adres Y początku paletki.
  int endY = centerY + paddleLength;             // Oblicz adres Y końca paletki.
  pwmX.pulse_perc(positionX);                    // Wyślij adres X początku paletki.
  for (int n = beginY; n <= endY; n++) {         // Rysuj paletkę, dobierz liczbę kroków do czytelności.
    pwmY.pulse_perc(n);                          // Stawiaj kolejne kropki na osi Y.
    delayMicroseconds(1000);                     // Wprowadź opóźnienie dające wyraźny rysunek paletki.
  }
}
void hitBall() {
  ballXSpeed = -ballXSpeed;              // Zmień kierunek poruszania piłki.
  ballCount++;                           // Zwiększ licznik odbić.
  if (ballCount == 10) ballXSpeed *= 2;  // Jeśli to dziesiąte odbicie, podwój szybkość poruszania się piłki.
}

Funkcja losowości pojawi się nam aż cztery razy. A właściwie pięć, bo zaczniemy od „zamieszania” bębnem losującym, czyli zainicjowania generatora pseudolosowego szumem z portu analogowego. Dobrym zwyczajem jest tak po prostu zrobić na wstępie. Pierwszą zmianą będzie losowe przyspieszenie ruchu paletki komputera. Od czasu do czasu popędzi za piłką dwukrotnie szybciej, dzięki czemu rzadziej będzie ją przepuszczał. Musimy jednak dołożyć dodatkowe warunki zabezpieczające nas przed znikaniem paletki, gdyby jej rysowanie rozpoczęło się poza układem współrzędnych – wszak teraz tak może się zdarzyć. Współczynniki przyspieszenia – podstawowy i dwukrotny – możemy poszerzyć, ale wówczas paletka będzie się zachowywać nerwowo i coraz trudniej będzie zdobyć punkt.

Kolejny element ludzki to zmiana kąta odbicia piłki przez komputer – również losowo. Dodajemy współczynnik niezależny od odległości piłki od środka paletki i teraz jej zachowanie będzie bardziej nieoczekiwane. Tym razem współczynnik przyjmuje także liczby ujemne, wszak odchylanie ma działać w obie strony. Zmienimy też warunki startowe. Nie tylko kąt pierwszego rzutu będzie się zmieniał, ale nie rozpocznie się od środka – jak dotąd, lecz od paletki tego, kto wygrał poprzednią rundę. Zwróćmy uwagę na to, że teraz pozostawiam wszędzie dwupikselowy margines bezpieczeństwa. Jak mówiłem, współczynniki należy dobrać tak, by komputer był dla nas sprawnym przeciwnikiem, ale równocześnie, żeby dało się go pokonać.

Po kompilacji można już całkiem serio się pobawić. Jest na tyle dobrze, że tutaj zakończę eksperymenty z logiką samej gry i skupię się na tym, co ładne. A ładne byłoby boisko – jakiś zarys choćby, żeby wiedzieć ile piłka ma jeszcze do krawędzi. Ale to już w kolejnym artykule serii.

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2

Inne artykuły z tej kategorii

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2026 arduino.pl