Wzorzec projektowy dla gry planszowej

0

Piszę grę planszową 2D w C++ obiektowo. Jaki wzorzec architektoniczny wybrać?

Wstępne założenia

  1. Można podpiąć dowolną warstwę prezentacji, np. WinAPI, GDI, Qt.
  2. Łatwa konwersja na inną platformę, np. Linux.
  3. Obiekty nie wchodzą sobie w kompetencje - wykonują tylko swoje zadania.

Zacząłem pisać grę w WinAPI i rysować planszę za pomocą GDI. Prawdopodobnie zmienię na MFC, bo pisanie z palca całego interfejsu i innych okien z komponentami jest czasochłonne. Z drugiej strony tworząc coś w MFC można się zamęczyć. Biblioteki wykorzystujące cały czas 100% czasu procesora odpadają.

Założenie drugie to niedyskryminowanie innych platform niż Windows. I tu się zaczyna. Dylemat jest następujący - pisać spójną aplikację czy z podziałem na warstwy?

  1. Czy warstwa logiki ma operować bezpośrednio na obiektach okna?
  2. Jak interfejs ma komunikować się z warstwą logiki i pobierać dane?

Przykładowe problemy

  1. Kiedy gracz wykonuje ruch, warstwa logiki musi się o tym dowiedzieć i wykonać obliczenia.
  2. Kiedy komputer wykonuje ruch, pionki na planszy muszą się przestawić.
  3. Zatem jest potrzebna komunikacja obustronna.

Przykłady komunikacji

  1. Fasady - każda warstwa ma fasadę, czyli specjalny interfejs, przez który poszczególne warstwy mogą komunikować się. Zarówno model i widok mają funkcje, które mogą wywoływać nawzajem (może ich być dużo).
model.przesun(pionek, pozycja); //widok każe modelowi wykonać akcję
widok.koniecGry(); //model informuje widok o końcu gry
  1. MVC - komunikacja model-widok jest jednostronna. Widok odwołuje się do modelu i nie na odwrót. A co wtedy, gdy model musi wykonać akcję w widoku, np. przesunąć pionek na planszy lub powiadomić, że gra się skończyła? Można zastosować wzorzec Obserwator. Albo inaczej - widok nadzoruje model.
if(model.przesun(pionek, pozycja))
{
   this.przesun(pionek, pozycja); //wewnątrz widoku
}
while(akcja = model.getAction()) {} //można tak
if(model.zaznacz(pionek))
  if(model.pokazDozwolone(...)) {} //albo tak
if(model.wykonajRuchy()) {}
  1. Zdarzenia - można wysłać zdarzenie do okna, ale wtedy uzależnimy logikę od widoku. Innym sposobem jest utworzenie metod dla zdarzeń w obu warstwach albo wykorzystanie wzorca Obserwator.. Zdarzenia trzeba nazwać i obsłużyć instrukcją switch.
switch(zdarzenie)
{
   case EVENT_FINISH: this.finish(); break; 
   default: /* błąd */
}
  1. Wolne obiekty - nie ma ścisłego podziału na warstwy. Poszczególne obiekty mogą odwoływać się do publicznych metod i własności innych obiektów. Wyjątkiem są obiekty, które z założenia mogą być obsługiwane tylko przez 1 klasę lub metodę. To jednak nie rozwiązuje problemów z implementacją!

Jeszcze jedna rzecz
Zdarzenie WM_PAINT odrysowuje okno. Trzeba narysować aktualny stan planszy, pionki, a przy tym użyć właściwych kolorów lub bitmap. Kiedy zaznaczamy pionka, też trzeba to uwidocznić np. innym obramowaniem pola. Podobnie ostatni ruch oraz podpowiedzi, gdzie wolno przesunąć pionek. Pojawiają się pytania:

  1. Jak odczytać położenie pionków z klasy bez obciążenia procesora?
  2. Jaka warstwa ma przechowywać informacje o kolorach pól i graczy?

Ad 1. Aktualnie w klasie Gra mam własność int pola[10][10]. Powiecie, że powinna ona być prywatna. W końcu implementacja może się zmienić i zamiast tablicy liczb całkowitych będzie tablica obiektów Pole pola[10][10] bądź zostanie przeniesiona do innej klasy, cokolwiek. Z drugiej strony gdyby co chwilę wywoływać metody getCzyjePole(), getKolorPola(), getCosTam() - za duże obciążenie dla procesora.

for(x=0; x<10; x++)
   for(y=0; y<10; y++)
      switch(model.getCzyjePole(x, y)) {...}

A skąd wiem, ile jest pól? Że plansza jest kwadratowa? Jak zmienimy planszę na okrągłą, nic nie da zamiana klasy w modelu z PlanszaKwadrat na PlanszaKolo, bo trzeba wymienić cały widok. To tylko przykład, aby pokazać paradoks MVC. Nie da się całkowicie wyeliminować logiki (biznesu) programu z widoku.

Ad 2. Takie rzeczy zwykle podajemy w ustawieniach. A ich nie powierzymy widokowi. Równie dobrze widokiem może być tryb tekstowy bez kolorów. 2 pliki ustawień (drugi dla widoku, jak ma wyglądać plansza)? No way!

Pole może należeć do gracza lub być puste; być zaznaczone lub nie; być oznaczone jako ostatnie przesunięcie lub nie; jako podpowiedź lub nie - zatem gdzieś trzeba trzymać stany pól. Model lub widok. Bitwise operators? Obiekt Pole, a tam własności to opisujące? Hm? Sugestie?

Chyba wszystko już wyjaśniłem. Mam mało czasu na napisanie gry, chcę to zrobić dobrze, piszę głównie dla Windowsa. Jak poszczególne obiekty powinny się komunikować?

1

Dylemat jest następujący - pisać spójną aplikację czy z podziałem na warstwy?
Co to znaczy „spójną”? Spójna to taka, w której są podziały na warstwy. Inaczej dostajesz spagetti.

Mniejsza o wzorce, bo nie jestem ich wyznawcą. Ale kod powinien być taki, że bierzesz moduł logiki, i bez zmian możesz go zastosować w zupełnie innym GUI, czy nawet programie bez GUI (konsolowym czy serwerowym).

A co wtedy, gdy model musi wykonać akcję w widoku, np. przesunąć pionek na planszy lub powiadomić
funkcja logiki zwraca ruch jako rezultat funkcji, albo wywołuje zdarzenie GUI (callback).

Widok odwołuje się do modelu i nie na odwrót.
Może na odwrót, zgodnie z
Poszczególne obiekty mogą odwoływać się do publicznych metod i własności innych obiektów.
ale pod warunkiem, że interfejsy z obu stron będą ściśle przez ciebie określone i niezależne od platformy ani biblioteki GUI.

Tu znowu widzę że trzymanie się wzorców jest ograniczające. W każdym przytoczonym przez ciebie widzisz wady, i to że nie do końca odpowiadają temu, co chcesz osiągnąć.
Podział na warstwy jest dobry. Ale ścisłe trzymanie się zasad wzorcowych typu Takie rzeczy zwykle podajemy w ustawieniach. A ich nie powierzymy widokowi powoduje, że się blokujesz, bo nie ma wzorca „moja gra planszowa”.

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