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:
const std::string &name, int number_of_wheels,
Vehicle(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", 2, "Muscles", 30) {}
Bike() : Vehicle( };
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:
public
: wszystkie pola z klasy bazowej zachowują swój poziom dostępu,protected
: publiczne pola i metody klasy bazowej stają się chronione w klasie potomnej,private
: chronione i publiczne pola klasy bazowej stają się prywatne w klasie potomnej.
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:
const std::string &name, const std::string &propulsion_type,
Car(double max_speed, bool has_abs)
4, propulsion_type, max_speed),
: Vehicle(name, 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:
"Volkswagen Passat", "Diesel", 200, true);
Car passat(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:
- traktor,
- motocykl,
- samolot,
- helikopter.
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.
- Zdefiniuj klasę tak, aby możliwe było utworzenie jej obiektu w następujący sposób:
120.0, 60.0);
sf::Vector2f size(120.0, 60.0);
sf::Vector2f position( 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:
100, 50, 250)); my_rectangle.setFillColor(sf::Color(
- 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:
100, 150, 10); // predkosc x, y, obrotowa my_rectangle.setSpeed(
Podpowiedź: pola w klasach mogą mieć domyślną wartość przypisaną przy deklaracji pola:
private:
int speed_x_ = 0;
- 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();/* ... */
- 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:
- poprzez podanie lewej, prawej, górnej i dolnej granicy:
0, window.getSize().x, 0, window.getSize().y); my_rectangle.setBounds(
- poprzez podanie obiektu typu
sf::IntRect
zawierającego prostokąt opisujący granice obszaru:
10, 10, 200, 100); // prostokat o poczatku w punkcie 10,10, szerokosci 200 i wysokosci 100
sf::IntRect rect1( my_rectangle.setBounds(rect1);
- 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 metodyanimate
.
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.
- 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
:
dla
is_selected == false
prostokąt porusza się tak jak w poprzednim zadaniu, odbijając od ściandla
is_selected == true
prostokąt nie porusza się samoczynnie, reaguje natomiast na naciśnięcia klawiszy strzałek na klawiaturze, przesuwając się płynnie w wybranym kierunku; pamiętaj aby prędkość była stała niezależnie od liczby wyświetlanych klatek na sekundę oraz zabezpiecz prostokąt przed opuszczeniem okna.
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:
- kliknięcie lewym przyciskiem “wybiera” prostokąt i ustawia jego kolor na losowy
- kliknięcie prawy przyciskiem usuwa wybór z prostokąta i przywraca domyślny kolor
Przykładowy fragment kodu, który możesz wykorzystać:
std::vector<CustomRectangleShape> rectangles;
for (int i=0; i<10; i++) {
120.0, 60.0);
sf::Vector2f size(std::rand() % (window.getSize().x - 120), std::rand() % (window.getSize().y - 60));
sf::Vector2f position(
rectangles.emplace_back(CustomRectangleShape(size, position));
}
for (auto &rec : rectangles) {
0, 255, 0));
rec.setFillColor(sf::Color(0, window.getSize().x, 0, window.getSize().y);
rec.setBounds(100, 200, 10);
rec.setSpeed(
}
while (window.isOpen()) {
/* ... */
for(const auto &rec : rectangles) {
window.draw(rec);
}
window.display(); }
Autorzy: Dominik Pieczyński, Jakub Tomczyński, Tomasz Mańkowski