Piszę grę planszową 2D w C++ obiektowo. Jaki wzorzec architektoniczny wybrać?
Wstępne założenia
- Można podpiąć dowolną warstwę prezentacji, np. WinAPI, GDI, Qt.
- Łatwa konwersja na inną platformę, np. Linux.
- 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?
- Czy warstwa logiki ma operować bezpośrednio na obiektach okna?
- Jak interfejs ma komunikować się z warstwą logiki i pobierać dane?
Przykładowe problemy
- Kiedy gracz wykonuje ruch, warstwa logiki musi się o tym dowiedzieć i wykonać obliczenia.
- Kiedy komputer wykonuje ruch, pionki na planszy muszą się przestawić.
- Zatem jest potrzebna komunikacja obustronna.
Przykłady komunikacji
- 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
- 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()) {}
- 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 */
}
- 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:
- Jak odczytać położenie pionków z klasy bez obciążenia procesora?
- 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ć?