Autor Wątek: wiele gier na serwerze, tylko 1 w pokoju  (Przeczytany 4444 razy)

Offline Puchaczov

  • Użytkownik

# Czerwiec 30, 2013, 17:53:39
Witam, mam taki problem, że nie wiem jak zrobić strukturę klas dla gier na serwerze i może Wy moglibyście mi coś podpowiedzieć

zasadniczo użytkownicy dołączają do serwera i wybierają grę w którą chcą zagrać i dla tej osoby tworzony jest pokój z grą (jej stanem) lub dołącza ona do otwartego pokoju (który został już stworzony przez innego gracza), czyli wybierana jest klasa z zaimplementowanymi zasadami gry które będą obowiązywały w danym pokoju. I tu mam problem bowiem gry nie są ze sobą za bardzo powiązane a mnie przydało by się coś żeby każdy pokój mógł przechowywać inna grę i dla niej interpretować po swojemu zasady wedle nadsyłanych parametrów. Myślałem żeby stworzyć coś takiego:

switch(WYBRANA_GRA_W_POKOJU)
{
  case 0:
    BaseGame = new Game1();
    break;
  case 1:
    BaseGame = new Game2();
}

//a pozniej na podstawie tego jaki typ gry jest w pokoju

 
switch(BaseGame.WhichGame())
{
  case 0:
    ((Game1)BaseGame).doSomething(...);
    break;
 case 1:
    ((Game2)BaseGame).doSomething(...,....,...); //inna gra
}


problemem jest to, że różne gry będą otrzymywały różne informacje od graczy (zmienna liczba parametrów,  ich typ). O ile jedna gra do sprawdzenia poprawności ruchu będzie potrzebowała 5 parametrów to 2ga może ich potrzebować tylko 2 albo 10. Teoretycznie mógłbym przesyłać listę stringów i potem w grze wedle uznać przerabiać je na inty lub typy które będą wymagane do oceny pozycji gracza itd.

Czy może napisać jedną klasę kontener który przechowa wszystkie gry? Byłoby to raczej marnowanie pamięci ale:

class GameContainer //bylby w kazdym pokoju
{ //moglbym inicjowac tylko ta gre ktora znajdzie sie w pokoju
   public Game1 game1 = new Game1(...);
   public Game2 game2 = new Game2(...,...,...);
}

//na serwerze
//gracz wybral gre "game1", klient wyslal dane z prosba o uruchomienie funkcji
Game1Check(param1, param2, param3)
{
   //odnaleziony pokoj w ktorym gra uzytkownik
   myroom.game1.doSomething(param1, param2, param3);
  //odeslij jakies dane do uzytkownika i pozostalich w pokoju
  this.send(/*do uzytkownika i wspolgraczy*/ dane.JSON);
}
język c# ale jak ktoś coś poda w innym języku to nie ma problemu
jak wy byście  to rozwiązali?
mam nadzieję, że zrozumiecie o co mi chodzi :/

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Simplex

  • Użytkownik

# Czerwiec 30, 2013, 23:47:49
Zawsze możesz stworzyć interfejs ala "GameParameters" i dziedziczyć po nim dla każdej gry. W połączeniu z tym, że chciałbyś coś jeszcze robić w zależności od tego co to za gra i parametry, mógłbyś przenieść tę funkcjonalność z klasy gry do "GameParameters" (ale wtedy zmieniłbym już jej nazwę ;)) - wtedy masz rozwiązanie będące de facto wykorzystaniem Strategy pattern. Czy na pewno chcesz tak robić to zależy od kontekstu - czy faktycznie tworzenie jednego interfejsu Game i dziedziczenie po tym to najlepsze rozwiązanie?

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 13:32:24
sprawę rozwiązałem tak:
W pokoju mam obiekt klasy GameBase będący podstawą dla wszystkich gier. Zawiera ona podstawową logikę którą można zastosować dla każdej gry planszowej (pilnowanie kolejności graczy, ile graczy może przystąpić do gry, kogo jest teraz ruch, podstawowy stan gry jak : czy zastopowana, czy gra wystartowała, czy może jest zakończona oraz GameType mówiący o tym jaka gra będzie przechowywana (czy np. szachy, czy może kółko i krzyżyk itd)

Programista chcący napisać własną grę dziedziczy po GameBase i implementuje jej logikę. Dzięki temu otrzymuje sporo podstawowej funkcjonalności i skupia się tylko na tym co potrzebne - zasady gry :) W kodzie zrobienie ruchu w grze wygląda mniej więcej tak:

funkcja zrób ruch:
if(pokoj.gra.gameType == GameType.Chess)
{
  Chess c = (Chess)pokoj.gra;
  //wykonaj jakis ruch zgodnie z z zasadami gry
  c.zrobCos(0,...,n);
}
może komuś się przyda :)

Offline Xion

  • Redaktor
    • xion.log

# Lipiec 23, 2013, 14:12:51
Cytuj
funkcja zrób ruch:
...powinna być wirtualną metodą którą po prostu wywołujesz, a nie switchem po typie gry. Robisz w kodzie to, co kompilator powinien robić za ciebie.

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 14:42:00
nie, nie. Wcale nie switchuje gry. Po prostu to jest takie 'zabezpieczenie', że będziemy rzutowali na poprawny obiekt odpowedniego typu.
W tym obiekcie który otrzymuje programista jest już tylko jeden obiekt GameBase, musimy się tylko upewnić, że można go rzutować na obiekt typu np. Chess, stąd ten "if(pokoj.gra.gameType == ...) {...} tu juz jest koniec funkcji :)"

Offline Xion

  • Redaktor
    • xion.log

# Lipiec 23, 2013, 15:56:18
Ah, czyli to jest kod klienta - tj. klasy implementującej już konkretną grę.

W takim razie jest jeszcze gorzej niż myślałem: zakładasz że twoje API będzie przekazywało twórcom pluginów nieprawidłowe obiekty i jeszcze każesz im się przed tym zabezpieczać?! Mon dieu...

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 17:44:10
Muszę to wytłumaczyć od początku. Załóżmy, że mam klasę "A" która przechowuje podłączonych klientów. Po tej klasie dziedziczy klasa "B" która implementuje zarządzanie klientami podłączonymi do serwera, implementuje pokoje oraz zawiera stworzone przez użytkowników gry (GameBase[] lista_gier). Teraz przyszły programista tworzy klasę "C" która dziedziczy po klasie "B" a gracz który wywoła metodę stworzenia gry w klasie "C" stworzy grę np. szachy i doda ją do lista_gier. i teraz każda gra jest rozszerzeniem GameBase czyli w klasie "C" w jakiejś metodzie robimy base.dodaj_do_listy_gier(new Szachy); W "lista_gier" mamy nowy obiekt, zaś GameBase implementuje coś co wymusza na programiście (przez konstruktor domyślny) zrobienia czegoś takiego:

class InnaGra : GameBase
{
  public InnaGra():GameBase(unikalne_id_gry){}

i teraz dopiero w klasie "C" mamy funkcje
if(base.lista_gier[moja_gra_albo_jestem_w_tym_pokoju_jako_gracz].unikalne_id_gry == unikalne_id_szachow)
{ //jestem bardziej pewien, ze ta gra to szachy
  Chess c = (Chess)pokoj.gra;
  //wykonaj jakis ruch zgodnie z z zasadami gry
  c.zrobCos(0,...,n);
}

przechowując gry w jednym worku nie wiem która jest która a żeby łatwo tworzyło się kolejne gry takie rozwiązanie wydaje mi się idealne, nic innego nie mogłem wymyślić :)
« Ostatnia zmiana: Lipiec 23, 2013, 18:25:33 wysłana przez Puchaczov »

Offline Xion

  • Redaktor
    • xion.log

  • +2
# Lipiec 23, 2013, 19:06:38
what is this I dont even

Okej, po kolei:

0) Czy nazwy twoich faktycznych klas są tajne, że musisz używać A, B i C?
1) Jakie jest właściwie powiązanie między klasami A i B? Czemu nie mogą być jedną klasą? Czemu B nie może zawierać A? Czy są jakieś inne klasy pochodne od A niż B?
2) Co u diabła robi ta klasa C? Dlaczego programista dodający grę musi ją w ogóle tworzyć? Tylko po to żeby wywołać base.Add? Przecież piszesz w C#, masz refleksje, możesz samemu stworzyć obiekt klasy nieznanej w trakcie kompilacji.
3) Czemu "przechowujesz gry w jednym worku" skoro potem i tak musisz je rozróżniać ifami (a nie metodami wirtualnymi). Ba, wymagasz od autorów gier żeby to oni sami je rozróżniali. (Nie będę nawet zaczynał na temat security takiego rozwiązania...)

Reasumując: http://i.imgur.com/bazqF.gif

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 20:23:10
nie no, wcale nie są tajne :). uzupełniając A: Hub, B:GameHub, C:ChessHub. Powiązanie A i B jest takie, że A to jest obiekt który umożliwia mi komunikacje z przeglądarką. Obiekt B dziedziczy po A ale posiada także listę podłączonych klientów oraz stworzonych przez nich pokoi (razem z grą w którą grają). W zamyśle chodziło o to, żeby w klasie C programista mógł dodać jakieś niestandardowe procedury zarządzania np. całym pokojem lub kompletnie porzucić część funkcjonalności pisząc ich własną obsługę (np. ze względu na to, że wymagania gry mogą być bardzo niestandardowe). Przechowywanie gier w jednym worku pozwala mi na to, że ktoś kto będzie później tworzył nową grę, nie będzie musiał edytować klasy B żeby dodać swoją grę bo po prostu wywoła z niej procedure base.dodaj() i doda tą grę a właściwie przechowa obiekt podstawowy tej gry który będzie zawierał unikalny numer, każda klasa gry ma inny (jest do tego enum). Problem z metodami wirtualnymi mam taki, że wymagają one tej samej ilości argumentów co w klasie bazowej. A ja nie wiem ile argumentów będę potrzebował pisząc jakąś grę na przykład za tydzień . Dla szachów potrzebuje 4, dla kółka i krzyżyk np 3 a dla innej może 10? Nie, nie ma innej klasy pochodnej od A, tylko B

Offline Xirdus

  • Redaktor

# Lipiec 23, 2013, 20:37:52
Powiązanie A i B jest takie, że A to jest obiekt który umożliwia mi komunikacje z przeglądarką. Obiekt B dziedziczy po A ale posiada także listę podłączonych klientów oraz stworzonych przez nich pokoi (razem z grą w którą grają).
Na moje oko A jest zbędne. GameHub powinien sam w sobie mieć całą funkcjonalność Hub, bez dodatkowego kroku dziedziczenia (chyba że jest coś o czym nie wiem).

Problem z metodami wirtualnymi mam taki, że wymagają one tej samej ilości argumentów co w klasie bazowej.
Jako ostatni parametr daj tablicę par stringów.


Nadal nie powiedziałeś nam, co robi klasa C.

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 20:46:25
"A" musi pozostać, wywodzi się z frameworka w którym to robię (signalR), "C" w zamyśle ma zawierać procedury obsługi gry. Czyli co odesłać jeżeli mamy remis, co zrobić jeżeli ktoś wygrał, ogólnie przyjmowanie danych od grających klientów (ale tylko w 1 grę, dla innej gry inny klasa (np. "D") dziedziczy po "B" ) i przekazywanie ich do klasy gry i w odpowiedzi na to co klasa gry odpowie, wywołane zostają procedury odsyłające takie bądź inne info
« Ostatnia zmiana: Lipiec 23, 2013, 20:55:52 wysłana przez Puchaczov »

Offline Xirdus

  • Redaktor

# Lipiec 23, 2013, 20:56:16
No to w takim razie ja bym zlikwidował C, a komunikację klient-serwer zostawił samej klasie gry. W sensie możesz mieć jakiś generyczny sposób przesyłu danych wspólny dla wszystkich gier, ale interpretacja komunikatu to już rzecz specyficzna dla każdej z gier, więc logiczne żeby to klasa gry go obsługiwała, od początku do końca.

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 21:09:57
właściwie to zapomniałem o jednej ale istotnej rzeczy którą robi "C". Otóż ładuje ono odpowiednie skrypty JS w zależności od gry. Czyli ChessHub ładuje skrypty gry szachów, TickTackToeHub ładuje skrypty dla gry kółko i krzyżyk itd. Ten if w funkcji zrób ruch ma taką funkcję, że użytkownik który próbuje wysłać dane do pokoju używa jego publicznego identyfikatora. Pokój zostaje odnaleziony i posiada on jakąś grę. Teraz jeżeli ktoś byłby sprytny i chciałby wysłać inny numer id, bez tego ifa mógłbym rzutować np zamiast na TickTackToeGame to na ChessGame. To byłoby niebezpiecznie w związku z czym robię tak, że ta klasa GameBase ma enuma i programista musi nadać klasie to "enum id" dzięki czemu niemożliwym staje się rzutowanie na zły obiekt. Niestety poważną wadą tego rozwiązania jest to, że zakładam iż programista będzie na tyle bystry i sprawdzi to czy gra w pokoju ma ten sam id co ta gra która zarządza ten konkretny Hub. Teraz sobie pomyślałem, że mógłbym to ukryć przed programistą przenosząc do klasy "B" tę funkcjonalność dzięki czemu ktoś nie będzie musiał już się tym martwić.

I jeszcze jedna istotna informacja. Użytkownik wchodząc na stronę i wybierając grę "szachy" wywołu "ChessHub", jeżeli chce kółko i krzyżyk wywołuje "TickTackToeHub".

i jeszcze jedno:

@Xion: dzięki za kotka, poprawiłeś mi humor ^^
« Ostatnia zmiana: Lipiec 23, 2013, 21:27:12 wysłana przez Puchaczov »

Offline Xirdus

  • Redaktor

# Lipiec 23, 2013, 21:55:36
właściwie to zapomniałem o jednej ale istotnej rzeczy którą robi "C". Otóż ładuje ono odpowiednie skrypty JS w zależności od gry. Czyli ChessHub ładuje skrypty gry szachów, TickTackToeHub ładuje skrypty dla gry kółko i krzyżyk itd.
Ale czekaj, to ty robisz grę webową czy jak? Bo już się pogubiłem. Te skrypty to odpowiadają za UI, czy za sam gameplay, czy co? Bo jak gameplay to czy nie powinny być częścią klasy gry, czy coś w ten deseń?

Ten if w funkcji zrób ruch ma taką funkcję, że użytkownik który próbuje wysłać dane do pokoju używa jego publicznego identyfikatora. Pokój zostaje odnaleziony i posiada on jakąś grę. Teraz jeżeli ktoś byłby sprytny i chciałby wysłać inny numer id, bez tego ifa mógłbym rzutować np zamiast na TickTackToeGame to na ChessGame.
Chyba czegoś nie rozumiem. Z tego co widzę, to dowolna osoba połączona z serwerem może zrobić ruch w dowolnej grze, w dowolnym pokoju, jako dowolny gracz. Coś mi tu ostro śmierdzi.

Offline Puchaczov

  • Użytkownik

# Lipiec 23, 2013, 22:09:54
Ale czekaj, to ty robisz grę webową czy jak? Bo już się pogubiłem. Te skrypty to odpowiadają za UI, czy za sam gameplay, czy co? Bo jak gameplay to czy nie powinny być częścią klasy gry, czy coś w ten deseń?
Chyba czegoś nie rozumiem. Z tego co widzę, to dowolna osoba połączona z serwerem może zrobić ruch w dowolnej grze, w dowolnym pokoju, jako dowolny gracz. Coś mi tu ostro śmierdzi.

oczywiście, że nie. Żeby ktoś mógł zrobić w ogóle ruch w pokoju to: musi w nim być zapisany jako gracz, musi być jego kolej, ogólnie musi mieć uprawnienia do grania w tym pokoju. Ale potencjalny gracz może mieć otwartych wiele różnych pokoi z różnymi grami w których może grać. Dlatego mógłby przycwaniaczyć i spróbować wysłać id pokoju w którym jest i może grać a w którym jest inna gra i wtedy doszłoby do próby błędnego rzutowania. Przed taką ewentualnością musiałem się zabezpieczyć

Tak, będą to gry webowe, Te skrypty odpowiadają za gameplay (są dynamicznie wstrzykiwane do przeglądarki gdy użytkownik będzie chciał wejść do pokoju)