Rozdzielenie rysowania i logiki w grach

11

Jestem programistą, który dopiero zaczyna bałagan w tworzeniu gier. Jestem facetem .Net, więc pomieszałem z XNA i teraz bawię się Cocos2d na iPhone'a. Moje pytanie jest jednak bardziej ogólne.

Załóżmy, że buduję prostą grę w Ponga. Miałbym Ballklasę i Paddleklasę. Wychodząc z rozwoju świata biznesu, moim pierwszym instynktem jest brak kodu do rysowania lub wprowadzania danych w żadnej z tych klas.

//pseudo code
class Ball
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Move(){}
}

Nic w klasie piłek nie obsługuje wprowadzania ani nie zajmuje się rysowaniem. Miałbym wtedy inną klasę, moją Gameklasę lub moją Scene.m(w Cocos2d), która wprowadzałaby nową piłkę, a podczas pętli gry manipulowała piłką w razie potrzeby.

Chodzi o to, że w wielu samouczkach dla XNA i Cocos2d widzę taki wzór:

//pseudo code
class Ball : SomeUpdatableComponent
{
   Vector2D position;
   Vector2D velocity;
   Color color;
   void Update(){}
   void Draw(){}
   void HandleInput(){}
}

Moje pytanie brzmi, czy to prawda? Czy jest to wzór, który ludzie używają do tworzenia gier? Jakoś jest to sprzeczne ze wszystkim, do czego jestem przyzwyczajony, aby moja klasa Ball zrobiła wszystko. Co więcej, w tym drugim przykładzie, w którym Ballwiem, jak się poruszać, jak poradziłbym sobie z wykrywaniem kolizji z Paddle? Czy Ballpotrzeba znajomości Paddle? W moim pierwszym przykładzie Gameklasa miałaby odniesienia do obuBall i do Paddle, a następnie wysłałaby obie te CollisionDetectionrzeczy do jakiegoś menedżera lub czegoś, ale jak mam poradzić sobie ze złożonością różnych komponentów, jeśli każdy pojedynczy komponent robi wszystko sam? (Mam nadzieję, że mam sens .....)

BFree
źródło
2
Niestety tak właśnie dzieje się w wielu grach. Nie uważałbym tego jednak za najlepszą praktykę. Wiele samouczków, które czytasz, może być skierowanych do początkujących, więc nie zamierzają wyjaśniać czytelnikowi modelu / widoku / kontrolera ani wzorców jarzma.
tenpn
@tenpn, twój komentarz wydaje się sugerować, że nie uważasz, że to dobra praktyka, zastanawiałem się, czy możesz wyjaśnić coś więcej? Zawsze uważałem, że jest to najbardziej odpowiednie ustawienie ze względu na metody zawierające kod, który prawdopodobnie jest bardzo specyficzny dla każdego typu; ale sam mam niewielkie doświadczenie, więc chciałbym usłyszeć wasze myśli.
sebf
1
@sebf nie działa, jeśli jesteś w projektowaniu zorientowanym na dane i nie działa, jeśli lubisz projektowanie zorientowane obiektowo. Zobacz SOLIDNE księgi
tenpn
@tenpn. Ten artykuł i dokument w nim powiązane są bardzo interesujące, dziękuję!
15:15

Odpowiedzi:

10

Moje pytanie brzmi, czy to prawda? Czy jest to wzór, który ludzie używają do tworzenia gier?

Twórcy gier zwykle używają tego, co działa. To nie jest rygorystyczne, ale hej, wysyła.

Jakoś jest to sprzeczne ze wszystkim, do czego jestem przyzwyczajony, aby moja klasa Ball zrobiła wszystko.

Bardziej uogólnionym idiomem tego, co masz, jest uchwytMessages / update / draw. W systemach, które intensywnie wykorzystują wiadomości (które mają zalety i wady, jak można się spodziewać), podmiot gry chwyta wszystkie wiadomości, na których im zależy, wykonuje logikę na tych wiadomościach, a następnie sam się rysuje.

Zauważ, że to wywołanie draw () może w rzeczywistości nie oznaczać, że jednostka wywołuje putPixel lub cokolwiek w sobie; w niektórych przypadkach może to po prostu oznaczać aktualizację danych, które są później odpytywane przez kod odpowiedzialny za rysowanie. W bardziej zaawansowanych przypadkach „rysuje się”, wywołując metody udostępnione przez mechanizm renderujący. W technologii, nad którą teraz pracuję, obiekt rysuje się za pomocą wywołań renderera, a następnie każda ramka, w której wątek renderujący organizuje wszystkie wywołania wszystkich obiektów, optymalizuje pod kątem zmian stanu, a następnie wyciąga całość (wszystkie tego, co dzieje się w wątku innym niż logika gry, więc otrzymujemy renderowanie asynchroniczne).

Przekazanie odpowiedzialności należy do programisty; w przypadku prostej gry w ponga sensowne jest, że każdy obiekt całkowicie obsługuje własny rysunek (wraz z wywołaniami niskiego poziomu). To kwestia stylu i mądrości.

Co więcej, w tym drugim przykładzie, w którym moja kula wie, jak się poruszać, jak poradziłbym sobie z wykrywaniem kolizji za pomocą wiosła? Czy piłka musiałaby mieć wiedzę na temat wiosła?

Jednym z naturalnych sposobów rozwiązania tego problemu jest to, aby każdy obiekt przyjmował każdy inny obiekt jako pewien rodzaj parametru do sprawdzania kolizji. Skaluje się słabo i może mieć dziwne wyniki.

Kazanie każdej klasie zaimplementować jakiś interfejs Collidable, a następnie mieć kod wysokiego poziomu, który po prostu zapętla wszystkie obiekty Collidable w fazie aktualizacji twojej gry i obsługuje kolizje, ustawiając odpowiednio pozycje obiektów.

Ponownie, musisz po prostu wyobrazić sobie (lub podjąć decyzję), w jaki sposób należy przekazać odpowiedzialność za różne rzeczy w grze. Renderowanie jest pojedynczą czynnością i może być obsługiwane przez obiekt w próżni; zderzenie i fizyka zasadniczo nie mogą.

W moim pierwszym przykładzie klasa Game zawierałaby odniesienia zarówno do piłki, jak i łopatki, a następnie wysyłałaby je do jakiegoś menedżera CollisionDetection czy coś takiego, ale jak mam poradzić sobie ze złożonością różnych komponentów, jeśli każdy pojedynczy komponent wszystko sami?

Zobacz powyżej, aby dowiedzieć się więcej na ten temat. Jeśli jesteś w języku, który z łatwością obsługuje interfejsy (tj. Java, C #), jest to łatwe. Jeśli jesteś w C ++, może to być ... interesujące. Jestem zwolennikiem używania wiadomości do rozwiązywania tych problemów (klasy obsługują wiadomości, które potrafią, ignorują inne), a niektórzy ludzie lubią składanie komponentów.

Ponownie, cokolwiek działa i jest dla ciebie najłatwiejsze.

ChrisE
źródło
2
Aha, i zawsze pamiętaj: YAGNI (nie będziesz go potrzebować) jest tutaj naprawdę ważna. Możesz zmarnować dużo czasu na wymyślenie idealnego elastycznego systemu, który poradzi sobie z każdym możliwym problemem, a potem nigdy nic nie zrobisz. Chodzi mi o to, że jeśli naprawdę chcesz dobrego szkieletu dla wielu graczy dla wszystkich możliwych wyników, użyłbyś CORBA, prawda? :)
ChrisE
5

Niedawno stworzyłem prostą grę Space Invadors, używając „systemu bytów”. Jest to wzór, który bardzo dobrze oddziela atrybuty i zachowania. Pełne zrozumienie tego zajęło mi kilka iteracji, ale kiedy już zaprojektujesz kilka komponentów, tworzenie nowych obiektów przy użyciu istniejących komponentów staje się niezwykle proste.

Powinieneś przeczytać to:

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

Jest często aktualizowany przez niezwykle kompetentnego faceta. To także jedyna dyskusja na temat systemu encji z konkretnymi przykładami kodu.

Moje iteracje przebiegały następująco:

Pierwsza iteracja miała obiekt „EntitySystem”, który był jak opisuje Adam; jednak moje komponenty nadal miały metody - mój komponent „renderowalny” miał metodę paint (), a mój element pozycji miał metodę move () itp. Kiedy zacząłem tworzyć elementy, zdałem sobie sprawę, że muszę zacząć przekazywać wiadomości między komponenty i zlecanie wykonania aktualizacji komponentów .... zbyt bałagan.

Więc wróciłem i ponownie przeczytałem blog T-Machines. W wątkach komentarzy jest wiele informacji - w nich naprawdę podkreśla, że ​​komponenty nie mają zachowań - zachowania są dostarczane przez systemy encji. W ten sposób nie trzeba przekazywać komunikatów między komponentami i zamawiać aktualizacji komponentów, ponieważ kolejność jest określona przez globalną kolejność wykonywania systemu. Dobrze. Może to zbyt abstrakcyjne.

W każdym razie dla iteracji nr 2 to właśnie poznałem z bloga:

EntityManager - działa jako komponent „baza danych”, do którego można wyszukiwać encje zawierające określone typy komponentów. Może to być nawet wspierane przez bazę danych w pamięci dla szybkiego dostępu ... zobacz t-machine część 5, aby uzyskać więcej informacji.

EntitySystem - każdy system jest w zasadzie tylko metodą działającą na zestawie entites. Każdy system użyje komponentu x, y i z encji, aby wykonać swoją pracę. Więc zapytasz menedżera o encje ze składnikami x, yiz, a następnie przekażesz ten wynik do systemu.

Podmiot - tylko identyfikator, jak długi. Encja jest tym, co grupuje zestaw instancji komponentów razem w „encję”.

Komponent - zestaw pól .... brak zachowań! kiedy zaczynasz dodawać zachowania, zaczyna się robić bałagan ... nawet w prostej grze Space Invadors.

Edycja : nawiasem mówiąc, „dt” to czas delta od ostatniego wywołania głównej pętli

Więc moja główna pętla Invadors jest następująca:

Collection<Entity> entitiesWithGuns = manager.getEntitiesWith(Gun.class);
Collection<Entity> entitiesWithDamagable =
manager.getEntitiesWith(BulletDamagable.class);
Collection<Entity> entitiesWithInvadorDamagable = manager.getEntitiesWith(InvadorDamagable.class);

keyboardShipControllerSystem.update(entitiesWithGuns, dt);
touchInputSystem.update(entitiesWithGuns, dt);
Collection<Entity> entitiesWithInvadorMovement = manager.getEntitiesWith(InvadorMovement.class);
invadorMovementSystem.update(entitiesWithInvadorMovement);

Collection<Entity> entitiesWithVelocity = manager.getEntitiesWith(Velocity.class);
movementSystem.move(entitiesWithVelocity, dt);
gunSystem.update(entitiesWithGuns, System.currentTimeMillis());
Collection<Entity> entitiesWithPositionAndForm = manager.getEntitiesWith(Position.class, Form.class);
collisionSystem.checkCollisions(entitiesWithPositionAndForm);

Na początku wygląda trochę dziwnie, ale jest niesamowicie elastyczny. Jest również bardzo łatwy do optymalizacji; dla różnych typów komponentów możesz mieć różne zapasowe magazyny danych, aby przyspieszyć pobieranie. W przypadku klasy „form” można ją zabezpieczyć czterokrotnie, aby przyspieszyć dostęp do wykrywania kolizji.

Jestem jak Ty; Jestem doświadczonym programistą, ale nie miałem doświadczenia w pisaniu gier. Poświęciłem trochę czasu na badania, które dały wzorce twórców, a ten przykuł moją uwagę. Nie jest to w żaden sposób jedyny sposób na robienie rzeczy, ale uważam to za bardzo intuicyjne i niezawodne. Wierzę, że wzorzec ten został oficjalnie omówiony w książce 6 serii „Gem Programming Gems” - http://www.amazon.com/Game-Programming-Gems/dp/1584500492 . Sam nie czytałem żadnej książki, ale słyszę, że są de facto odniesieniem do programowania gier.

homebrew
źródło
1

Nie ma wyraźnych zasad, których należy przestrzegać podczas programowania gier. Oba wzory uważam za całkowicie akceptowalne, pod warunkiem, że podążasz za tym samym wzorem w całej grze. Byłbyś zaskoczony, że istnieje wiele innych wzorów projektowania gier, z których kilka nie jest nawet na szczycie OOP.

Teraz, zgodnie z moimi osobistymi preferencjami, wolę rozdzielać kod według zachowania (jedna klasa / wątek do narysowania, inna do zaktualizowania), mając jednocześnie minimalistyczne obiekty. Łatwiej jest zrównoleglać i często pracuję nad mniejszą liczbą plików jednocześnie. Powiedziałbym, że jest to bardziej proceduralny sposób robienia rzeczy.

Ale jeśli pochodzisz ze świata biznesu, prawdopodobnie wygodniej jest ci pisać zajęcia, które wiedzą, jak zrobić wszystko dla siebie.

Jeszcze raz polecam napisać to, co uważasz za najbardziej naturalne.


źródło
Myślę, że źle zrozumiałeś moje pytanie. Mówię, że nie lubię zajęć, które same robią wszystko. Upadłem jak Piłka powinna być logiczną reprezentacją Piłki, jednak nie powinna wiedzieć nic o pętli gry, przetwarzaniu danych wejściowych itp.
BFree
Szczerze mówiąc, w programowaniu biznesowym, jeśli go sprowadzasz, istnieją dwa pociągi: obiekty biznesowe, które ładują, utrzymują i wykonują logikę na sobie, oraz DTO + AppLogic + Repository / Persitance Layer ...
Nate
1

obiekt „Kula” ma atrybuty i zachowania. Uważam, że sensowne jest uwzględnienie unikalnych atrybutów i zachowań obiektu we własnej klasie. Sposób, w jaki obiekt Ball jest aktualizowany, różni się od sposobu, w jaki wiosło aktualizuje, więc ma sens, że są to unikalne zachowania, które uzasadniają włączenie go do klasy. To samo z rysunkiem. Często występują wyjątkowe różnice w sposobie rysowania wiosła w stosunku do piłki i łatwiej jest mi zmodyfikować metodę rysowania pojedynczego obiektu, aby spełnić te potrzeby, niż wypracować oceny warunkowe w innej klasie, aby je rozwiązać.

Pong jest stosunkowo prostą grą pod względem liczby obiektów i zachowań, ale kiedy wejdziesz w dziesiątki lub setki unikalnych obiektów w grze, możesz znaleźć swój drugi przykład nieco łatwiej w modularyzacji wszystkiego.

Większość ludzi korzysta z klasy wejściowej, której wyniki są dostępne dla wszystkich obiektów gry za pośrednictwem usługi lub klasy statycznej.

Steve H.
źródło
0

Myślę, że to, czego prawdopodobnie używasz w świecie biznesu, to oddzielanie abstrakcji i implementacji.

W świecie twórców gier zazwyczaj myślisz o szybkości. Im więcej obiektów masz, tym więcej wystąpi przełączników kontekstu, co może spowolnić działanie w zależności od bieżącego obciążenia.

To tylko moje spekulacje, ponieważ jestem niedoszłym twórcą gier, ale profesjonalnym twórcą.

Joey Green
źródło
0

Nie, to nie jest „dobre” ani „złe”, to tylko uproszczenie.

Niektóre kody biznesowe są dokładnie takie same, chyba że pracujesz z systemem, który wymusza oddzielenie prezentacji i logiki.

Tutaj przykład VB.NET, w którym „logika biznesowa” (dodanie liczby) jest zapisana w module obsługi zdarzeń GUI - http://www.homeandlearn.co.uk/net/nets1p12.html

jak radzić sobie ze złożonością różnych komponentów, jeśli każdy pojedynczy komponent robi wszystko sam?

Jeśli i kiedy stanie się to skomplikowane, możesz to rozdzielić.

class Ball
{
    GraphicalModel ballVisual;
    void Draw()
    {
        ballVisual.Draw();
    }
};
Kylotan
źródło
0

Z mojego doświadczenia w rozwoju wynika, że ​​nie ma właściwego lub niewłaściwego sposobu robienia rzeczy, chyba że w grupie, z którą pracujesz, istnieje akceptowana praktyka. Spotkałem się z sprzeciwem programistów, którzy są niechętni do oddzielania zachowań, skór itp. I jestem pewien, że powiedzą to samo o moim zastrzeżeniu do pisania zajęć, które zawierają wszystko pod słońcem. Pamiętaj, że musisz nie tylko napisać go, ale także móc czytać swój kod jutro i w przyszłości, debugować go i być może wykorzystać go w następnej iteracji. Powinieneś wybrać ścieżkę, która działa dla ciebie (i zespołu). Wybranie trasy, z której korzystają inni (i jest dla Ciebie sprzeczne z intuicją) spowolni cię.

kosmoknot
źródło