Lab 08 - dziedziczenie, obsluga zdarzen w SFML

Lab 08 - Dziedziczenie i obsługa zdarzeń w SFML

Dziedziczenie

W ramach kursu stworzyliśmy przykładowe klasy mogące reprezentować byty z rzeczywistego świata (np. klasa Student). Wiele typów rzeczywistych obiektów może współdzielić pewne cechy bądź należeć do wspólnej, większej grupy. Przykładowo samochody, motocykle, hulajnogi i rowery są rodzajami pojazdów. Oznacza to, że będą współdzieliły pewne cechy. Przekładając to na język programowania obiektowego, możemy powiedzieć, że klasy Car, Motorcycle, Scooter i Bike dziedziczą właściwości klasy Vehicle. Mechanizm ten nazywa się dziedziczeniem (ang. inheritance) i pozwala nam nie tylko na ustalenie pewnej hierarchicznej zależności pomiędzy tymi klasami, ale przede wszystkim ustalenie wspólnego interfejsu, który powinien być zaimplementowany we wszystkich klasach potomnych, jednocześnie unikając niepotrzebnego powielania kodu.

Przykład klasy bazowej (ang. base):

class Vehicle {
public:
    std::string name() { return name_; }
    int number_of_wheels() { return number_of_wheels_; }
    std::string propulsion_type() { return propulsion_type_; }
    double max_speed() { return max_speed_; }

protected:
    Vehicle(const std::string &name, int number_of_wheels,
            const std::string &propulsion_type, double max_speed)
        : name_(name), number_of_wheels_(number_of_wheels),
          propulsion_type_(propulsion_type), max_speed_(max_speed) {}

    std::string name_;
    int number_of_wheels_;
    std::string propulsion_type_;
    double max_speed_;
};

W powyższym przykładzie została użyta lista inicjalizacyjna, dzięki której możliwe jest zaincjalizowanie pól klasy w momencie ich tworzenia - w przeciwieństwie do inicjalizacji w samym ciele konstruktora, gdzie najpierw zostałoby utworzone pole z domyślną wartością, a następnie przypisana nowa wartość.

Modyfikator dostępu protected

Główna zmiana w stosunku do klas, które implementowaliśmy poprzednio, to nowy modyfikator dostępu protected użyty w klasie Vehicle. Zachowuje się on podobnie do modyfikatora private (uniemożliwia dostęp z poza klasy), ale w przeciwieństwie do niego umożliwia dostęp do pól/metod z poziomu klas, które dziedziczą z wyżej opisanej klasy. Działanie wszystkich trzech modyfikatorów opisuje poniższa tabela:

Dostęp z poziomu public protected private
członków tej samej klasy tak tak tak
członków klasy potomnej tak tak nie
spoza klasy tak nie nie

Możesz zauważyć, że konstruktor klasy Vehicle oznaczony jest jako protected (chroniony). Ma to miejsce, ponieważ nie chcemy umożliwiać stworzenia instancji obiektu klasy Vehicle bezpośrednio. Użyjemy jej jako “szablonu” dla poszczególnych klas potomnych.

Przykładową klasą dziedziczącą z klasy Vehicle jest klasa Bike:

class Bike : public Vehicle {
public:
    Bike() : Vehicle("Bike", 2, "Muscles", 30) {}
};

Klasy bazowe podajemy po dwukropku przy deklaracji klasy potomnej. Widzimy, że klasa Bike dziedziczy z klasy Vehicle z modyfikatorem public. Podczas dziedziczenia modyfikatory mają następujący efekt:

Klasa Bike definiuje konstruktor, który z pomocą listy inicjalizacyjnej wywołuje konstruktor klasy bazowej z pewnymi stałymi parametrami. Dokonujemy pewnych założeń co do rowerów: nie mają specjalnych nazw, mają zawsze dwa koła, używają tylko siły mięśni jako źródła napędu i mają prędkość maksymalną równą 30. Klasa Bike nie dodaje żadnej funkcjonalności do klasy bazowej, ale możliwe jest już utworzenie jej instancji. Możemy np. odwoływać się do getterów klasy bazowej:

Bike bike;
std::cout << bike.max_speed() << std::endl; // Will print 30

Możemy zdefiniować kolejną klasę potomną - Car:

class Car : public Vehicle {
public:
    Car(const std::string &name, const std::string &propulsion_type,
        double max_speed, bool has_abs)
        : Vehicle(name, 4, propulsion_type, max_speed),
          has_abs_(has_abs) {}

    bool has_abs() { return has_abs_; }

private:
    bool has_abs_;
};

Klasa Car jest nieco bardziej skomplikowana: ma konstruktor, który przyjmuje argumenty i przekazuje je do konstruktora klasy bazowej, zakładając jedynie stałą wartość liczby kół równą 4. Dodatkowo klasa definiuje właściwość has_abs_ wraz z getterem, która również jest inicjalizowana w konstruktorze. Jest to właściwość specyficzna dla samochodów, nie współdzielona z innymi typami pojazdów.

Po utworzeniu instancji obiektu klasy Car możemy się odwoływać do metod i pól zarówno klasy bazowej, jak i dodanych w klasie potomnej:

Car passat("Volkswagen Passat", "Diesel", 200, true);
std::cout << "Name: " << passat.name() << std::endl;
std::cout << "Has ABS: " << passat.has_abs() << std::endl;

Wielodziedziczenie

Klasy mogą również dziedziczyć z wielu klas bazowych, otrzymując zbiór wszystkich właściwości i metod klas bazowych. Nazywa się to wielodziedziczeniem i może zostać zapisane w następujący sposób: class Car : public Object, public Vehicle.

Oczywiście poruszyliśmy jedynie fragment zagadnienia, jakim jest dziedziczenie. Dziedziczenie daje wiele możliwości, np. tworzenie kontenerów polimorficznych - przechowujących wskaźniki do różnych klas, które mają wspólną klasę bazową.


🛠🔥 Zadanie 🛠🔥

Zdefiniuj kilka dodatkowych klas, które dziedziczą z klasy Vehicle. Dla każdej klasy zdefiniuj konstruktor i dodaj pola specyficzne dla danego typu pojazdu. Stwórz instancje obiektów nowo zdefiniowanych klas. Przykładowe typy pojazdów, które możesz opisać to:

Możesz również stworzyć dodatkową “pośrednią” klasę bazową dziedziczącą po Vehicle. Przykładowo, klasa opisująca statek powietrzny może dziedziczyć po Vehicle, ale jednocześnie być klasą bazową dla kolejnych klas opisujących obiekty takie jak samolot czy helikopter.


Dziedziczenie w SFML

Ponieważ SFML jest biblioteką obiektową, możemy wykorzystać mechanizm dziedziczenia, aby rozszerzyć możliwości wbudowanych klas zgodnie ze swoimi potrzebami, unikając jednocześnie powielania już istniejącego kodu czy implementacji funkcjonalności, która jest już dostępna.


🛠🔥 Zadanie 🛠🔥

Bazując na powyższym opisie i przykładach dziedziczenia, stwórz klasę CustomRectangleShape dziedzicząc z klasy sf::RectangleShape. Docelowo Twój CustomRectangleShape poza standardowymi cechami prostokąta, ma przechowywać w sobie informacje o swojej prędkości liniowej (w poziomie i w pionie), prędkości obrotowej, a także umożliwiać wygodną animację z możliwością odbijania od krawędzi ekranu.

  1. Zdefiniuj klasę tak, aby możliwe było utworzenie jej obiektu w następujący sposób:
sf::Vector2f size(120.0, 60.0);
sf::Vector2f position(120.0, 60.0);
CustomRectangleShape my_rectangle(size, position);

Podpowiedź: pamiętaj, że skoro CustomRectangleShape dziedziczy po sf::RectangleShape, to wewnątrz jego metod możesz odwoływać się bezpośrednio do metod klasy bazowej, np. setPosition. Pamiętaj, aby wywołać w swoim konstruktorze , w liście inicjalizacyjnej, konstruktor klasy bazowej z odpowiednimi parametrami.

Podpowiedź: jeśli wykonasz dziedziczenie z modyfikatorem public, wszystkie pola i metody klasy bazowej pozostaną niezmiennie dostępne:

my_rectangle.setFillColor(sf::Color(100, 50, 250));
  1. Dodaj do swojej klasy pola prywatne opisujące poszczególne składowe prędkości, z domyślną wartością równą 0. Dodaj metodę publiczną pozwalającą na ich ustawienie:
my_rectangle.setSpeed(100, 150, 10); // predkosc x, y, obrotowa

Podpowiedź: pola w klasach mogą mieć domyślną wartość przypisaną przy deklaracji pola:

private:
    int speed_x_ = 0;
  1. Dodaj do swojej klasy metodę publiczną void animate(const sf::Time &elapsed). Metoda powinna przyjąć czas, jaki upłynął od narysowania ostatniej klatki obrazu. Zaimplementuj metodę tak, aby odpowiednio aktualizowała położenie i rotację obiektu. Główna pętla programu powinna wywoływać metodę animate, przekazując jej zmierzony czas:
/* ... */
window.clear(sf::Color::Black);

sf::Time elapsed = clock.restart();

my_rectangle.animate(elapsed);

window.draw(rectangle);
window.display();
/* ... */
  1. Aby umożliwić odbijanie prostokąta wewnątrz określonego obszaru (np. granic okna), klasa musi znać granice tego obszaru. Dodaj odpowiednie pola prywatne i dwie metody pozwalające na ustawienie granic:
  1. poprzez podanie lewej, prawej, górnej i dolnej granicy:
my_rectangle.setBounds(0, window.getSize().x, 0, window.getSize().y);
  1. poprzez podanie obiektu typu sf::IntRect zawierającego prostokąt opisujący granice obszaru:
sf::IntRect rect1(10, 10, 200, 100); // prostokat o poczatku w punkcie 10,10, szerokosci 200 i wysokosci 100
my_rectangle.setBounds(rect1);
  1. Dodaj metodę prywatną void bounce(), która będzie zmieniać zwrot odpowiednich prędkości liniowych prostokąta po przecięciu krawędzi obszaru granicznego. Wywołuj metodę bounce z wnętrza metody animate.

Podpowiedź: aby uniknąć utknięcia obiektu “w granicy” wyznaczaj nową prędkość korzystając z wartości bezwzględnej i nadając jej odpowiedni znak, w zależności od tego, z którą krawędzią obszaru granicznego nastąpił kontakt.

  1. Dodaj do sceny kilka instancji CustomRectangleShape, o różnych rozmiarach i prędkościach, uruchom animację.

Obsługa zdarzeń oraz urządzeń wejścia w SFML

Większość gier komputerowych musi reagować na zewnętrzne sygnały wejścia zadawane przez użytkownika - generowane przez np. klawiaturę lub mysz. W SFML mamy dwie możliwości przechwytywania tych sygnałów - system zdarzeń z kolejką oraz ręczne sprawdzanie stanu. Każdy z nich ma swoje zastosowanie w innych przypadkach, w zależności od żądanego efektu.

System zdarzeń

Naciśnięcie klawisza na klawiaturze lub przesunięcie myszy - to zdarzenia (ang. event), które system operacyjny przekazuje aplikacji. Biblioteka SFML obsługuje różne typy zdarzeń przez dedykowane klasy. Należy pamiętać, że zdarzenia mają charakter jednorazowy - naciśnięcie i przytrzymanie przycisku myszy przez kilka sekund wygeneruje zdarzenia tylko w dwóch momentach - naciskania oraz puszczania przycisku. Zdarzenia są zatem wygodne przy wprowadzaniu tekstu, wykrywaniu kliknięć czy pojedynczych naciśnięć klawiszy. Wewnątrz pętli głównej programu wykorzystującego SFML znajduje się zazwyczaj dodatkowa pętla while przeglądająca wszystkie zdarzenia, jakie zostały zakolejkowane od ostatniej klatki obrazu.

sf::Event event;
while (window.pollEvent(event)) {
    // "close requested" event: we close the window
    if (event.type == sf::Event::Closed) {
        std::cout << "Closing Window" << std::endl;
        window.close();
    }

    if (event.type == sf::Event::KeyReleased) {
        if (event.key.code == sf::Keyboard::Space) {
            std::cout << "Space released" << std::endl;
        }
    }

    if (event.type == sf::Event::MouseButtonPressed) {
        if(event.mouseButton.button == sf::Mouse::Left) {
            sf::Vector2i mouse_pos = sf::Mouse::getPosition(window);
            std::cout << "Mouse clicked: " << mouse_pos.x << ", " << mouse_pos.y << std::endl;
        }
    }
}

Polling (sprawdzanie stanu)

Aby uzyskać informację o bieżącym stanie urządzenia wejścia, niezależnie od systemu zdarzeń, SFML oferuje klasy sf::Keyboard oraz sf::Mouse. Są to klasy statyczne, co oznacza, że nie możemy utworzyć ich instancji - w programie istnieje jedna, globalna instancja każdej z tych klas i możemy odwoływać się do jej metod z dowolnego miejsca w programie. Wynika to ze specyfiki urządzeń wejścia - nawet, jeśli podłączymy do komputera kilka myszy lub klawiatur, z punktu widzenia aplikacji są widoczne jako jedno źródło wejścia.

W przypadku gier często nie reagujemy na zdarzenia (np. kliknięcie czy wprowadzanie tekstu), tylko chcemy sprawdzić w danym momencie stan przycisku (np. przytrzymanie klawisza na klawiaturze powoduje ciągły ruch postaci w danym kierunku). Możemy to zrobić w następujący sposób, z dowolnego miejsca w programie:

if(sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) {
    std::cout << "Up key is pressed" << std::endl;
}

if(sf::Mouse::isButtonPressed(sf::Mouse::Middle)) {
    std::cout << "Middle mouse button is pressed" << std::endl;
}

Uwaga! Po zmianie rozmiaru okna, współrzędne “widoku”, w którym poruszają się elementy pozostają niezmienione - mają zakresy takie, jak w momencie tworzenia okna. Współrzędne, które będziemy otrzymywali w zdarzeniach od myszy są jednak wyrażone w pikselach, w aktualnych współrzędnych okna, zatem może istnieć konieczność przemapowania ich na aktualne współrzędne sceny:

sf::Vector2f mouse_position = window.mapPixelToCoords(sf::Mouse::getPosition(window));

Zadanie końcowe 🛠🔥

Poruszanie prostokątem klawiszami

Dodaj do prostokąta prywatną właściwość logiczną (pole) is_selected o domyślnej wartości false, która będzie zmieniała zachowanie prostokąta. Dodaj odpowiednie metody select() i unselect() pozwalające na ustawienie wybranego stanu. Zmodyfikuj metodę animate, tak aby działała różnie w zależności od aktualnego stanu is_selected:

Dodaj do sceny kilka prostokątów, jednemu z nich ustaw is_selected na true. Przetestuj działanie programu.

Wybór przesuwanego prostokąta/prostokątów

Dodaj do sceny 10 prostokątów w losowych pozycjach (w obrębie okna), umieść je w kontenerze.

W głównej pętli zdarzeń przechwytuj kliknięcia i sprawdzaj czy znajdują się w obrębie któregoś z prostokątów:

Przykładowy fragment kodu, który możesz wykorzystać:

std::vector<CustomRectangleShape> rectangles;

for (int i=0; i<10; i++) {
    sf::Vector2f size(120.0, 60.0);
    sf::Vector2f position(std::rand() % (window.getSize().x - 120), std::rand() % (window.getSize().y - 60));
    rectangles.emplace_back(CustomRectangleShape(size, position));
}

for (auto &rec : rectangles) {
    rec.setFillColor(sf::Color(0, 255, 0));
    rec.setBounds(0, window.getSize().x, 0, window.getSize().y);
    rec.setSpeed(100, 200, 10);
}

while (window.isOpen()) {

    /* ... */

    for(const auto &rec : rectangles) {
        window.draw(rec);
    }

    window.display();
}
Cool rectangles

Autorzy: Dominik Pieczyński, Jakub Tomczyński, Tomasz Mańkowski