Napiszmy grę w JavaScript: Stan oraz stałe

Cześć! To jest fragment książki JavaScript od podstaw, która ma pomóc w nauce programowania od zera.

Gdzie definiujemy najważniejsze zmienne i stałe, które będą określały położenie i rodzaj elementów widoku, oraz jak powinny być rysowane.

Stałe

Kiedy rysowaliśmy paletki czy piłeczkę, w funkcjach znalazły się stałe, takie jak:

  • wymiary płótna;
  • umiejscowienie punktacji;
  • wymiary paletki;
  • umiejscowienie paletek w poziomie;
  • wielkość piłeczki;
  • pozycja początkowa piłeczki.

Te wartości pozostają takie same przez cały czas działania programu. Ich wartości nadajemy my, twórcy. Jest więc spora szansa, że moglibyśmy chcieć je w przyszłości zmienić, na przykład poprzez zmniejszenie paletki albo przyspieszenie piłeczki, aby utrudnić grę.

Na takie wartości mówi się stałe. Za dobrą praktykę uważa się ich wydzielenie do osobnych zmiennych typu const i umieszczenie ich razem, ponieważ:

  • dobrze nazwana zmienna staje się bardziej zrozumiała niż samotna wartość (nazwa wyjaśnia rolę zmiennej, dzięki czemu nie trzeba się jej domyślać);
  • jeśli kiedyś zechcesz zmodyfikować którąś z tych wartości (na przykład wymiary paletki), to znacznie łatwiej będzie zmienić wartość w jednej zmiennej, niż poszukiwać w kodzie, gdzie została ona użyta.

Stałe programu są wartościami w pewien sposób specjalnymi, a więc przyjęło się:

  • definiować wszystkie obok siebie, na początku programu;
  • nazywać je w szczególny sposób, czyli wyłącznie wielkimi literami oraz oddzielając słowa podkreślnikiem _ (taka notacja jest zwana SCREAMING_SNAKE_CASE albo CONSTANT_CASE).

Dzięki tym konwencjom łatwiej rozpoznać i odnaleźć stałe globalne.

Zacznijmy od zdefiniowania wymienionych stałych (dodatkowe komentarze objaśniające znaczenie zmiennej możesz zostawić, bo mogą przydać Ci się także później).

const CANVAS_HEIGHT = canvas.height; // wysokość płótna
const CANVAS_WIDTH = canvas.width; // szerokość płótna

const BOARD_Y = 50; // y obydwu punktacji
const BOARD_P1_X = 300; // x punktacji gracza 1
const BOARD_P2_X = 500; // x punktacji gracza 2

const PADDLE_WIDTH = 20; // szerokość paletki
const PADDLE_HEIGHT = 100; // wysokość paletki
const PADDLE_P1_X = 10; // pozycja x paletki gracza 1
const PADDLE_P2_X = 770; // pozycja x paletki gracza 2
const PADDLE_START_Y =
  (CANVAS_HEIGHT - PADDLE_HEIGHT) / 2;
// początkowa pozycja y paletek

const BALL_R = 15; // promień piłeczki
const BALL_START_X = CANVAS_WIDTH / 2;
// pozycja początkowa x środka piłeczki
const BALL_START_Y = CANVAS_HEIGHT / 2;
// pozycja początkowa y środka piłeczki
const BALL_START_DX = 4.5;
// początkowa prędkość lotu piłeczki na współrzędnej x
const BALL_START_DY = 1.5;
// początkowa prędkość lotu piłeczki na współrzędnej y

Ciekawą zmienną jest PADDLE_START_Y, która zawiera początkową pozycję y góry paletek. Chcemy umieścić je dokładnie na środku. Gdybyśmy ustawili tę wartość na CANVAS_HEIGHT / 2, to zaczęlibyśmy rysować od środka płótna w dół, a więc środek paletki znajdowałby się poniżej środka płótna. Aby dokonać korekty, musielibyśmy przesunąć tę paletkę w górę, o połowę jej wysokości. Przesunięcie w górę to odjęcie od współrzędnej, zatem moglibyśmy obliczyć poprawną wartość przez CANVAS_HEIGHT / 2 - PADDLE_HEIGHT / 2, co jest równoznaczne z (CANVAS_HEIGHT - PADDLE_HEIGHT) / 2.

Zdefiniowane tutaj elementy nie są wszystkimi, które będą nam potrzebne, ale na początek wystarczą. Nowe stałe poznasz po charakterystycznych wielkich literach. Dla zachowania porządku najlepiej umieść je na samym początku programu, razem z resztą stałych.

Stan

Podczas gdy pewne wartości pozostają stałe, inne się zmieniają. O tych drugich mówimy, że stanowią one stan gry. Przykładowo, gdy patrzysz na szachownicę podczas gry w szachy, jej stan to: ustawienie figur, wartości liczników czasu oraz czyja jest kolej na ruch. Stan to wszystko, co się zmienia. Zapisując grę, na dysku zostanie zarejestrowany jej stan. Kiedy ją otworzysz, wystarczy wczytać wartości do zmiennych reprezentujących stan. U nas stanem są na przykład:

  • pozycja i kierunek lotu piłeczki (kierunek wyrazimy jako dx i dy, oznaczające zmianę zmiennych x i y co stały czas);
  • ustawienie paletek na współrzędnej y;
  • liczba punktów każdego z graczy.

Te elementy powinny być zmienne, a więc zdefiniowane przy pomocy let.

let ballX = BALL_START_X;
let ballY = BALL_START_Y;
let ballDX = BALL_START_DX;
let ballDY = BALL_START_DY;
let p1PaddleY = PADDLE_START_Y;
let p2PaddleY = PADDLE_START_Y;
let p1Points = 0;
let p2Points = 0;

Tutaj moglibyśmy zgrupować nasze zmienne do obiektów. Zrobimy to jednak dopiero później (w rozdziale Elementy programowania obiektowego), a teraz pozostaniemy przy zmiennych, aby kod pozostał możliwie jak najprostszy.

W oparciu o te wartości jesteśmy w stanie wyświetlić aktualny stan gry przy użyciu następującej funkcji:

function drawState() {
  clearCanvas();
  drawPoints(p1Points.toString(), BOARD_P1_X);
  drawPoints(p2Points.toString(), BOARD_P2_X);
  drawBall(ballX, ballY);
  drawPaddle(PADDLE_P1_X, p1PaddleY);
  drawPaddle(PADDLE_P2_X, p2PaddleY);
}

Całość jednak będzie znacznie ciekawsza, gdy ten stan zacznie się zmieniać.

Zmiana stanu

Teoretycznie moglibyśmy dokonywać zmiany stanu w jednej z poznanych pętli, na przykład while. Niesie to jednak za sobą pewne konsekwencje. Nie mielibyśmy wtedy kontroli nad tym, jak często zmiana stanu następuje. Z uwagi na to, znacznie lepszym rozwiązaniem jest wywołanie jakiejś funkcji co określony czas. Do tego służy dostarczana przez przeglądarkę funkcja setInterval1. Jako argumenty przyjmuje:

  • funkcję, którą powinna wywoływać;
  • co ile milisekund powinna tę funkcję wywoływać (tysiąc milisekund to jedna sekunda).

Poniższy kod wyświetli alert co sekundę (proszę, nie dodawaj takiej funkcjonalności do żadnej strony, jest bardzo irytująca).

function showAlert() {
  alert("New alert");
}

setInterval(showAlert, 1000);

My tę funkcję wykorzystamy do modyfikowania stanu. To, jak często powinniśmy zmienić stan gry, decyduje o jej prędkości. Częstość zmiany stanu określamy w milisekundach. Tę wartość nazwijmy STATE_CHANGE_INTERVAL i umieśćmy pośród innych stałych. Na początek możemy założyć, że będzie ona równa 20 (czyli 20 ms). W przyszłości zawsze możesz ją zwiększyć lub zmniejszyć, by w efekcie odpowiednio spowolnić lub przyspieszyć grę.

Funkcja zmiany stanu powinna najpierw zmodyfikować stan gry, a potem wyświetlić jej aktualny stan.

const STATE_CHANGE_INTERVAL = 20;

function updateState() {
  // Tutaj będziemy zmieniali stan
}

function updateAndDrawState() {
  updateState();
  drawState();
}

setInterval(updateAndDrawState, STATE_CHANGE_INTERVAL);

Jedną z czynności, jakie powinny się odbywać w ramach zmiany stanu, jest ruch piłeczki zgodnie z założonym kierunkiem. Jest to dodanie ballDXballDY odpowiednio do ballXballY.

ballX += ballDX; // ballX = ballX + ballDX
ballY += ballDY; // ballY = ballY + ballDY

To sprawi, że nasza piłeczka poleci. Aby teraz zobaczyć ruch paletek, czy zmianę punktów, możemy dokonywać dowolnej sztucznej modyfikacji w każdym kroku. Tylko pamiętajmy, by ją później usunąć.

Kod, z którym powinieneś skończyć po tym rozdziale znajdziesz pod linkiem:

https://github.com/MarcinMoskala/pong/blob/master/2_state.html

Zamiast linku, możesz użyć tego kodu QR.

Aktualny stan gry w CodePen z przykładową zmianą stanu punktów i pozycji paletek.

W tej sekcji uporządkowaliśmy nasz kod poprzez określenie stałych oraz stanu. Nauczyliśmy się także, jak modyfikować ten stan. Dzięki nabytym umiejętnościom sprawiliśmy już, że paletki i piłeczka zaczęły się ruszać, a tablice punktów zmieniają wyświetlane wartości. Wszystko to jednak dzieje się samoistnie. Jesteśmy gotowi by to gracze przejęli kontrolę nad paletkami.

1:

Współcześnie w grach przeglądarkowych stosuje się raczej funkcję window.requestAnimationFrame. Jest bardzo podobna do setInterval.