Projektowanie menu przy użyciu MVC

0

Cześć. Pisząc moją grę snake 3d, z ilością kodu zauważyłem że jest mi coraz ciężej programować, mimo że żadne z klas/ funkcji które implementowałem nie były trudne. Poszperałem trochę na ten temat i dowiedziałem się że powinienem nauczyć się jak używać MVC, czyli Model View Controller pattern. Polega on na podzieleniu kodu na 3 częśći, model, czyli same obiekty które "wiedzą" tylko o sobie, view czyli funkcje renderowania, i controller, czyli funkcje obslugi wejscia i do dokonywania odpowiednich zmian na modelach.

No i teraz próbuję przy użyciu tego napisać menu, które składa się z guzików rysowanych na ekranie. Niektóre guziki mogą mieć pod-menu(ale to zostawiam na później) które po kliknięciu na guzik renderowane są jako nowe menu w miejscu starego.Podzieliłem mój projekt tak:

Model:
Klasa Button która zawiera buttonWidth, buttonHeight, buttonLabel, point i kilka getterów i setterów.
Klasa Menu która składa się z tablicy Button'ów i currentButton(żeby wiedzieć który podświetlić).

View:
Renderuje menu

Control:
funkcje które zmieniają currentButton w klasie menu jeśli strzałka w góre lub w dół została naciśnięta.

Ale teraz, czy myślicie że mój design jest dobry? Osobiście uważam że nie było by mi tak trudno się domyśleć jak zaimplementować pod-menu gdyby to było poprawnie zaprojektowane.

1

Chyba nie do końca zrozumiałeś o co chodzi w MVC.
Model to są obiekty które określają model logiczny aplikacji. Na przykład jak piszesz grę w szachy, to obiekty modelu zajmują się logiką gry - trzymaja ustawienia pionków, określaja legalność ruchów itd
View to są obiekty które na podstawie Modelu są w stanie malować interfejs - zwykle są podpięte do modelu observerem i jak model się zmieni (np. ktoś ruszy pionkiem) to się przerysowują
Controller to są obiekty które slużą do zmian modelu. Nie wolno zmieniać modelu ot tak, manipulując bezpośrednio jego obiektami, bez przejscia przez kontroler. Kontroler zwykle jest podpięty np. do guzików w GUI.

0
Shalom napisał(a):

Chyba nie do końca zrozumiałeś o co chodzi w MVC.
Model to są obiekty które określają model logiczny aplikacji. Na przykład jak piszesz grę w szachy, to obiekty modelu zajmują się logiką gry - trzymaja ustawienia pionków, określaja legalność ruchów itd
View to są obiekty które na podstawie Modelu są w stanie malować interfejs - zwykle są podpięte do modelu observerem i jak model się zmieni (np. ktoś ruszy pionkiem) to się przerysowują
Controller to są obiekty które slużą do zmian modelu. Nie wolno zmieniać modelu ot tak, manipulując bezpośrednio jego obiektami, bez przejscia przez kontroler. Kontroler zwykle jest podpięty np. do guzików w GUI.

Okej, ale wydaję mi się że tu właśnie tak jest, czyli wszystko trzyma informacje o sobie samym w modelach i potem view rysuję a controller modyfikuje obiekty. Czy maż na myśli żeby np. guziki nie wiedziały o swoich rozmiarach, labelach i tak dalej?

Właśnie pod czas pisania tego posta wpadłem na pomysł. Czy chodziło ci o usunięcie informacji o położeniu guzika z klasy Button? Wtedy nic bym tam nie miał, ale w takim razie dodałbym stan kliknięty/nie kliknięty, dzięki czemu łatwo będę mógł rysować podmenu. Ale w takim razie informacje o rysowaniu guzików muszą być zakodowane w view, czy tak ma być?

1

@jammasterz zasadniczo chodzi o to żeby te nieszczęsne guziki nie wiedziały w ogóle o tym że mogą być rysowane i w jaki sposób. Chodzi o to żeby totalnie oderwać model od sposobu jego prezentacji. Dzięki temu możesz podpiąć dwa zupełnie niezależne View na przykład :) Elementy Modelu interesuje tylko logika biznesowa.

0

Okej. Po napisaniu wersji początkowej jestem bardzo zadowolony z wyglądu kodu. Użyłem struktury z pewnego tutoriala i oto rezultat.
W klasie głównej stworzyłem tylko nową instancję TitleEngine i w pętli gry nawołują tylko:

engine.render();
engine.poll(); 

Mam nadzieję że nie będzie problemu z javą, jest właściwie taka sama jak c++.

package model;

public class Button {
	private boolean clicked;
	
	public Button(){
		clicked = false;
	}
	public void setClicked(boolean arg){
		this.clicked = arg;
	}
	public boolean getClicked(){
		return this.clicked;
	}
}
 
package model;

import java.util.ArrayList;
import java.util.List;

public class Menu {
	private List<Button> buttons = new ArrayList<Button>();
	private final int numberOfButtons;
	private int currentButton;
	
	public Menu(int noButtons, int currentButton){
		this.numberOfButtons = noButtons;
		this.currentButton = currentButton;
		for(int i = 0; i < numberOfButtons;i++){
			buttons.add(new Button());
		}
	}
	public int getCurButton(){
		return this.currentButton;
	}
	public void setCurButton(int button){
		this.currentButton = button;
	}
	public Button getButton(int button){
		return buttons.get(button);
	}
	public int getNoButtons(){
		return this.numberOfButtons;
	}
}
 
package control;

import model.Menu;

public class TitleController {
	private Menu menu;
	
	public TitleController(Menu menu){
		this.menu = menu;
	}
	
	public void onDown(){
		if (menu.getCurButton() == menu.getNoButtons()-1)
			menu.setCurButton(0);
		else
			menu.setCurButton(menu.getCurButton() + 1);
	}
	public void onUp(){
		if (menu.getCurButton() == 0)
			menu.setCurButton(menu.getNoButtons() - 1);
		else
			menu.setCurButton(menu.getCurButton() - 1);
	}
	public void onEnter(){
		menu.getButton(menu.getCurButton()).setClicked(true);
	}
}
 
package view;

public interface Renderer {
	public void render();
}
 
package view;

import java.awt.Point;

import org.lwjgl.opengl.GL11;

import model.Menu;

public class TitleScreenRenderer implements Renderer{
	private final int width = 800, height = 600;
	private final int buttonSpacing = 10;
	private final int buttonHeight = 74;
	private final int buttonWidth = 216;
	private final int buttonBorder = 5;
	private final Point point = new Point(width - buttonSpacing - buttonWidth + 2, height - buttonSpacing);
	private Menu menu;
	
	public TitleScreenRenderer(Menu menu){
		this.menu = menu;
	}
	@Override
	public void render() {
		for(int i = 0;i<menu.getNoButtons();i++){
			//the quad
			GL11.glColor3f(0.5f, 0f, 0.5f);
			if(menu.getCurButton() == i)
				GL11.glColor3f(0.7f, 0f, 0.45f);
			GL11.glBegin(GL11.GL_QUADS);
				GL11.glVertex2f(point.x, point.y - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x + buttonWidth, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x + buttonWidth, point.y - (i * (buttonHeight + buttonSpacing)));
			GL11.glEnd();
			//button border
			GL11.glLineWidth(buttonBorder);
			GL11.glColor3f(0.3f, 0, 0.45f);
			GL11.glBegin(GL11.GL_LINES);
				//upper
				GL11.glVertex2f(point.x, point.y - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x + buttonWidth, point.y - (i * (buttonHeight + buttonSpacing)));
				//down
				GL11.glVertex2f(point.x, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x + buttonWidth, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
				//right
				GL11.glVertex2f(point.x, point.y - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
				//left
				GL11.glVertex2f(point.x + buttonWidth, point.y - (i * (buttonHeight + buttonSpacing)));
				GL11.glVertex2f(point.x + buttonWidth, point.y - buttonHeight - (i * (buttonHeight + buttonSpacing)));
			GL11.glEnd();
		}
	}
}
 
package control;

import org.lwjgl.input.Keyboard;
import model.Menu;
import view.Renderer;
import view.TitleScreenRenderer;

public class TitleEngine {
	private TitleController controller;
	private Renderer renderer;
	private Menu menu;
	
	public TitleEngine(){
		this.menu = new Menu(6,0);
		this.controller = new TitleController(menu);
		this.renderer = new TitleScreenRenderer(menu);
	}
	public void poll(){
		if (Keyboard.next()&& !Keyboard.getEventKeyState()){
			if (Keyboard.getEventKey() == Keyboard.KEY_UP)
				controller.onUp();
			if (Keyboard.getEventKey() == Keyboard.KEY_DOWN)
				controller.onDown();
			if (Keyboard.getEventKey() == Keyboard.KEY_RETURN || Keyboard.getEventKey() == Keyboard.KEY_SPACE)
				controller.onEnter();
		}
	}
	public void render(){
		renderer.render();
	}
}
 

Dla tych którzy nie znają javy- w konstruktorach niektórych klas inicjuję swoje klasy klasami z argumentów. Nie znając javy zapewnie pomyślicie że pola w klasie będą tylko kopią tych w argumentach. Ale tak na prawdę w argumentach nie podaję kopii klasy tylko wskaźnik. Tak FYI.

Dla ciekawskich i leniwych to wynik:
http://imageshack.us/photo/my-images/255/menuready.jpg/

Czy ten kod jest "dobry" czy jest w nim jeszcze pare niedociągnięć(chodzi mi tu tylko o strukturę MVC)?

0

Na pierwszy rzut oka wygląda ok ;)

0

Okej i natknąłem się na pierwszy problem. Powiedzmy że jeśli kliknę Enter na pierwszym guziku, guzki mają sie powoli przesunąć w lewo, a w czasie kiedy się przesuwają z lewej wyłania się pod menu guzika który kliknąłem. Najlepszym sposobem bylło by po prostu zmneijszenie pozycji menu w funkcji update() którą wywołałbym przed engine render. Ale nie mam co updatować, bo controller powinien updatować tylko modele a dane dotyczące pozycji modeli na akrenie są zakodowane na twardo w render(). Czy aby na pewno pozycje nie mogą być w modelach?

2

Przy takich bardziej skomplikowanych rzeczach lepiej sprawdza sie MVP - Model View Presenter. Jest to dość podobna koncepcja do MVC, ale zakłada inny model komunikacji.
W MVC mamy komunikację:

  • View wyświetla model i go obserwuje
  • Controller zmienia Model
    W MVP mamy komunikację:
  • Prezenter zmienia Model i triggeruje zmiany View
  • View nie obserwuje Modelu tylko czeka na informacje od Prezentera
    W twoim przypadku powinno to rozwiązać problem.
0

Okej poszperałem trochę na internecie, ale niestety dowiedziałem się tylko mniej więcej o co chodzi z MVP(właściwie z MVC też jest trochę mglisto, ale przyjdzie mi to z praktyką).

Tak więc. W moim kodzie użyłem struktury MVC, czyli w main loopie wywołuję 3 metody, update(), która updatuje modele(controller), render() która je renderuje(view) i poll() która zbiera dane od użytkownika. Jeśli dobrze zrozumiałem chcąc teraz zmienić ten kod na MVP, musiałbym wymienić kontrolera na prezentera po czym w pętli głównej wywoływać tylko jego metodę która manipuluje modelami, zbiera dane od użytkownika i renderuje. Jeśli źle to zrozumiałem to prawdopodobnie dlatego że nie za bardzo rozumiem w jaki sposób interfejsy(nigdy nie używałem, oprócz w tym przykładzie, tylko dlatego że mi powiedziano że to mądre rozwiązanie) mogłyby umożliwić to co chcę osiągnąć. Czytałem trochę o interfejsach, ale wciąż nie rozumiem w jaki sposób mogą tu pomóc.

0

Tak jak pisałem wyżej, nie musisz używać tutaj MVP - może tak zrobić że jak dane się zmienią to wtedy View sobie odpala tą "animację". Ale możesz też potrenować sobie MVP :P
Ogólnie teraz w swoim MVC powinieneś mieć coś takiego:

  • User ma dostęp tylko (!) do Controllera i właściwie tylko interakcja z kontrolerem występuje w "glównej pętli".
  • Controller może zmienić coś w Modelu (i tylko w Modelu)
  • View rejestruje swojego Listenera w Modelu i czeka na eventy które będą przekazane do tego listenera i jeśli jakieś się pojawią to modyfikuje wygląd i go odrysowuje
  • View jest zarejestrowane jako Observer w Modelu, tzn jeśli model się zmieni to przekazuje zarejestrowanym Observerom/Listenerom informacje że coś się zmieniło

W przypadku MVP różnica jest tylko taka że:

  • View nie potrzebuje Obserwować Modelu i tego nie robi
  • Controller zostaje rozszerzony tak że ma teraz dostęp do View, więc staje się Prezenterem
  • Prezenter po zmianie Modelu wysyła też informacje o tej zmianie do View
0

Tak sobie ostatnio doczytywałem o interfejsach i pomyślałem że zrobię małe odstąpienie od MVC. Gdzieś mi powiedziano że użycie interfejsu do renderowania to dobry pomysł i teraz chyba wiem dlaczego. Jako że będę miał kilka ekranów, takich jak np. highscores, edytor poziomów, credits i tak dalej, postanowiłem że napiszę osobny renderer dla każdego z ekranów i po prostu kiedy w menu coś wybiorę, zmienię na odpowiedni renderer. Renderery będą niestety(tutaj odstąpienie) wczytywały pozycje obiektów(jeśli takie są) z modeli(view przecież tylko czyta, więc nie jest aż tak źle), a kontroler je zmieniał. Dzięki temu po prostu zmieniam pozycje w modelu o kilka pixeli, a przez to że renderowanie wywoływane jest kilkadziesiąt razy na sekundę, dostanę moją animację. Szczerze mówiąc nie rozumiem dlaczego modele nie mogą mieć swoich pozycji(chociaż trochę tak, ale gdzie indziej schować pozycje żeby nie wkuwać ich na twardo do jednej metody i to do tego renderowania).

1

Czemu? Bo właśnie nierozerwalnie powiązałeś model i view i złamałes zasadę jednej odpowiedzialności. Normalnie model powinien interesować się tylko tym jakie zadania ma wykonywać a nie tym jak należałoby go rysować. Co więcej o ile wcześniej mogłes podpiąć różne moduły do wizualizacji które mogłyby wizualizować model na różne sposoby, teraz sobie to ograniczyłeś.
Nie bardzo rozumiem czemu View nie może u ciebie przechowywać tej pozycji. Przecież możesz tak zrobić że jak wykryjesz zmiane w modelu to wtedy uruchamiasz sobie modyfikowanie pozycji.

0
Shalom napisał(a):

Czemu? Bo właśnie nierozerwalnie powiązałeś model i view i złamałes zasadę jednej odpowiedzialności. Normalnie model powinien interesować się tylko tym jakie zadania ma wykonywać a nie tym jak należałoby go rysować. Co więcej o ile wcześniej mogłes podpiąć różne moduły do wizualizacji które mogłyby wizualizować model na różne sposoby, teraz sobie to ograniczyłeś.
Nie bardzo rozumiem czemu View nie może u ciebie przechowywać tej pozycji. Przecież możesz tak zrobić że jak wykryjesz zmiane w modelu to wtedy uruchamiasz sobie modyfikowanie pozycji.

No dobrze. Moje menu jest rysowane zaczynając od jednego punktu który określa jego lewy górny róg. Jeśli spojrzysz na mój kod, nie ma żadnej możliwości zmienienia tego punktu, żeby menu zostało wyrenderowane w innym miejscu. Okej, mógłbym napisać nowy renderer który renderuje je po lewej stronie, ale to tyle. Jeśli chcę mieć miękką animacje przesuwania się menu w lewo, najłatwiej by było w metodzie update() zmieniać tą pozycje o np. 1 pixel za każdym wywołaniem dopóki menu nie dotrze do swojej pozycji, a view będzie po prostu renderować model( nie obserwuję modelu, tylko renderuje w głównej pętli cały czas, 60 razy na sekundę, nawet jeśli nie było zmiany. To będzie baza gry, więc nie chciało mi się bardziej tego komplikować). Jedyna rzecz jaką moja metoda render() pobiera to jaki guzik jest aktualnie zaznaczony. U mnie zmiany w modelu to tylko mała część pracy, a skoro nie mogę dodać pozycji obiektu do modelu to nie mam możliwości jej zmienienia. Oczywiście mógłbym napisać specjalny renderer który sam zmienia pozycję menu za każdym wywołaniem, a potem je renderuje, ale wtedy nie jest tylko rendererem, bo robi coś więcej. Do tego takie rozwiązanie jest brzydkie. Czytałem trochę o MVP, ale tam też nie widzę żadnej możliwości zmiany tych punktów.

0

Wie ktoś w jaki sposób to rozwiązać? Poczytam jeszcze trochę o MVP, ale nie wiem czy coś znajdę, mało jest informacji a ogólne tłumaczenia nie za bardzo mi pomagają.

0

No i właśnie o to mi chodzi. Znalazłem już 20 takich artykułów. Bardzo przydatne, ale nie za bardzo tłumaczą jak to zrobić. Są strzałki itd. ale nie mam pojęcia jak się za to zabrać. Mój kod MVC napisałem po przeczytaniu kilku przykładów, a dla MVP znalazłem tylko jeden przykład który był bardzo skomplikowany. Jeśli ma ktoś czas to na prawdę byłbym wdzięczny jeśli by mi opisał jaki jest najłatwiejszy sposób uzyskania tego efektu(chodzi mi o opisanie kodu, nie proszę o napisanie).

0

Powiem Ci, że ja nie mam pojęcia jak zrobić menu przy użyciu MVC/MVP. W ogóle nawet nie wpadłbym na to, żeby coś takiego robić.
Spróbuj może użyć tych wzorców w jakimś konkretniejszym, bardziej życiowym przykładzie, bo ten jest taki... naciągany.

0

Na samym pocztku chcialem zeby to bya gra 3d. Ale zaczynam od menu dlatego ze nie moge isc dalej bez wyboru poziomow. Z gra bedzie dokladnie to samo- waz bedzie mial swoja pozycje, plansza bedzie mialo swoja rotacje. Skoro nie bedzie mozna ich umiescic w modelach na tej samej zasadzie co nie mozna umiescic pozycji menu w jego modelu, natkne sie na ten sam problem- czyli pozycje zmienne takie jak pozycja weza itd. , ktore nie wiadomo gdzie mam umiescic.

0

@jammasterz^ czym innym jest "pozycja węża" która wpływa na logikę gry (np. na to że zaraz zje coś) a czym innym jest pozycja węża w sensie tego gdzie jest namalowany na ekranie...

0
Shalom napisał(a):

@jammasterz^ czym innym jest "pozycja węża" która wpływa na logikę gry (np. na to że zaraz zje coś) a czym innym jest pozycja węża w sensie tego gdzie jest namalowany na ekranie...

No tak ale w taki czy inny sposob musze te liczby jakos do tej metody przekazac. Chyba z tego zrezygnuje. I tak moj kod jest o wiele czystszy niz wczesniej, a ze nie planuje byc developerem gier, tyle mi wystarczy.

Dzieki wszystkim za pomoc.

EDIT: Nie no nie moge tego tak zostawic :D. W takim razie w jaki sposob gry renderuja np. Potwory? Ich pozycja musi byc updatowana, nie moze byc przeciez stala w metodzie render().
To chyba nie gra roli czy pozycja czegos sluzy do logiki gry czy nie, i tak metody renderowania musza sie o tym dowiedziec. Czy aby na pewno MVC i inne pokrewny patterny sa dobrym wyborem jesli codzi o pisanie gier?

0

@jammasterz o_O ty jesteś chyba trochę niepoważny. Przecież View może obliczać sobie pozycję potwora na bazie jego pozycji "logicznej" oraz np. wymiarów planszy które należą do ustawień View.
Prosty przykład:

  • Pionek na szachownicy wie tylko że stoi na polu XY i absolutnie nie powinien wiedzieć że powinien być narysowany w punkcie 100,200 na ekranie, bo to przecież zależy od tego jak będzie wizualizowany, a Pionek w ogóle (!) nie powinien wiedzieć jak będzie wizualizowany.
  • View jest parametryzowane rozmiarem szachownicy i potrafi sobie policzyć gdzie w zależności od rozmiaru szachownicy (bo user moze sobie resizowac okno) będzie pole XY i stąd tez View potrafi tego pionka namalować tam gdzie powinien być namalowany
    W ten sposób cała logika gry jest oderwana od tego jak to będzie wizualizowane. Może być po prostu wypisywane w konsoli, może być okienkowe, może być 2d, może być 3d, może być webowe, to nie ma znaczenia.
0

@jammasterz musisz pamiętać, że ludzie mają różne rozdzielczości monitorów i nie powinno się "na sztywno" zapisywać współrzędnych przycisków z menu. Lepiej jest ich ilość i stan zapisać w warstwie Modelu, a to, jak będą wyświetlane, na jakich pozycjach XY - zapisać w warstwie Widoku (która to sobie wyliczy na podstawie np. aktualnej rozdzielczości). Jeżeli będziesz chciał w przyszłości napisać coś w stylu "10 nowych skórek" do tej gry, to będziesz zmieniał Widok, a nie wpychał wszystkie współrzędne do Modelu. Łatwiej to będzie ogarnąć po kilku miesiącach od napisania kodu.

0

Współrzędne guzików właśnie obliczam w rendererze, na podstawie wymiarów okna.

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