Gra Pong jak napisałem i jak powinienem ją napisać

Gra Pong

Jest to prosta gra, gdzie mamy dwie ‚paletki’ i odbijamy między sobą piłkę. Jeśli nie zdążymy odbić przeciwnik dostaje punkt a piłka wraca na środek. Pewnie każdy w to grał, ale w razie czego daję obrazek, który wyjaśni wszystko.

Mój kod

A więc zrobiłem sobie wyzwanie napisać tą grę w 1h. Udało mi się 🙂 Było to 27 kwietnia 2016 roku. Tak więc po prawie roku czasu postanowiłem przejrzeć mój kod i zobaczyć czego się nauczyłem od tego czasu. Mój kod ma 214 linii, składa się z 3 funkcji i jednej struktury. Zobaczcie sami: https://github.com/Patys/pong/blob/master/main.cpp

Analiza kodu

Piękna i jedyna struktura

struct Enity
{
  sf::Vector2f position;
  sf::Vector2f velocity;

  sf::Vector2f size;

  sf::Vector3f color;
}

Z piękną literówką 🙂 Pamiętam, że czytałem wtedy o ECS, ale jeszcze nie próbowałem tego używać. Ale zainspirowany komponentami, które są umieszczone w kontenerze zrobiłem coś podobnego i zrobiłem dosyć uniwersalną strukturę, która opisuje każdy obiekt w grze. Wygląda ładnie, może jedynie nazwa jest trochę nie tak ale mi się nadal podoba 🙂

Zamiana int na string

std::string inttostring(int x)
{
  std::stringstream type;
  type << x;
  return type.str();
}

Prosta funkcja, którą chyba znalazłem na stackoverflow 🙂 Potrzebna była abym mógł zamieniać punkty zapisane w int na string, ponieważ tego wymagała funkcja z SFML.

Kolizja AABB

bool checkCollision(int x, int y, int oWidth, int oHeight, int xTwo, int yTwo, int oTwoWidth, int oTwoHeight)
{
    // AABB 1
    int x1Min = x;
    int x1Max = x+oWidth;
    int y1Max = y+oHeight;
    int y1Min = y;

    // AABB 2
    int x2Min = xTwo;
    int x2Max = xTwo+oTwoWidth;
    int y2Max = yTwo+oTwoHeight;
    int y2Min = yTwo;

    // Collision tests
    if( x1Max < x2Min || x1Min > x2Max ) return false;
    if( y1Max < y2Min || y1Min > y2Max ) return false;

    return true;
}

Sprawdzanie kolizji pomiędzy dwoma prostokątami. Też znalezione gdzieś w google.

Tworzenie tekstu

  // Create a text which uses our font
  sf::Text SCORE_TEXT;
  SCORE_TEXT.setFont(font);
  SCORE_TEXT.setCharacterSize(22);
  SCORE_TEXT.setPosition(350, 10);
  SCORE_TEXT.setString("Get a point!");

Tutaj widzę pierwszą rzecz, którą bym zmienił. Zamknąłbym to w funkcji. Dodatkowo nazwę zmiennej zapisałbym małymi literami. Wtedy wyglądałoby to tak:

  sf::Text score_text = createText(font, 22, sf::Vector2f(350,10));
  score_text.setString("Get a point!");

Wydaje mi się to lepszym rozwiązaniem, ponieważ nie muszę po kolei ustawiać poszczególnych pól tylko robiłaby to funkcja. Kod jest czytelniejszy i łatwiej go używać w przyszłości.

Ach te Enity 🙂

Enity paddleone;
  paddleone.position = sf::Vector2f(10, 275);
  paddleone.size = sf::Vector2f(20,50);
  paddleone.color = sf::Vector3f(64,255,64);

  Enity paddletwo;
  paddletwo.position = sf::Vector2f(770, 275);
  paddletwo.size = sf::Vector2f(20,50);
  paddletwo.color = sf::Vector3f(64,64,255);
sf::RectangleShape rect_paddleone;
  rect_paddleone.setSize(paddleone.size);
  rect_paddleone.setFillColor(sf::Color(paddleone.color.x, paddleone.color.y, paddleone.color.z));
  rect_paddleone.setPosition(paddleone.position);

  sf::RectangleShape rect_paddletwo;
  rect_paddletwo.setSize(paddletwo.size);
  rect_paddletwo.setFillColor(sf::Color(paddletwo.color.x, paddletwo.color.y, paddletwo.color.z));
  rect_paddletwo.setPosition(paddletwo.position);
  Enity ball;
  ball.position = sf::Vector2f(390, 290);
  ball.size = sf::Vector2f(20,20);
  ball.velocity = sf::Vector2f(3,-3);
  ball.color = sf::Vector3f(255,64,64);

  sf::CircleShape circle_ball;
  circle_ball.setRadius(10);
  circle_ball.setFillColor(sf::Color(ball.color.x, ball.color.y, ball.color.z));
  circle_ball.setPosition(ball.position);

Podobna sytuacja jak wcześniej. Zamknąć wszystko do funkcji i jej używać. Kod jest bardzo podobny. Popełniłem błąd nie zamykając tego w funkcji tylko po kolei ustawiając pola.

Bo kod zawsze można kopiować

      // PADDLEONE
      
      if(paddleone.velocity.y < 0)
	paddleone.velocity.y += 0.2;
      else if(paddleone.velocity.y > 0)
	paddleone.velocity.y -= 0.2;

      if(sf::Keyboard::isKeyPressed(sf::Keyboard::S))
	paddleone.velocity.y += 1;
      if(sf::Keyboard::isKeyPressed(sf::Keyboard::W))
	paddleone.velocity.y -= 1;

      if(paddleone.velocity.y > 4)
	paddleone.velocity.y = 4;
      else if(paddleone.velocity.y < -4)
	paddleone.velocity.y = -4;

      if(paddleone.position.y > 550)
	paddleone.velocity.y = -0.1;
      else if(paddleone.position.y < 0)
	paddleone.velocity.y = 0.1;

      paddleone.position += paddleone.velocity;

      rect_paddleone.setPosition(paddleone.position);


      //PADDLETWO

      if(paddletwo.velocity.y < 0)
	paddletwo.velocity.y += 0.2;
      else if(paddletwo.velocity.y > 0)
	paddletwo.velocity.y -= 0.2;

      if(sf::Keyboard::isKeyPressed(sf::Keyboard::Down))
	paddletwo.velocity.y += 1;
      if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up))
	paddletwo.velocity.y -= 1;

      if(paddletwo.velocity.y > 4)
	paddletwo.velocity.y = 4;
      else if(paddletwo.velocity.y < -4)
	paddletwo.velocity.y = -4;

      if(paddletwo.position.y > 550)
	paddletwo.velocity.y = -0.1;
      else if(paddletwo.position.y < 0)
	paddletwo.velocity.y = 0.1;

      paddletwo.position += paddletwo.velocity;

      rect_paddletwo.setPosition(paddletwo.position);

Okropne, prawda? Ten sam kod różniący się paroma liczbami. Obecnie wiem, że takie coś musi być w jakiejś funkcji lub metodzie. Źle to nie wygląda ale nie powinno to znajdować się w główniej funkcji, gdzie gra się wykonuje.

Zdobywanie punktów

      if(ball.position.x < 0)
	{
	  ball.position.x = 390;
	  ball.position.y = 290;
	  // add point
	  SCORE_TWO += 1;
	  std::string textpoint = inttostring(SCORE_ONE) + " : " + inttostring(SCORE_TWO);
	  SCORE_TEXT.setString(textpoint);
	}
      if(ball.position.x > 800)
	{
	  ball.position.x = 390;
	  ball.position.y = 290;
	  // add point
	  SCORE_ONE += 1;
	  std::string textpoint = inttostring(SCORE_ONE) + " : " + inttostring(SCORE_TWO);
	  SCORE_TEXT.setString(textpoint);
	}

Zdobywanie punktów zawsze jest fajne 🙂 Tutaj jest prawie idealnie poza tym że to jest ten sam kod 2 razy. Różni się tylko jedną zmienną: SCORE_ONE z SCORE_TWO. Ten sam błąd… Znowu…

Reszta

Reszta kodu jest prawie ok. Mogłaby być również zamknięta w oddzielnie funkcje.

Wnioski

Z tej analizy udało mi się zrozumieć jak wiele niepotrzebnych rzeczy wtedy pisałem, powtarzający się kod, literówki, zmienne raz dużymi, a raz małymi literami. Wrzucenie wszystkiego do jednej funkcji. Okropne błędy.

Moim zdaniem kod powinien wyglądać jak książka. Czytając to widzi się co się może wydarzyć w programie. Nie ważne jest jak to jest zrobione tylko co to robi. Wolę zobaczyć prostą funkcję checkCollision niż całą implementację tej funkcji. Będę się teraz starał pisać książki 🙂

Na koniec video z programowania tego w timelapsie (EMACS i linux do tworzenia gier 😀 ):

Please follow and like us:
0

Dodaj komentarz

Bądź pierwszy!

Powiadom o
avatar