W drugim odcinku serii o Footrzastej stworzymy podwaliny pod dalszą pracę — będziemy rozróżniać pomiędzy ‚poziomami’ oraz ‚menu’, a także skonfigurujemy Springa tak, aby wygodnie nam się pracowało dalej.
Póki co to, co wyświetlimy będzie bardziej przypominało powrót do złotych lat 90’tych, ale będziemy na tym budować dalej :)
Spring IO
Spring IO w swojej podstawowej wersji to przede wszystkim zarządzanie wersjami zależności — wersja platformy określa wersje dla wielu popularnych bibliotek i narzędzi, które można importować do projektu. Dzięki temu nasze pom’y są wolne od numerów wersji (o ile oczywiście dana biblioteka jest zarządzana przez Spring IO), a aktualizacja wersji sprowadza się do zaktualizowania numeru wersji Spring IO (oczywiście być może będziemy musieli dostosować nasz kod — mamy natomiast pewność, że biblioteki i ich wersje są ze sobą zgodne i współpracują bez problemu).
Korzystamy ze Spring IO w naszym projekcie
Zaczynamy od utworzenia nowego projekt (tutaj przeczytasz, jak można to zrobić w Eclipse) i w głównym pom.xml dodajemy poniższy fragment:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>2.0.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
To wystarczy, abyśmy mogli korzystać w plikach pom.xml z dependency bez podawania numerów wersji (odbywa się to za pomocą mechanizmu Mavena o nazwie dependency management — projekt, który pobieramy to tak naprawdę <a>długi pom.xml</a> z wieloma tagami <dependencyManagement> określającymi prawidłową wersję okreslonej biblioteki; pobierając jedną z nich jako <dependency> w naszym projekcie, jeśli nie podamy wersji wykorzystywana jest ta zdefiniowana tutaj). Utworzymy sobie pięć modułów w naszym projekcie:
- game-model — tutaj będzie model naszej aplikacji, wszystkie interfejsy i klasy, których będziemy używać
- game-resources — moduł ten będzie nam służył jedynie do przechowywania zasobów gry — grafik, plików dźwiękowych itp
- game-runtime — tutaj trafią implementacje wszystkich interfejsów, serwisy, konfiguracja Spring’a, główna klasa służąca do uruchamiania itp
- game-views — w tym module umieścimy wszystkie widoki, czyli zarówno plansze jak i menu
- game-persistence — traffic tutor kid odpowiedzialny za utrwalanie obiektów, zapis stanu gry itp
Taki podział ma charakter głównie porządkowy i jest on dość arbitralny — w przypadku aplikacji desktopowych ‚konwencje’ związane z tym jak dzielić projekt na moduły nie są tak wykrystalizowane i naturalne jak w przypadku aplikacji webowych, dodatkowo nasza gra nie jest też standardową aplikacją CRUDową. Można oczywiście umieścić wszystko w jednym, głównym module Mavenowym — my preferujemy jednak podział na mniejsze podmoduły.
W tym miejscu mała dygresja — dość częst dostajemy pytania ‚dlaczego zostało to podzielone na takie moduły a nie inne’, lub ‚czy można to podzielić tak i tak’. Odpowiedź najczęściej brzmi: tak, można to podzielić inaczej, a zastosowany przez nas podział okazał się praktyczny i sprawdził się w innych projektach. To nie jest wiedza magiczna, czy jakiś standard — jeśli zastosowany przez nas podział wydaje Ci się nielogiczny lub zbyt drobiazgowy, nic nie stoi na przeszkodzie abyś zastosowała własny, który będzie dla Ciebie bardziej intuicyjny. Jest to jedna z tym rzeczy, kiedy większość rozwiązań nie jest ‚dobra’ czy ‚zła’, a najważniejszym kryterium powinno być dopasowanie do preferencji osób pracujących z projektem.
Poniżej przedstawiamy diagram naszych modułów oraz zależności pomiędzy nimi dla zobrazowania jak zorganizowana będzie aplikacja.
Widoki i elementy graficzne
Przede wszystkim to, co będzie widoczne dla użytkownika — czyli po prostu widoki ;) Widokiem dla nas będzie zarówno konkretne menu, jak i konkretna plansza / poziom, na którym będziemy grać. Każdy widok musi mieć możliwość obsługi klawiatury oraz wygenerowania grafiki, dlatego nasz główny interfejs będzie wyglądał następująco:
public interface View {
public void renderFrame();
public void handleKey(KeyboardKey key);
}
Od widoku zależy to, co wyświetlamy, oraz to, jak reagujemy na konkretne klawisze. Stąd nasz interfejs będzie posiadał dwie metody: renderFrame() odpowiedzialną za wyświetlanie (metody odpowiedzialne za wyświetlanie są statyczne, dlatego nie przekazujemy żadnego obiektu w parametrach) oraz handleKey(KeyboardKey key), która posłuży nam do przekazania informacji o naciśnięciu klawisza.
Widoki te oczywiście mogą się zmieniać, menu może prowadzić do konkretnej planszy lub do innego menu — słowem, potrzebujemy sposobu, który pozwoli nam poruszać się pomiędzy widokami. Stworzymy do tego klasę, która będzie Springowym Serwisem — ViewManager — która pozwoli nam zmienić widok (wyświetlić inny, poprzez metodę displayView(String viewName) i będzie odpowiadała za wywoływanie odpowiednich jego metod (render / handleKey).
Skoro jesteśmy już przy widokach — jak zapewne pamiętasz ze wstepu, zdecydowaliśmy że wszystkie zasoby (grafiki, dźwięki itp) przechowywać będziemy w pamięci, i wczytamy je przed uruchomieniem konkretnego poziomu czy menu. Do zarządzania nimi stworzymy klasę ResourcesManager — zajmie się ona obsługą plików, wczytywaniem ich i przechowywaniem w pamięci. Na ten moment będziemy potrzebować przede wszystkim grafik, w przyszłości dodamy obsługę pozostałych typów w razie potrzeby.
Stan gry
Drugą ważną grupą obiektów jest stan gry, czyli bieżący status w jakim jest nasza gra. Model tej klasy jest ściśle związany z mechaniką gry, a więc jeśli piszesz własną wersję, może się ona różnić od naszej. W naszym przypadku musimy obsłużyć:
- Status bohatera
- lokalizację (x, y)
- kierunek poruszania się (lewo-prawo)
- bieżąca czynność (czy stoi, idzie, skacze itp — wykorzystamy to do animacji w kolejnych częściach)
- ilość zdrowia (tudzież karmy w naszym wypadku)
- poziom ‚złości’ na kotka
- aktywne bonusy
- Status gry
- zdobyte osiągnięcia
- ilość punktów w sumie i w podziale na plansze
- stan leveli — czy zostały pokonane, z jakim wynikiem, jakie osiągnięcia zdobyliśmy itp
Zamodelujemy to za pomocą 2 klas — GameStatus oraz HeroStatus, z którymi będziemy pracować.
Aby umożliwić funkcjonalność wczytywania gry i jej zapisywania, ułatwimy sobie to poprzez dodatkowy serwis, który stworzymy: GameStatusManager. Jego interfejs na ten moment będzie wyglądał następująco:
public interface GameStatusManager {
public GameStatus getGameStatus();
public Map<String, GameStatus> listGameStatuses();
public void loadGameStatus(String id);
public void saveGameStatus();
public void resetGameStatus();
public HeroStatus getCurrentHeroStatus();
public void resetHeroStatus();
}
Podczas gry potrzebujemy też przechowywać bieżący status planszy — tzn. pozycje wrogów, elementy otoczenia, bonusy itp. Informacje te będziemy zapisywać w obiektach typu GameLevel z wykorzystaniem osobnych klas dla każdego typu obiektu na planszy.
Uwaga: założenie, z jakiego wyszliśmy podczas tworzenia gry jest takie, że stan gry ‚w trakcie’ planszy się nie zapisuje — zapis stanu gry dokonywany jest po zakończeniu planszy. Z tego powodu stan konkretnej planszy mogliśmy pozostawić w obiekcie GameLevel. Moglibyśmy to zmienić przenosząc te informacje do GameStatus — polecamy taki zabieg jako ciekawe zadanie rozszerzające we własnym zakresie. My jednak w trakcie kursu będziemy pracowali z założeniem, że informacje te ‚przepadają’ jeśli zakończymy grę w trakcie jakiejś planszy.
Uwaga 2: część z opisanych elementów ulegnie zmianie w trakcie pisania naszej gry wraz z dodawaniem kolejnych funkcji i wiążącymi się z tym koniecznymi modyfikacjami. Podejście takie jest bezpieczniejsze i łatwiejsze niż próba przewidzenia wszystkiego na początku i dostosowywania modelu już od początku do pełnej funkcjonalności — wymagania się zmieniają z czasem, możemy mieć inne pomysły w przyszłości, a części może nam się nie udać zrealizować. Przyrostowe projektowanie i budowanie aplikacji pozwala bardzo elastycznie podchodzić do nowych wymagań i zmian, ale warunkiem powodzenia jest pisanie aplikacji w sposób, który zakłada jak najmniej i nie blokuje nam określonych zmian. W ogólnym przypadku oznacza to korzystanie w jak najszerszym zakresie z ‚automagii’ — wiązania obiektów jedynie poprzez ich interfejsy, unikania zapisywania rzeczy ‚na sztywno’ w kodzie oraz czerpanie garściami z wzorca Inversion of Control (w tym także z wstrzykiwania zależności, które w naszym wypadku ‚weżmiemy’ od Springa).
Automagia ze Springiem
Jak wspominaliśmy we wstepie, pobawimy się także Springiem, wykorzystamy trochę więcej jego ‚automagii’. Najpierw jednak musimy skonfigurować naszą aplikację tak, aby uruchamiała kontekst Springa przy starcie aplikacji. Będziemy więc potrzebowali metody pozwalającej nam uruchomić naszą grę — w tym wypadku stworzymy klasę Game ze statyczną metodą main. Oczywiście aplikacja ta jeszcze nic nie robi, poza uruchamianiem się. Aby zainicjować Springa musimy utworzyć kontekst — możemy to zrobić za pomocą XMLa (będzie to wyglądało podobnie jak w naszym kursie pisania aplikacji webowych), a możemy pokusić się o konfigurację tylko za pomocą adnotacji. W tym wypadku skorzystamy z tej drugiej opcji — naszą metodę main modyfikujemy tak, aby inicjowała kontekst Springa i szukała odpowiednich adnotacji w naszych pakietach:
@ComponentScan(basePackages="pl.kobietydokodu.footrzasta")
public class Game {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Game.class);
ctx.refresh();
ctx.close();
}
}
Możemy uruchomić naszą aplikację, na konsoli powinniśmy zobaczyć już informacje od Springa*.
*) pamiętaj o konfiguracji dla biblioteki logującej — w tym przypadku skorzystamy z biblioteki log4j; wystarczy dodać plik log4j.properties z poniższą konfiguracją do katalogu src/main/resources w module game-runtime:
# Root logger option
log4j.rootLogger=INFO, stdout
# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
Obsługa widoków
Springa wykorzystamy też do rozwiązania ciekawego problemu: mamy w naszej aplikacji klasę, która będzie zarządzała widokami. Musi ona wiedzieć o wszystkich widokach, które są osobnymi klasami. Oczywiście możemy to zrobić ręcznie — po prostu wpisać w kodzie listę klas-widoków. Jest to jednak żmudne, narażone na błędy, ale przede wszystkim nieautomatyczne — a przecież jesteśmy programistami ;) Z pomocą przychodzi nam funkcjonalność @Autowired ze Springa oraz kontekst aplikacji.
Z @Autowired korzystaliśmy już w naszym kursie, i jeśli potrzebujesz przypomnienia jak ona działa odsyłamy do lekcji #09.
Do tej pory wykorzystywaliśmy ją jednak tylko po to, aby pobrać jeden obiekt określonego typu. Adnotacja ta pozwala nam też na pobranie kolekcji beanów — w tym wypadku wszystkich, które implementują określony interfejs / dziedziczą po określonej klasie. Wystarczy, że pole nad którym umieścimy tą adnotację (lub analogicznie — argument konstruktora czy setter) będzie typu np. List, Set czy po prostu będzie tablicą. Spring uzupełni je stosownym zbiorem zawierającym wszystkie beany, które ‚pasują’ do określonego typu. Dzięki temu zabiegowi dodając nowy widok nie musimy pamiętać, aby ‚poinformować’ o nim naszego ViewManagera. W ten sposób nie znamy jednak id związanego z danym obiektem. Moglibyśmy w klasie widoku dodać pole, które by takie id przechowywało, ale leniwy programista to efektywny programista ;) Trzymają się tej maksymy skorzystamy z ApplicationContext. Jest to kontekst naszej aplikacji i domyślnie jest także beanem. Implementuje on interfejs ListableBeanFactory, który zawiera ciekawą metodę: getBeansOfType(Class klass). Zwraca ona dokładnie to, czego potrzebujemy — mapę, w której kluczem jest id, a wartością obiekt. Skoro lista dostępnych widoków nie jest już problemem, implementujemy pozostałe metody w najprostszy możliwy sposób:
public class MapViewManager implements ViewManager {
private Logger log = LoggerFactory.getLogger(getClass());
private Map<String, View> views;
@Autowired
private ApplicationContext context;
public void initializeViewsMap() {
views = Collections.unmodifiableMap(context.getBeansOfType(View.class));
views.entrySet().forEach(entry -> log.info("Found view {} under id {}", entry.getValue().getClass().getName(), entry.getKey()));
}
public void displayView(String viewName) {
if (views.containsKey(viewName)) {
currentView = views.get(viewName);
} else {
log.error("Cannot find view {}, ignoring", viewName);
}
}
//...
}
Uwaga! Aby kod ten działał, konieczne jest wywołanie metody initializeViewsMap po uruchomieniu aplikacji. Możemy to zrobić np. z użyciem adnotacji @Bean:
@Configuration
public class ViewsConfiguration {
@Bean(initMethod="initializeViewsMap")
public ViewManager gameViews() {
return new MapViewManager();
}
}
Parametr initMethod (istnieje także analogiczny parametr destroyMethod) określa nazwę metody, która zostanie wywołana po zainicjowaniu całego kontekstu Springa. Jest to wygodny (i bezpieczny) sposób na wykonywanie wszelkich czynności inicjujących w aplikacji.
Główny widok
Mamy już View managera, który pozwoli nam zmieniać widoki, ale nadal nie określiliśmy co się dzieje po uruchomieniu gry — który widok jest ‚pierwszy’. Ponownie — moglibyśmy na sztywno wskazać w kodzie, który widok należy traktować jako główny, ale ponownie — będziemy sprytniejsi ;)
Oczywiście musimy przechowywać informacje o tym, który widok jest ‚bieżący’ — najwygodniej będzie nam dodać pole typu View w naszym ViewManagerze, nazywając je np. currentView. Jeśli dodamy nad nim adnotację @Autowired, Spring powinien je uzupełnić automatycznie. Niestety tak się nie stanie, jeśli mamy więcej niż jedną implementację interfejsu View, ponieważ Spring nie będzie wiedział, którą ma wybrać (o czym nas poinformuje wyjątkiem podczas uruchamiania aplikacji). Są jednak co najmniej dwa sposoby na to, aby powiedzieć Springowi który widok jest ważniejszy od innych.
Pierwszy to interfejs Ordered, który zawiera metodę getOrder. Jeśli w jakiejkolwiek sytuacji Spring musi ‚uporządkować’ wiele beanów, to o ile implementują one ten interfejs, jest on wykorzystywany aby określić kolejność. Ta metoda ma jednak pewną wadę — wymaga od nas implementacji dodatkowej metody w każdym widoku, na dodatek takiej, która zwraca to samo dla większości z nich. To zdecydowanie nie jest clean code, i o ile moglibyśmy ‚wyciągnąć’ ją do klasy nadrzędnej AbstractView, to skorzystamy z przyjemniejszego rozwiązania.
Jest nim adnotacja @Primary, którą możemy umieścić nad całą klasą. Jeśli kiedykolwiek Spring napotka na konflikt podczas uzupełniania zależności, i dokładnie jeden z ‚możliwych’ beanów będzie posiadał tą adnotacje, to on zostanie wybrany. Wykorzystamy tą właściwość do wyświetlenia widoku MainMenu jako pierwszego. Deklarujemy więc nasze pole w następujący sposób:
@Autowired
private View currentView;
oraz tworzymy dwie klasy implementujące interfejs View — MainMenu oraz GameLevel, nad obiema umieszczając adnotację @Component oraz umieszczając adnotację @Primary nad klasą MainMenu. Implementacją tych widoków zajmiemy się w dalszej części.
Pierwsze pixele
Skoro wiemy już, jak będzie wyglądała nasza aplikacja od strony organizacji kodu, wiemy z jakich technologii będziemy korzystać, nie pozostaje nam nic innego jak zaprząc te wszystkie elementy do wspólnej pracy :) Na ten moment zadowolimy się potwierdzeniem, że nasza aplikacja ‘działa’ — możemy zmienić widok, wyświetlić wybrany widok itp.
Uruchamiamy LWJGL
Pierwszy krok to oczywiście uruchomienie naszego silnika graficznego — w tym wypadku biblioteki LWJGL. Aby nasza gra działała, musimy zainicjować tryb graficzny, a następnie uruchomić pętlę, która będzie odpowiedzialna za wyświetlanie i obsługę klawiszy.
Aby zainicjować tryb graficzny musimy określić m.in. rozdzielczość ekranu, sposób wyświetlania, projekcję itp — póki co skopiujmy poniższy fragment, a szczegółowy sposób jego działania omówimy w kolejnej lekcji. Potrzebny nam będzie centralny punkt aplikacji — obiekt, który będzie zarządzał pętlą wyświetlającą widoki. Wykorzystamy do tego klasę Game, która zawiera naszą metodę main. Klasa ta także może być beanem i to w niej zaimplementujemy główną pętlę gry. Aby zrobić z Game bean Springowy, dodajemy adnotację @Component — pozwoli nam to znaleźć za pomocą Springa naszą klasę, a następnie uruchamiać jej metody. Zmodyfikujmy więc naszą klasę Game aby wyglądała w poniższy sposób:
Naszą grę docelowo będziemy uruchamiać w trybie pełnoekranowym, potrzebujemy więc jakiegoś sposobu na jej zamknięcie. Ponieważ nie powinniśmy po prostu zakończyć aplikacji w momencie np. naciśnięcia klawisza Esc (być może przed zamknięciem musimy wykonać jakieś czynności — zwolnić zasoby, zapisać stan itp), musimy zbudować mechanizm który pozwoli tym zarządzać. Przed chwilą wykorzystaliśmy klasę Game do obsługi głównej pętli programu — zatem tam też musimy zaimplementować wyłączanie. Zrobimy to poprzez warunek pętli while i zmienną typu boolean. Zmienna ta będzie inicjowana wartością false, i dopóki pozostanie ona nieprawdziwa, pętla będzie działała nadal. Metoda closeGame ustawi wartość zmiennej na false — nie zakończy to działania aplikacji od razu, ale spowoduje przerwanie pętli, a więc zakończenie aplikacji po wykonaniu bieżącej iteracji do końca. W przyszłości wykorzystamy ten mechanizm także do ‚powiadamiania’ innych komponentów o tym, że aplikacja będzie zamykana. Klasę Game modyfikujemy w następujący sposób (to, co się dzieje w dodanym kodzie omówimy w kolejnej lekcji):
@Component
@ComponentScan(basePackages = "pl.kobietydokodu.footrzasta")
public class Game {
private Logger log = LoggerFactory.getLogger(getClass());
private boolean doClose = false;
@Autowired
ViewManager viewManager;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Game.class);
ctx.refresh();
Game gameBean = ctx.getBean(Game.class);
gameBean.renderFrames();
ctx.close();
}
private void renderFrames() {
try {
Display.setDisplayMode(new DisplayMode(800, 600));
Display.create();
} catch (LWJGLException e) {
log.error("Cannot create OpenGL display", e);
System.exit(0);
}
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glOrtho(0, 640, 480, 0, 1, -1);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
while (!doClose && !Display.isCloseRequested()) {
glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
viewManager.loop();
Display.update();
}
Display.destroy();
}
public void closeGame() {
doClose = true;
}
}
Wywołaniem metody closeGame zajmiemy się w poszczególnych widokach.
Budujemy widoki
Na ten moment nasze widoki będą realizować dwie funkcje: na ekranie pojawi się prostokąt, dzięki któremu będziemy wiedzieli, że wyświetlany jest konkretny widok, oraz będziemy obsługiwać klawisze pozwalające przechodzić do drugiego widoku. Dlaczego nie napiszemy jakiegoś tekstu? Otóż wymaga to trochę więcej pracy — musimy wczytać czcionkę, zamienić ją na grafikę i następnie narysować. Zajmiemy się tym w przyszłości, a póki co idziemy najszybszą drogą do celu — rysujemy prostokąt.
Rysowanie na ekranie
Pierwsze, co musimy zrobić, to określić kolor, którym będziemy rysować. Wywołujemy poniższą metodę:
GL11.glColor3f(0.5f, 0.5f, 1.0f);
Kolor wybieramy podając jego trzy składowe — r, g oraz b, każda w zakresie od 0 do 1. Jeśli nie wiesz, jak wybrany przez Ciebie kolor jest reprezentowany, możesz to sprawdzić np na stronie w3c, pamiętając jednak aby przeliczyć wartości RGB dzieląc każdą z nich przez 255.
Następnie rysujemy kształt. W OpenGL jedną z metod rysowania na ekranie jest tworzenie wielokątów, które mogą być wypełnione lub samymi krawędziami itp. Każdy kształt inicjujemy wywołując metodę glBegin, następnie wywołujemy glVertex2f (dla dwuwymiarowej grafiki, możemy w identyczny sposób tworzyć grafikę trójwymiarową — metoda ma w nazwie ‘3’ zamiast ‘2’) dla każdego z punktów podając jego współrzędne, a na końcu wywołujemy glEnd. Narysowanie prostokąta sprowadza się do poniższego kodu:
GL11.glBegin(GL11.GL_QUADS); // informujemy, że chcemy narysować prostokąt
GL11.glVertex2f(100, 100); //współprzędne pierwszego wierzchołka
GL11.glVertex2f(100 + 200, 100);
GL11.glVertex2f(100 + 200, 100 + 200);
GL11.glVertex2f(100, 100 + 200);
GL11.glEnd();
Szczegółowe wyjaśnienie jakie są inne dostępne opcje znajdziemy w dokumentacji OpenGL — LWJGL stanowi jedynie ‘most’ pomiędzy biblioteką OpenGL (napisaną w języku C) oraz kodem w Javie.
Jak pewnie zauważyłaś, wszystkie metody, których tutaj używamy, są statyczne. To jedna z niewielu sytuacji, w której metody statyczne są uzasadnione — w systemie zawsze będzie jedna klawiatura (w każdym razie nawet jeśli będzie ich więcej, to są obsługiwane identycznie), jeden wyświetlacz itp.
Obsługujemy klawisze
Klawiaturę możemy obsługiwać na dwa sposoby — pierwszy to sprawdzając, czy wybrany klawisz został naciśnięty. To mało wygodny sposób, jeśli chcemy zbudować uniwersalny mechanizm (każdy nasz widok może korzystać z innych klawiszy). Drugi sposób to użycie bufora klawiatury — każde naciśnięcie klawisza spowoduje dodanie odpowiedniego zdarzenia do kolejki, którą następnie możemy odczytywać. Ponieważ jest to bardziej elastyczne rozwiązanie, zastosujemy je w naszej grze. Oznacza to, że rejestrowane są wszystkie naciśnięcia klawiszy pomiędzy kolejnymi wyświetleniami ramek, a więc dla każdego jednego wywołania metody renderFrame() w naszym widoku, metoda handleKey może być wywołana zero, jeden lub więcej niż jeden raz. W naszym przypadku nie będzie to robiło różnicy, miej to jednak na uwadze pisząc własną grę — być może będzie to istotne w Twoim przypadku.
Przyjmijmy następujące zasady obsługi klawiszy na ten moment — przycisk Esc na widoku GameLevel przechodzi do głównego menu, z kolei na widoku MainMenu zamyka aplikację. Na widoku MainMenu ‚enter’ spowoduje przejście do widoku GameLevel.
Ponieważ LWJGL operuje na kodach klawiszy jako liczbach typu int, dla uproszczenia dodamy enum, w którym zapiszemy wszystkie klawisze w sposób bardziej czytelny. Ponadto obsługa klawiatury generuje dwa zdarzenia — naciśnięcie klawisza oraz jego zwolnienie. Zakładamy, że na ten moment interesuje nas tylko zwolnienie klawisza (to się zmieni w przyszłości, ale zmodyfikujemy to w swoim czasie).
Enum, którego będziemy używać do przekazywania informacji o klawiszach:
public enum KeyboardKey {
KEY_UNKNOWN(-1),
KEY_ESCAPE(1),
KEY_ENTER(28);
private int keyCode;
private KeyboardKey(int keyCode) {
this.keyCode = keyCode;
}
public static KeyboardKey findByCode(final int keyCode) {
Optional optionalKey = Arrays.asList(values()).stream().filter(key -> key.keyCode==keyCode).findFirst();
return optionalKey.orElse(KEY_UNKNOWN);
}
}
Implementacja metody loop w ViewManagerze:
public void loop() {
while (Keyboard.next()) {
if (!Keyboard.getEventKeyState()) { //reagujemy tylko na zwolnienie klawisza
currentView.handleKey(KeyboardKey.findByCode(Keyboard.getEventKey()));
}
}
currentView.renderFrame();
}
Implementacja metody handleKey dla MainMenu:
public void handleKey(KeyboardKey key) {
log.debug("Pressed key {}", key);
switch (key) {
case KEY_ESCAPE:
game.closeGame();
break;
case KEY_ENTER:
viewManager.displayView("gameLevel");
break;
}
}
Podsumowanie
Gratuluje, napisałaś właśnie działającą grę ;) Być może fabuła jeszcze nie porywa, ale coś się już wyświetla i ogólnie ‚coś działa’. W kolejnych częściach będziemy dodawać kolejne elementy aż nasza gra będzie w pełni grywalna !
Kod źródłowy tylko do tej lekcji możesz pobrać z repozytorium na GitHub lub tam go podejrzeć.
Aktualny kod Footrzastej znajdziesz na naszym repozytorium na GitHub.
Jeśli masz wątpliwości jak posługiwać się Git’em, instrukcje i linki znajdziesz w naszym wpisie na temat Git’a.
Kod źródłowy
Kody źródłowe są dostępne w serwisie GitHub — użyj przycisków po prawej aby pobrać lub przejrzeć kod do tego modułu. Jeśli masz wątpliwości, jak posługiwać się Git’em, instrukcje i linki znajdziesz w naszym wpisie na temat Git’a.
Zasoby
- Konfiguracja LWJGL z Mavenem (UWAGA! Wymagana dodatkowa konfiguracja Eclipse przy uruchamianiu)
- Obsługa klawiatury i myszki w LWJGL
- Rysowanie w LWJGL