Jak zorganizować silnik gry w C ++? Czy korzystanie z dziedziczenia jest dobrym pomysłem?

11

Jestem początkującym zarówno w tworzeniu gier, jak i programowaniu.

Próbuję nauczyć się jakiejś zasady budowania silnika gry.
Chcę stworzyć prostą grę, jestem w punkcie, w którym próbuję wdrożyć silnik gry.

Pomyślałem więc, że mój silnik gry powinien kontrolować te rzeczy:

- Moving the objects in the scene
- Checking the collisions
- Adjusting movements based on collisions
- Passing the polygons to the rendering engine

Moje obiekty zaprojektowałem w ten sposób:

class GlObject{
private:
    idEnum ObjId;
    //other identifiers
public:
    void move_obj(); //the movements are the same for all the objects (nextpos = pos + vel)
    void rotate_obj(); //rotations are the same for every objects too
    virtual Polygon generate_mesh() = 0; // the polygons are different
}

i mam 4 różne przedmioty w mojej grze: samolot, przeszkoda, gracz, kula i zaprojektowałem je w następujący sposób:

class Player : public GlObject{ 
private:
    std::string name;
public:
    Player();
    Bullet fire() const; //this method is unique to the class player
    void generate_mesh();
}

Teraz w silniku gry chcę mieć ogólną listę obiektów, na której mogę sprawdzać na przykład kolizję, przenosić obiekty itd., Ale chcę również, aby silnik gry przyjmował polecenia użytkownika do sterowania odtwarzaczem ...

Czy to dobry pomysł?

class GameEngine{
private:
    std::vector<GlObject*> objects; //an array containg all the object present
    Player* hPlayer; //hPlayer is to be read as human player, and is a pointer to hold the reference to an object inside the array
public:
    GameEngine();
    //other stuff
}

Konstruktor GameEngine będzie taki:

GameEngine::GameEngine(){
    hPlayer = new Player;
    objects.push_back(hPlayer);
}

Używam wskaźnika, ponieważ muszę wywołać ten, fire()który jest unikalny dla obiektu Player.

Więc moje pytanie brzmi: czy to dobry pomysł? Czy moje użycie dziedziczenia jest tutaj nieprawidłowe?

Pella86
źródło
Widziałem je po opublikowaniu, a kompozycja to świetny pomysł! Zawsze boję się, że w końcu użyję c z klasami zamiast mieć prawdziwy projekt c ++!
Pella86
Myślę, że istnieje wiele innych powodów, dla których warto używać kompozycji niż dziedziczenia ... Moja odpowiedź podaje kilka przykładów dotyczących korzyści z elastyczności, jakie zapewnia. Dobry przykład implementacji można znaleźć w linkach do artykułów.
Coyote

Odpowiedzi:

12

Zależy od tego, co rozumiesz przez „dobry”. Z pewnością wykona to zadanie. Ma wiele zalet i wad. Nie znajdziesz ich, dopóki silnik nie będzie o wiele bardziej złożony. Zobacz to pytanie dotyczące SO w celu dyskusji na ten temat.

Tak czy inaczej, jest mnóstwo opinii, ale jeśli kod silnika jest wciąż na tak wczesnym etapie, trudno będzie ci wyjaśnić, dlaczego może być źle.

Ogólnie rzecz biorąc, Scott Meyers ma kilka wspaniałych rzeczy do powiedzenia na temat dziedziczenia. Jeśli nadal jesteś na tym etapie rozwoju, przeczytaj tę książkę (Effective C ++), zanim przejdziesz dalej. To fantastyczna książka, która jest wymagana przez programistów pracujących w naszej firmie. Ale podsumowując radę: jeśli piszesz klasę i rozważasz zastosowanie dziedziczenia, bądź absolutnie jasny, że relacja między klasą a jej przodkiem jest relacją „jest”. Oznacza to, że każde B jest literą A. Nie używaj dziedziczenia jako łatwego sposobu na zapewnienie B dostępu do funkcjonalności A, która prowadzi do poważnych problemów architektonicznych i istnieją inne sposoby na to.

Mówiąc prościej; w przypadku silnika dowolnej wielkości szybko okaże się, że obiekty rzadko wpadają w schludną, hierarchiczną strukturę, dla której działa dziedziczenie. Używaj go, gdy jest to właściwe, ale nie wpadaj w pułapkę używania go do wszystkiego - poznaj inne sposoby wzajemnego powiązania obiektów.

MrCranky
źródło
Popieram opinię na temat dziedziczenia. Moją osobistą zasadą jest: nie rób tego, chyba że będziesz musiał to zrobić inaczej, byłoby to głupie. Innymi słowy, dziedziczenie jest złym pomysłem w 99% przypadków (przynajmniej :)). Warto też oddzielić logikę gry (obracanie / przesuwanie) od renderowania (generate_mesh ()). Inną sprawą jest fire () - zastanów się nad listą akcji i ogólną funkcją akcji (moja aktywność) - w ten sposób nie skończysz z bazillionem różnych funkcji rozmieszczonych we wszystkich klasach. W obu przypadkach zachowaj prostotę i bezlitośnie refaktuj, gdy napotkasz problemy.
Gilead
8

W przypadku danych logicznych i funkcji twoich obiektów gry (bytów) zdecydowanie zalecałbym użycie kompozycji zamiast dziedziczenia. Dobrym początkiem byłoby podejście oparte na komponentach. Jest dobrze opisany w tym artykule dotyczącym programowania kowbojów, który opisuje, w jaki sposób można zaimplementować tę architekturę i jakie korzyści przyniosła seria Tony Hawk.

Również ten artykuł o systemach encji zainspirował mnie, gdy po raz pierwszy go przeczytałem.

Dziedziczenie jest doskonałym narzędziem do polimorfizmu. Z wyjątkiem bardzo prostych projektów powinieneś unikać używania go jako sposobu na uporządkowanie bytów i logiki gry. Jeśli przejdziesz do dziedziczenia, na pewno skończysz z koniecznością replikacji i utrzymywania kopii tego samego kodu w celu zaimplementowania funkcji, których nie można odziedziczyć od wspólnego rodzica. Prawdopodobnie zaczniesz zanieczyszczać swoje klasy logiką i danymi najczęściej używanymi, aby uniknąć replikacji zbyt dużej ilości kodu.

Komponenty są bardzo przydatne w wielu sytuacjach. Korzystając z nich, możesz osiągnąć znacznie więcej:

Elastyczność z typami twoich bytów: Są momenty w rozwoju gier, w których niektóre byty ewoluują nagle i podczas gdy twórca gry podejmuje decyzję, deweloper będzie musiał je wdrożyć. Wystarczy kilka kroków, aby dodać istniejące zachowanie bez replikacji kodu. Systemy oparte na komponentach umożliwiają wprowadzanie większej liczby zmian przez programistów. Na przykład każdy typ bytu zamiast reprezentowany przez klasę może być reprezentowany przez plik opisu, który zawiera wszystkie nazwy składników i parametry potrzebne do skonfigurowania typu. Umożliwia to programistom wykonywanie i testowanie zmian bez ponownej kompilacji gry. Wystarczy edycja plików opisu, aby dodać, usunąć lub zmienić komponenty używane przez każdy typ encji.

Elastyczność w określonych przypadkach i określonych scenariuszach : Są przypadki, w których potrzebujesz, aby Twój gracz mógł wyposażyć się w broń lub przedmioty, możesz dodać do ekwipunku komponent ekwipunku. podczas rozwoju twojej gry będziesz potrzebować innych jednostek, aby mieć ekwipunek (na przykład wrogów). W pewnym momencie masz w scenariuszu sytuację, w której drzewo powinno zawierać jakiś ukryty przedmiot. Dzięki komponentom możesz dodawać komponenty magazynowe nawet do encji, których typ nie został pierwotnie zaprojektowany, aby mieć taki bez dodatkowych nakładów programistycznych.

Elastyczność w czasie wykonywania : Komponenty oferują bardzo skuteczny sposób zmiany zachowania jednostek w czasie wykonywania. Możesz dodawać i usuwać komponenty w czasie wykonywania, a tym samym tworzyć lub usuwać zachowania i właściwości w razie potrzeby. Pozwalają również na podzielenie różnych typów danych na grupy, które można (czasem) obliczać osobno równolegle na wielu procesorach graficznych.

Elastyczność w czasie : Komponenty mogą być używane do zachowania zgodności między wersjami. Na przykład, gdy wprowadzane są zmiany między wydaniami gry, możesz (nie zawsze) po prostu dodać nowe komponenty, które implementują nowe zachowania i zmiany. Następnie gra utworzy odpowiednie komponenty dołączone do twoich bytów w zależności od załadowanego pliku zapisu, połączenia równorzędnego lub serwera, do którego dołącza gracz. Jest to bardzo przydatne w scenariuszach, w których nie można kontrolować dat wydania nowej wersji na wszystkich platformach (Steam, Mac App Store, iPhone, Android ...).

Aby uzyskać lepszy wgląd, spójrz na pytania oznaczone tagami [oparte na komponentach] i [encji-systemie] .

Kojot
źródło