Obiektowy design turowej gry planszowej

0

Cześć
Chciałem się spytać o jakieś sugestie odnośnie obiektowego designu dla planszowej gry turowej opartej o grid. Gra składa się z planszy n x m, po której porusza się dowolna liczba graczy, każdy sterowany z klawiatury i myszki lub np. poprzez sieć (serwer odpowiada za komunikację między graczami lub nawet p2p bez serwera). Pisane jest to w JS, ale to nie powinno mieć dużego znaczenia na razie. Dotychczas napisałem klasę GameWrapper, która ma klasy (obiekty klas) Render, Board, Players. Render ma być wymienne i aktualnie statycznie inicjalizowany jest obiekt klasy Render (lub niepisanego interfejsu render), która korzysta z canvas i na nim renderuje. Players zawiera tablicę graczy klasy Player, a każdy Player ma jeszcze PlayerControlls. Board zawiera dwuwymiarową tablicę obiektów klasy Field. W skrócie to tak właśnie wygląda.

  1. Czy taki design byłby uznany całkiem ok, czy polecany byłby jakiś inny, ewentualnie z jakimiś wzorcami konkretnymi?
  2. Odpowiedni obiekt PlayerControlls reaguje na input i odsyła przetworzony input (np. ruch góra/dół/lewo/prawo) do obiektu Player do metody move, tam jest reszta akcji, np. sprawdzenie, czy teraz jest kolej tego gracza poprzez sprawdzenie zmiennej isMoving. Jednak aby PlayerControlls mógł odwołać się do obiektu Player, który go ma, PlayerControlls musi mieć zmienną, która się do niego odnosi, przez co PlayerControlls dostaje this w konstruktorze lub w metodzie setPlayer podczas inicjalizowania konkretnego Player. Teraz przy inicjalizacji PlayerControlls w Player trzeba wołać tę metodę. Czy takie coś jest raczej ok?
    Podobnie jest z klasą Render. Taki obiekt też musi dostać odniesienie do Board i Players, aby sprawdzić kolory pól i położenia graczy i potem to wyświetlić. Na dodatek następna iteracja w pętli gry (której tutaj nie ma w typowej postaci) dzieje się wtedy, gdy PlayerControlls woła move na Player i ten sprawdza, że jest jego kolejka, wtedy odwołuje się do Board aby formalnie wykonać ruch i do Render, aby odświeżyć wyświetlaną rozgrywkę. To akurat można by sprowadzić do fasady w GameWrapper, ale wtedy Player powinien mieć jeszcze zmienną odnoszącą się do tego obiektu.
    Czy taki design jest raczej ok? Czy to odnoszenie się do obiektów, przez obiekty zawarte i zmienne, które się do nich odnoszą oraz te metody w stylu setPlayer, które w znacznej części wypadków dostają obiekt, który zawiera dany obiekt jest też raczej ok?
1

Ja bym zdecydowanie oddzielił logikę gry od wizualizacji. To jest, skupiłbym się najpierw na klasach regulujących zasady gry, planszę, jej elementy, jednostki itp., ale całkiem bez odniesienia do wyglądu.

Potem, jako osobną warstwę, próbowałbym zrobić renderowanie planszy i sterowanie przez graczy.

Powyższe podejście ma oczywiste zalety ułatwionego testowania osobnych elementów gry (logiki i widoku), a ponadto łatwej rozbudowy i przebudowy -- na przykład gdybyś chciał to przenieść na inny interfejs itp...

Ja odniosłem wrażenie, że tutaj niepotrzebnie mieszasz te rzeczy trochę...

0

No właśnie starałem się tego nie mieszać. Mówiłem o niepisanym interfejsie dla klas Render i że na razie jest statycznie dobierana tam klasa, która renderuje na canvas. Jeżeli będzie trzeba renderować na, powiedzmy, SVG, to podmienię klasę na inną, która wspiera ten sam interfejs (np. metoda render). Odnośnie kontrolowania i sterowania przez graczy, to każdy gracz dodany, jako obiekt klasy Player zawiera w sobie obiekt, który wspiera interfejs PlayerControlls. Na razie jest takowy dla strzałek dla gracza 1 i dla WSAD dla gracza 2, potem można dopisać i dla WebSocket i grać online, a podmienić tylko obiekt PlayerControlls. To właśnie jest tutaj oddzielone, przynajmniej trochę, wymienne i połączone w obiekcie GameWrapper. Jednak tak jak wyżej opisane z pytaniami, co byście sugerowali, aby obiekt Render mógł spokojnie sięgać do Board i Players, aby to wyrenderować, czy takie metody pokroju setBoard, które woła GameWrapper i przesyła tam uprzednio zaincjowany obiekt Board, czy to jest raczej ok? Bo potem takie plątaniny tych zmiennych z odniesienia są i od Player i od Board i od innych klas zawartych w GameWrapper. Czy PlayerControlls, który woła Player.move(); i przez to iteruje tą nieformalną pętlą gry, jest raczej ok? Czy coś innego można by tu polecić?

0

Jeśli chcesz podmieniać implementacje renderowania na pisz sobie stukure/class RenderData którą zwracają obiekty do na rysowania potem coś w stylu

 
                RenderFuction(RenderInstance){
                var toRender = GetRenderData(players);
                forEach(tr in toRender)
                {
                                RenderInstance.Rendering(tr);
                }
}

W ten sposób żeby zmienić implementacje wytarczy podmienić jeden konstruktor. Nie łąmiesz przy tym enkapsuacji. minus jest taki że musisz wiedzieć co chcesz rederować. Jak tego nie wiesz i będziesz zmieniał interfejs renderData co 5 min to krzyż na droge.
A mozesz spróbować napisac "menadżera średniego szczebla" taką klase której jedynem zadaniem bedzię spinanie pozostałych klas do kupy(ale tylko kilku, to nie ma być god object).

0

Mniej więcej miałoby to wyglądać tak, że gdy Player.move(); robi ruch, to potem woła GameWrapper.render(); a ten woła getRenderData();. Teraz Player potrzebuje tylko zmienną od GameWrapper, ale nadal aby wykonać ruch powinien mieć odniesienie do Board. Znowu zastosować tutaj coś w stylu fasady i zrobić GameWrapper.move();, który obsłuży planszę i całą resztę odnośnie ruchu, a poszczególne obiekty zawarte w nim powinny mieć tylko odniesienie do GameWrapper i tam wołać metody od podstawowej funkcjonalności, która zachodzi podczas rozgrywki. Takie podejście byłoby raczej ok? Z tym "menadżerem średniego szczebla" to chodzi właśnie o jakiś wrapper, czy co innego dokładniej?

1
Złoty Krawiec napisał(a):

Mniej więcej miałoby to wyglądać tak, że gdy Player.move(); robi ruch, to potem woła GameWrapper.render();...

No więc starałeś się nie mieszać, a mieszasz... :) Dlaczego gracz (element logiki gry) woła w ogóle renderowanie (choćby abstrakcyjne, ale będą ce elementem wizualizacji gry)...? Ja bym to starał się rozdzielić bardziej. Warstwa logiki w ogóle niezależnie napisana, a tylko dająca znać jakiemuś obserwatorowi, że coś się zmieniło na planszy i trzeba to przerysować. Rozumiem, że tak właśnie starsza się to robić, ale mam wrażenie, że jednak projektując/implementując logikę gry wciąż myślisz, co ma zrobić silnik wyświetlający. A powinieneś wtedy o tym nie myśleć. :)

0

Trochę może to połączone, ale gdyby był tam obserwator, to GameWrapper by się zarejestrował u graczy i potem samemu wołał renderowanie. Czyli Player zamiast wołać na nim render() to zawoła getNotification() (czy jak to tam w obserwatorze nazwać). To jest taki nieoficjalny obserwator, jedyne co Player musi znać (poza odniesieniem do planszy), to metodę render() na GameWrapper, przy obserwatorze byłoby to notify(). Może byłaby to jakaś standaryzacja, ale w obecnym momencie zmieniłaby się tylko nazwa metody, obserwator może gdzie indziej by tu pasował, w tym miejscu na pewno trochę standaryzuje, lecz nie zmienia tej plątaniny odniesień. Co byłoby tutaj polecane? Może coś w stylu dekoratora, że gra zaczyna się tylko od logiki, potem dodajesz graczy (logika gracza owinięta w odpowiedni kontroler gracza) i takie coś opakować jeszcze w klasę od renderowania.

0

Wiesz co, rozrysuj to na diagramie (choćby w tym: https://www.draw.io/ ,ale równie dobrze możesz rozrysować na kartce i wkleić tu zdjęcie). Będzie dużo prościej ocenić czy to ma sens czy nie. Jeden obrazek/diagram i w ciągu kilku sekund widać co się dzieje i jakie są relacje pomiędzy poszczególnymi częsciami programu.

PlayerControlls

pisze się to przez jedno l: PlayerControls. Ew. PlayerControllers

0

Wiesz co najlepiej bedzie jak napiszesz sobie ponga, pacmana, czołgi z pegazusa. zobaczysz jak bardzo mieszanie wszystko komplikuje i bedzie mniej pytan :)

0

Diagram rysowałem sobie na kartce, narysuję może w tej stronce i tutaj wrzucę. PlayerControlls, bo to kontrolowanie przez gracza i przy okazji trochę kontroler, bo to do niego idzie input i ten wybiera co dalej. Takie zlepienie 2 wyrazów.
Pisałem gierki, to jak mieszanie komplikuje jest raczej jasne. Tutaj właśnie staram się nie mieszać i trzymać te klasy oddzielnie. Nikt w sumie nie odpowiedział, co by uważał o takim desginie albo jaki inny by tu zaproponował. Diagram zapewne wrzucę tutaj później, ale w takim razie może niech ktoś zaproponuje jakiś diagram dla takiej gierki, czy jakby to połączył. Chodzi o propozycje, jak inaczej mogłyby wyglądać klasy w takiej gierce. Czy klasa od renderowania byłaby jako dekorator, czy GameWrapper by ją trzymał. Obok takiego designu, jak inaczej by to wyglądało.

0

Narysowany diagram klas w uproszczeniu. Tam gdzie w klasach powtarza się render, board lub players, odnosi się to obiektów tych klas z GameWrapper. W skrócie opisany jest też game loop. Nie ma na diagramie na razie za wiele odnośnie logiki gry i tego, kiedy gra jest zakończona. To jest do dodania do tego diagramu.

0

która ma klasy (obiekty klas) Render, Board, Players. Render ma być wymienne i aktualnie statycznie inicjalizowany jest obiekt klasy Render (lub niepisanego interfejsu render), która korzysta z canvas i na nim renderuje. Players zawiera tablicę graczy klasy Player, a każdy Player ma jeszcze PlayerControlls. Board zawiera dwuwymiarową tablicę obiektów klasy Field. W skrócie to tak właśnie wygląda.

To wydaje się w porządku (zachowana zasada separation of concerns odnośnie renderingu, inputów, mapy).

PlayerControlls, bo to kontrolowanie przez gracza i przy okazji trochę kontroler, bo to do niego idzie input i ten wybiera co dalej. Takie zlepienie 2 wyrazów.

dla mnie to nieintuicyjne jest i za rok możesz się zastanawiać co miałeś na myśli, kiedy to pisałeś.

Dotychczas napisałem klasę GameWrapper

po co Wrapper? GameWrapper mi się kojarzy z klasą Wrapperem, która posiada obiekt klasy Game. Jeśli nie masz klasy Game, ja bym zamiast GameWrapper zrobił po prostu Game.

Players zawiera tablicę graczy klasy Player, a każdy Player ma jeszcze PlayerControlls. Board zawiera dwuwymiarową tablicę obiektów klasy Field. W skrócie to tak właśnie wygląda.

kim jest Player? Co reprezentuje ta klasa? Gracza (Janek, Bartek, CPU1, CPU2 etc.)? Czy jednostkę poruszającą się po planszy? To nie to samo przecież, bo w wielu grach do jednego gracza może należeć ileś jednostek.

Chyba, że upraszczasz 1 gracz = 1 jednostka i masz świadomość ograniczeń. Wtedy okej. Zbytnia elastyczność jest często niepotrzebna, jak wiemy dokładnie co chcemy zrobić, i wybieramy ograniczoną prostotę zamiast rozdmuchanej często elastyczności.

Z drugiej strony co z obiektami, które nie należą do żadnego gracza? Np. "drzewo" czy "domek" na planszy? W jaki sposób to zaimplementujesz? (o ile oczywiście założyć, że na planszy będzie coś oprócz graczy).

A co jeśli gracz będzie chciał wystrzelić rakietę, strzałę z łuku itp.?

Ja raczej tak robię, że traktuję wszystkie obiekty na planszy po prostu jak jednostki (Entity). Player kojarzy mi się raczej z właścicielem jednostek.

. Czy PlayerControlls, który woła Player.move(); i przez to iteruje tą nieformalną pętlą gry, jest raczej ok? Czy coś innego można by tu polecić?

co do ruchów, zmianów stanu w grze i pętli gry to trzeba uważać na to, żeby się nie rozsynchronizowało. Lepiej rozdzielić same komendy ("porusz się", "umrzyj", "zmniejsz energię o 10", "usuń obiekt") od faktycznych zmian (np. object.x = 10, object.energy -= 10 etc.).

Bo jeśli będziesz przechodził w pętli przez wszystkie obiekty, to raczej chciałbyś pewnie założyć, że przechodzisz przez obiekty w pewnym stanie gry, np. w turze 51.
Jeśli jednak będziesz na żywca zmieniał je w pętli to potem okaże się, że niektóre obiekty są już w turze 51, niektóre w turze 52. Wobec tego stan gry będzie niespójny. A jeśli będzie niespójny to komunikacja między obiektami (np. odczytanie pozycji innego obiektu) będzie rozsynchronizowana.

Dlatego np. ja wolę wszystkie ruchy obiektów enkapsulować jako obiekt komunikatu (np. {type: 'move', x: 10, y: 20}) i zbierać gdzieś te komunikaty i przejść przez obiekty w grze, ale nic nie robić z nimi. A dopiero potem znowu przejść i zmieniać poszczególne obiekty.

No i dobrze jest też zapamiętywać gdzieś poprzedni stan gry (albo po prostu nie zmieniać go poza odpowiednimi miejscami w kodzie). Albo odwrotnie - nie pamiętać starego stanu gry, tylko zbierać "apdejty" do nowego. Wtedy mógłbyś nawet poradzić sobie bez komunikatów*, i nawet w pętli wywoływać metody move(), attack() itp. które jednak by nie zmieniały stanu obiektów, tylko raczej zmieniały by "przyszły stan obiektu".

A potem przejść jeszcze raz przez obiekty i hurtowo zamienić stary stan na nowy.

*chociaż jak to gra multiplayer, to i tak warto robić komunikaty, choćby do komunikacji klient/serwer.

1 użytkowników online, w tym zalogowanych: 0, gości: 1