2022-02-24
W czwartej części poradnika kończymy tworzyć logikę odpowiedzialną za grę. Musimy wprawić w ruch piłeczkę oraz sprawić by odbijała się ona od paletki.
Kod naszego obecnego programu wygląda tak:
Kod programu po wykonaniu trzeciej części poradnika
By piłka mogła się poruszać konieczne będzie dodanie dwóch zmiennych, które będą określać kierunek, a jednocześnie prędkość na płaszczyznach X i Y.
Do naszej klasy Ball musimy dodać dwie zmienne - x_speed
i y_speed
by nasza klasa wyglądała tak:
class Ball
{
public:
int x, y,
int x_speed, y_speed;
};
Założenie jest proste — nasza piłka przesuwa się automatycznie po osi X i Y o wartość jej prędkości — co określony przedział czasowy (do przedziału czasowego dojdziemy nieco później). Ustawiając prędkość na 0, piłka nie będzie się poruszać, ustawiając prędkość na 1, piłka będzie się poruszać o jedną jednostkę w danym kierunku, ustawiając prędkość na -1, piłka będzie się poruszać o jedną jednostkę w odwrotnym kierunku.
Odbijanie piłki będzie się odbywać poprzez sprawdzenie, czy piłka graniczy z naszą paletką, a wówczas zmienimy jej kierunek ruchu na przeciwny. Tak samo zrobimy, jeżeli piłka napotka krawędź ekranu. Jeśli piłka przejdzie za paletkę, to gracz przegrywa.
W naszej funkcji single_player dodajemy następujące linie:
// ...Pozostała część programu
void single_player(WINDOW *win)
{
Ball ball1;
ball1.x = 10;
ball1.y = 10;
ball1.x_speed = 1;
ball1.y_speed = 1;
mvwprintw(win, ball1.y, ball1.x, "o");
Paddle paddle1;
paddle1.x = getmaxx(win) / 2;
paddle1.y = getmaxy(win) - 2;
paddle1.width = 5;
while (true)
{
ball1.x += ball1.x_speed;
ball1.y += ball1.y_speed;
mvwprintw(win, ball1.y, ball1.x, "o");
int quit = move_paddle(win, paddle1);
if (quit == 1)
{
break;
}
usleep(500);
wrefresh(win);
}
}
W teorii nasz program powinien zadziałać tak, że piłka będzie się przesuwać po ekranie, aż z niego nie wyleci. Sprawdźmy, czy tak jest. Kompilujemy i uruchamiamy program:
g++ main.cpp -o main.out -lncurses && ./main.out
A efekt po uruchomieniu wygląda mniej więcej tak:
Jak widać, nie jest to nasz oczekiwany cel. Piłka w sekundzie przemieściła się na koniec ekranu i zostawiła niepotrzebne ślady. Naprawmy te błędy.
W funkcji single_player
w pętli while
dodajemy 4 linię, która będzie usuwać poprzednią pozycję piłki.
while (true)
{
/* ... */
mvwprintw(win, ball1.y, ball1.x, " ");
ball1.x += ball1.x_speed;
ball1.y += ball1.y_speed;
mvwprintw(win, ball1.y, ball1.x, "o");
int quit = move_paddle(win, paddle1);
/* ... */
}
Po uruchomieniu programu w takiej wersji szybko zauważymy jednak, że dalej piłka w mgnieniu oka wymyka się poza ekran — śladu faktycznie nie ma, ale piłka też znikła.
Moglibyśmy teraz zmienić naszą funkcję usleep
i dodać do niej większą wartość czasu. Spowoduje to, że piłka będzie się
przesuwać wolniej, gdyż pętla while, w której piłka się porusza, będzie się wykonywać co dłuższy odstęp czasu. Problem w
tym, że wtedy nasza paletka także stanie się mniej responsywna — będzie się przesuwać wolniej, gdyż będzie oczekiwać
taką samą ilość czasu jak piłka.
By rozwiązać ten problem, musimy zrobić coś, co jednocześnie spowolni piłkę, ale nie spowolni naszej paletki. Wpadłem na pomysł, by rozwiązać to w następujący sposób:
counter
counter
o 1.%
czy zmienna counter
jest podzielna
przez 300. Jeśli tak, to będziemy przesuwać piłkę o pole. Natomiast niezależnie od tego, w każdej iteracji pętli
możemy przesuwać paletkę.By nasz plan zadziałał wprowadźmy następujące zmiany w pętli while
znajdującej się w funkcji single_player
(do
funkcji dodałem też komentarze, by opisać nasz kod na przyszłość):
/**
* @brief Funkcja odpowiedzialna za gre dla jednego gracza
*
* @param win - okno, w którym odbywa się gra
*/
void single_player(WINDOW *win)
{
// Deklaracja piłki oraz jej początkowej pozycji i prędkości
Ball ball1;
ball1.x = 10;
ball1.y = 10;
ball1.x_speed = 1;
ball1.y_speed = 1;
mvwprintw(win, ball1.y, ball1.x, "o");
// Deklaracja paletki, jej pozycji w połowie ekranu na osi X oraz na przedostatniej linii osi Y
Paddle paddle1;
paddle1.x = getmaxx(win) / 2;
paddle1.y = getmaxy(win) - 2;
paddle1.width = 5;
/* Counter pozwala na przesuwanie piłki z opóźnieniem */
int counter = 0;
// Pętla w której znajduje się cała logika gry
while (true)
{
counter++;
// Co 300 klatek przesuwamy piłkę, usuwając stary ślad
if (counter % 300 == 0)
{
mvwprintw(win, ball1.y, ball1.x, " ");
ball1.x += ball1.x_speed;
ball1.y += ball1.y_speed;
mvwprintw(win, ball1.y, ball1.x, "o");
}
// Funkcja odpowiedzialna za przesuwanie paletki
int quit = move_paddle(win, paddle1);
if (quit == 1)
{
break;
}
// Usypiamy program na 500 mikrosekund i odświeżamy okno z nowymi danymi
usleep(500);
wrefresh(win);
}
}
Udało się! Teraz piłka faktycznie przesuwa się, a nasza paletka działa płynnie. Piłka jednak cały czas wylatuje za ekran i nie odbija się od paletki. Pora to zmienić.
Zasady odbijania piłki są relatywnie proste:
Plan jest gotowy — bierzmy się do roboty!
Wprowadźmy następujące zmiany do pętli while(true)
znajdującej się w funkcji single_player
:
// Pętla w której znajduje się logika gry
while (true)
{
counter++;
// Co 300 klatek przesuwamy piłkę, usuwając stary ślad
if (counter % 200 == 0)
{
mvwprintw(win, ball1.y, ball1.x, " ");
ball1.x += ball1.x_speed;
ball1.y += ball1.y_speed;
mvwprintw(win, ball1.y, ball1.x, "o");
}
/* Odbijanie piłki */
if (ball1.y == getmaxy(win) - 3)
{
for (int i = paddle1.x; i < paddle1.x + paddle1.width; i++)
{
if (ball1.x == i)
{
ball1.y_speed = -1;
}
}
}
if (ball1.y == getbegx(win))
{
ball1.y_speed = 1;
}
if (ball1.x == getmaxx(win) - 2)
{
ball1.x_speed = -1;
}
if (ball1.x == getbegx(win))
{
ball1.x_speed = 1;
}
/* Odbijanie piłki */
// Pętla odpowiedzialna za przesuwanie paletki
int quit = move_paddle(win, paddle1);
if (quit == 1)
{
break;
}
// Usypiamy program na 500 mikrosekund i odświeżamy okno z nowymi danymi
usleep(500);
wrefresh(win);
}
Nasze nowe warunki if
sprawdzają:
Z zasady program powinien najpierw działać, a dopiero później powinniśmy się zajmować jego optymalizacją. Dlatego też,
skoro nasze odbijanie działa, warto teraz uporządkować program i przenieść logikę odpowiedzialną za odbijanie piłki do
innej funkcji by nie zaśmiecała nam funkcji single_player
.
Wycinamy kod z warunkami spomiędzy komentarza /* odbijanie piłki */
i wstawiamy go do nowej funkcji:
int ball_bounce(WINDOW *win, Ball &ball, Paddle &paddle)
{
if (ball.y == getmaxy(win) - 3)
{
for (int i = paddle.x; i < paddle.x + paddle.width; i++)
{
if (ball.x == i)
{
ball.y_speed = -1;
}
}
}
if (ball.y == getbegx(win))
{
ball.y_speed = 1;
}
if (ball.x == getmaxx(win) - 2)
{
ball.x_speed = -1;
}
if (ball.x == getbegx(win))
{
ball.x_speed = 1;
}
}
W funkcji single_player
zmieniamy kod tak, żeby wywoływała się funkcja ball_bounce
zamiast warunków if if
:
/* Odbijanie piłki */
ball_bounce(win, ball1, paddle1);
By nazwać nasz program grą, potrzebujemy jeszcze dwóch rzeczy: Możliwości zdobywania punktów i możliwości przegranej — oczywiście tylko wtedy, gdy piłka wyleci za ekran.
Najlepszą metodą, jeśli chodzi o programowanie, jest rozbijanie problemu na coraz mniejsze problemy, aż do momentu, w którym jesteśmy w stanie zacząć je rozwiązywać. Następnie po kolei rozwiązujemy te mikro problemy, aż dojdziemy do momentu, gdy mamy ukończony program. Tak i tym razem rozbijmy nasz problem na mniejsze i wypiszmy sobie listę wszystkich rzeczy, które potrzebujemy.
By gracz mógł przegrać, nasza piłka musi wylecieć za ekran-innymi słowy musi się znaleźć co najmniej na poziomie paletki. W naszej wcześniejszej funkcji sprawdzaliśmy, czy piłka jest na pozycji o jeden większej od pozycji paletki na osi Y. Jeśli tak było, to piłka się odbijała. W przeciwnym wypadku zakładamy, że piłka znalazła się na równi z paletką, co oznacza, że gracz przegrał.
Więc nasz warunek jest prosty — Jeśli piłka znajduje się na pozycji takiej samej jak paletka, to gracz przegrał.
W kodzie będzie to wyglądać tak:
if (ball1.y == getmaxy(win) - 1) {
/* Przegrana */
}
Sprawdzamy, czy pole .y naszej piłki jest równe maksymalnemu poziomowi-Y naszego ekranu win
-1 (bo tam jest paletka).
Jeśli tak to robimy coś, co oznacza, że gracz przegrał.
W grze po przegranej dobrze jest wyświetlić informację o przegranej oraz wyświetlić wynik gracza. Zrobimy to, ale musimy
pamiętać, że musimy najpierw wyczyścić ekran, by usunąć z niego paletkę. W tym celu użyjemy funkcji wclear(win)
z
curses. Po jej użyciu musimy ponownie narysować ramkę, gdyż funkcja wyczyściła cały ekran. Następnie możemy wypisać
stosowne komunikaty.
W naszej funkcji single_player
w pętli while(true)
po linii odpowiedzialnej za odbijanie piłki dodajemy następujący
kod:
/* ... */
ball_bounce(win, ball1, paddle1, score);
if (ball1.y == getmaxy(win) - 1)
{
wclear(win);
box(win, 0, 0);
mvwprintw(win, getmaxy(win) / 2, getmaxx(win) / 2, "GAME OVER");
mvwprintw(win, getmaxy(win) / 2 + 1, getmaxx(win) / 2, "Your score: %d", score / 300);
mvwprintw(win, getmaxy(win) / 2 + 2, getmaxx(win) / 2, "Press any key to continue");
mvwprintw(win, getmaxy(win) / 2 + 3, getmaxx(win) / 2, "Press q to quit");
wrefresh(win);
}
/* ... */
Po skompilowaniu jednak dalej pojawia nam się paletka i możemy nią sterować. Możemy wyjść z gry, ale nasza opcja wyboru, by kontynuować grę, nie działa.
Wobec tego musimy zrobić następujące rzeczy:
Zrobimy to, zmieniając kod wyżej w następujący sposób:
int single_player(WINDOW *win)
{
// Deklaracja piłki oraz jej początkowej pozycji i prędkości
Ball ball1;
ball1.x = 10;
ball1.y = 10;
ball1.x_speed = 1;
ball1.y_speed = 1;
mvwprintw(win, ball1.y, ball1.x, "o");
// Deklaracja paletki, jej pozycji w połowie ekranu na osi X oraz na przedostatniej linii osi Y
Paddle paddle1;
paddle1.x = getmaxx(win) / 2;
paddle1.y = getmaxy(win) - 2;
paddle1.width = 5;
/* Counter pozwala na przesuwanie piłki z opóźnieniem */
int counter = 0;
int score = 0;
// Pętla w której znajduje się cała logika gry
while (true)
{
counter++;
mvwprintw(win, 0, 0, "Score: %d", score / 300);
// Co 300 klatek przesuwamy piłkę, usuwając stary ślad
if (counter % 300 == 0)
{
mvwprintw(win, ball1.y, ball1.x, " ");
ball1.x += ball1.x_speed;
ball1.y += ball1.y_speed;
mvwprintw(win, ball1.y, ball1.x, "o");
}
/* Odbijanie piłki */
ball_bounce(win, ball1, paddle1, score);
if (ball1.y == getmaxy(win) - 1)
{
wclear(win);
box(win, 0, 0);
mvwprintw(win, getmaxy(win) / 2, getmaxx(win) / 2, "GAME OVER");
mvwprintw(win, getmaxy(win) / 2 + 1, getmaxx(win) / 2, "Your score: %d", score / 300);
mvwprintw(win, getmaxy(win) / 2 + 2, getmaxx(win) / 2, "Press any key to continue");
mvwprintw(win, getmaxy(win) / 2 + 3, getmaxx(win) / 2, "Press q to quit");
wrefresh(win);
nodelay(stdscr, FALSE);
int ch;
ch = getch();
if (ch == 'q' || ch == 'Q')
return 1;
else
{
wclear(win);
box(win, 0, 0);
nodelay(stdscr, TRUE);
return single_player(win);
}
}
// Funkcja odpowiedzialna za przesuwanie paletki
int quit = move_paddle(win, paddle1);
if (quit == 1)
{
break;
}
// Usypiamy program na 500 mikrosekund i odświeżamy okno z nowymi danymi
usleep(500);
wrefresh(win);
}
return 0;
}
main
nodelay(stdcr, FALSE)
- oznacza to, że program nie będzie kontynuował swojego działania, dopóki nie
wprowadzimy jakiegoś klawisza. Naszym celem jest wyświetlić ekran z wynikiem, a później oczekiwać na wprowadzenie
klawisza do kontynuowania gry lub wyjścia z gry.getch()
single_player
zwracając wartość 1 by program wiedział, że opuściliśmy grę dla jednego gracza (może się to przydać w przyszłości np.
gdybyśmy chcieli stworzyć menu z różnymi opcjami gry)nodelay(stdscr, TRUE)
, która pozwoli na dalszą grę bez blokowania klatek w
oczekiwaniu na klawisz od gracza (nasza piłka powinna się przesuwać bez względu na to, czy ruszamy paletką, czy nie —
bez tej opcji nie będzie się ona poruszać, dopóki nie naciśniemy klawisza). Na koniec zwracamy funkcję single player
używając return
. Gdybyśmy nie użyli returnu, tylko wstawili samą funkcję single_player(win)
to zostałaby ona
wywołana rekurencyjnie przez pierwszą funkcję. Teoretycznie gra by
działała prawidłowo, ALE przy próbie wyjścia, po naciśnięciu klawisza 'q' wyszlibyśmy tylko z jednej funkcji, a nie z
całej gry. Pozostałe wywołania by dalej były aktywne, więc wyjście z gry by było możliwe dopiero po naciśnięciu 'q'
tyle razy, ile razy uruchomiliśmy ponownie grę. Użycie słowa kluczowego return
pozwala nam zakończyć bieżące
wywołanie funkcji i uruchomić nowe.Sukces! Nasza gra działa zgodnie z wszystkimi założeniami.
Co udało nam się stworzyć:
Programistyczne koncepcje wykorzystane podczas tworzenia tej gry:
Tworząc tego posta, korzystałem intensywnie z poniższych źródeł, które warto sprawdzić, jeśli chcesz pogłębić swoją wiedzę na temat Ncurses i innych tematów, które poruszałem w tym poście:
Piątą część poradnika możesz znaleźć tutaj: Jak zacząć przygodę z C++ i Ncurses - Cz. 5 - Doxygen