2. Footrzasta — OpenGL, teksty i menu

By 22 January 2016 February 9th, 2016 Footrzasta

W poprzed­niej częś­ci uru­chomil­iśmy już naszą grę, która korzys­ta z OpenGL do wyświ­et­la­nia grafi­ki. Dzisi­aj zgłębimy się w to, co tak naprawdę ostat­nio zro­bil­iśmy, oraz dodamy obsługę wyświ­et­la­nia tek­stów i menu.

OpenGL w LWJGL

Korzys­tanie z OpenGL było potrak­towane dość tuto­ri­alowo w drugiej częś­ci serii — z uwa­gi na obsz­er­ność treś­ci zde­cy­dowal­iśmy się wydzielić to i dzisi­aj przyjrzymy się bliżej, co tak naprawdę robimy.

Trochę teorii

Aby zrozu­mieć, dlaczego robimy pewne rzeczy, zaczni­jmy od wyjaśnienia zasady dzi­ała­nia bib­liote­ki OpenGL od strony graficznej. Oso­by po ASP, architek­turze lub malu­jące w przeszłoś­ci mogą śmi­ało przeskoczyć do następ­nej sekcji ;)

Mon­i­tor kom­put­era jest płaszczyzną dwuwymi­arową, co nieule­ga wąt­pli­woś­ci. Jed­nocześnie zadaniem tej bib­liote­ki jest odzwier­ciedle­nie (możli­wie bliskie rzeczy­wis­toś­ci) trzech wymi­arów w sposób najbardziej nat­u­ral­ny jak tylko się da. Z drugiej strony założe­niem tej bib­liote­ki jest wspar­cie także mod­e­lowa­nia 2D, w którym takie pode­jś­cie było­by mniej real­isty­czne (ponieważ obraz 2D był­by pod­dawany trans­fro­macjom 3D).

Projekcje

OpenGL aby rozwiązać ten prob­lem ofer­u­je nam do wyboru tzw. pro­jekc­je — inny­mi słowy sposób patrzenia na świat. W przy­pad­ku obrazów 3D, najprost­szym przy­bliże­niem tego jak należy reprezen­tować obiek­ty w 3D na płaskim ekranie jest stożek — oko (czy kam­era) jest punk­tem, z którego ‘pole’ widzenia jest tym więk­sze, im dalej obiekt się zna­j­du­je. Pozwala to gen­erować wraże­nie głębii i oszuki­wać nasze oko.

W przy­pad­ku grafi­ki dwuwymi­arowej, wygod­niejsze jest trak­towanie trze­ciego wymi­aru jako ‘kole­jnoś­ci’ widzenia obiek­tów, a nie ‘odległoś­ci’, która wpły­wa na pro­por­c­je. Służy do tego pro­jekc­ja ortog­o­nal­na — czyli w uproszcze­niu przed­staw­ie­nie pola widzenia jako pudeł­ka o równoległych ścianach a następ­nie zwykłym ‘spłaszcze­niu’ go.

Różnice w pro­jekc­jach w OpenGL (źródło: http://www.labri.fr/perso/nrougier/teaching/opengl/)

Macierze

W doku­men­tacji OpenGL wielokrot­nie spotkasz się z macierza­mi, które obrazu­ją określoną funkcję czy kon­cepcję. Ma to swo­je uza­sad­nie­nie — macierze moż­na bard­zo łat­wo wyko­rzys­tać do przed­staw­ienia infor­ma­cji o świecie 3D oraz o przek­sz­tałce­ni­ach tego świa­ta (np. obro­cie o określony kąt itp). Takie oper­ac­je (obrót, pomniejszenie/powiększenie) sprowadza­ją się do właś­ci­wie jed­nej oper­acji — mnoże­nia macierzy (oczy­wiś­cie uprzed­nio musimy je wygen­erować). Ten spry­t­ny zabieg poz­wolił na znaczne przyspiesze­nie obliczeń graficznych w cza­sach kiedy moc obliczeniowa była trud­no dostęp­na (a współcześnie pozwala wycis­nąć jeszcze więcej ‘mocy’ z kart graficznych) — zwykły pro­ce­sor musi wiedzieć jak wykony­wać nie tylko dodawanie, ode­j­mowanie czy mnoże­nie, ale też dzie­le­nie czy pier­wiastkowanie — z tego powodu jest ukła­dem bard­zo uni­w­er­sal­nym, nada­ją­cym się do każdego rodza­ju obliczeń, który nie jest ‘wybit­ny’ w żad­nym rodza­ju obliczeń. GPU z kolei (czyli pro­ces dedykowany do zas­tosowań graficznych) musie wiedzieć tylko jak szy­bko mnożyć macierze, dzię­ki czemu moż­na zop­ty­mal­i­zować go tylko pod kątem tej oper­acji (to oczy­wiś­cie trochę uproszcze­nie, ale nie aż tak duże). Moż­na lubić matem­atykę lub nie, ale fak­tem jest, że dzię­ki niej może­my cieszyć się efek­ta­mi, które są podob­ne do zdjęć, a które generu­je mała czarna kost­ka gdzieś w środ­ku komputera ;)

Cieka­wost­ka: takie samo pode­jś­cie wyko­rzys­tu­je się w pro­ce­so­rach typu RISC, które zna­j­du­ją zas­tosowanie w tele­fonach komórkowych i innych urządzeni­ach mobil­nych — pozby­wa­jąc się częś­ci oper­acji, które pro­ce­sor jest w stanie wykon­ać, i opty­mal­izu­jąc go pod kątem tylko częś­ci najważniejszych, moż­na uzyskać układ mniejszy, prost­szy i bardziej ener­gooszczęd­ny, którego wyda­jność w niek­tórych aspek­tach będzie mniejsza, ale te moż­na ‘nadro­bić’ odpowied­nio mody­fiku­jąc opro­gramowanie tak, aby korzys­tało tylko z tych ‘szyb­szych’ operacji.

GL11 ?

Być może zas­tanow­iło Cię także co to za klasa GL11, z której staty­cznych metod korzys­tamy? Otóż ta i podob­ne klasy odpowiada­ją poszczegól­nym wer­sjom specy­fikacji OpenGL — GL11 to wer­s­ja 1.1 specy­fikacji. Tworząc bardziej pro­fesjon­alne gry ma to znacze­nie — w naszym przy­pad­ku nie za duże, nasza gra nie będzie ani wyma­ga­ją­ca ani skom­p­likowana graficznie, pod­sta­wowe funkc­je ze specy­fikacji 1.1 są w zupełnoś­ci wystar­cza­jące i raczej nie będzie potrze­by korzys­ta­nia z innych. Zain­tere­sowanych odsyłamy do doku­men­tacji OpenGL (funkc­je nazy­wa­ją się tak samo w LWJGL, jak ich odpowied­ni­ki w OpenGL).

Co my tak właściwie zrobiliśmy?

Wróćmy ter­az do kodu — czyli co tak naprawdę zro­bil­iśmy. Przeanal­izu­jmy lin­ij­ka po lin­i­jce kod, który inicju­je nasz tryb graficzny OpenGL.

GL11.glMatrixMode(GL11.GL_PROJECTION);

Ponieważ jak już wspom­nieliśmy OpenGL reprezen­tu­je wszys­tko na macierzach, więk­szość jego oper­acji jest uni­w­er­sal­na — np. obrót może doty­czyć kamery, ale może też doty­czyć tego, co już narysowal­iśmy. Ta funkc­ja to swego rodza­ju deklarac­ja z naszej strony, że wszys­tkie kole­jne wywoła­nia będą doty­czyły wybranego aspek­tu — w naszym przy­pad­ku projekcji.

GL11.glLoadIdentity();

To swego rodza­ju ‘rese­towanie’ — funkc­ja ta ‘czyś­ci’ (przy­wraca do domyśl­nego stanu) macierz przek­sz­tałceń. Dzię­ki temu mamy pewność, jaki jest stan początkowy, a więc także na jakiej pod­staw­ie dokonu­je­my przekształceń.

GL11.glOrtho(0, 640, 480, 0, 1, -1);

To bard­zo waż­na lin­ij­ka, ponieważ mówi ona OpenGL, że do pro­jekcji należy zas­tosować przek­sz­tałce­nie ortog­o­nalne (‘pudełko’, o którym pisal­iśmy trochę wyżej, w częś­ci teo­re­ty­cznej). Kole­jne para­me­try określa­ją zakres tego, co widz­imy, w kolejności:

  • lewa krawędź — oś X
  • prawa krawędź — oś X
  • dol­na krawędź — oś Y
  • gór­na krawędź — oś Y
  • bliska’ grani­ca widzenia — oś Z
  • dale­ka’ grani­ca widzenia — oś Z

Jest tutaj zaszyte kil­ka bard­zo ważnych infor­ma­cji. Przede wszys­tkim sposób, w jaki opisu­je­my położe­nie — zwróć uwagę, że gór­na krawędź ekranu to ‘0’, a gór­na to ‘480’ — punkt 0,0 jest więc w lewym górnym rogu ekranu.

Dru­ga kwes­t­ia to jed­nos­t­ki — o ile okres­lamy, że nasz widok ma 640 jed­nos­tek na sze­rokość i 480 na wysokość, nie jest to ani rozdziel­czość ani wymi­ary okien­ka. To umowne jed­nos­t­ki, których będziemy uży­wali do rysowa­nia po ekranie (OpenGL pracu­je z liczba­mi zmi­enno­przecinkowy­mi, więc możesz przyjąć zakres od 0 do 1, i też nie zmieni to rozdziel­czoś­ci czy jakoś­ci obrazu), nie są one związane z pik­se­la­mi czy jakimikol­wiek inny­mi jed­nos­tka­mi w kom­put­erze. Jedyne, co defini­u­ją, to pro­por­c­je obrazu — w tym wypad­ku 4:3, czyli klasy­cznych mon­i­torów. Być może zechcesz zmienić to na 16:9 lub inne, to kwes­t­ia wyboru. Zas­tanaw­iasz się też pewnie dlaczego uży­wa się takich liczb, a nie np. 1000 i 750, łatwiejszych do dzie­le­nia przez człowieka itp? Z tech­nicznego punk­tu widzenia to praw­da, i o ile pro­por­c­je będą zachowane, obraz będzie wyglą­dał podob­nie. Wybór 640x480 wiąże się z tym, że wartoś­ci te łat­wo przełożyć na realne pix­ele w mon­i­torze — rozdziel­czość ekranu jest częs­to wielokrot­noś­cią tych wartoś­ci. Dzię­ki temu grafik pracu­ją­cy nad grą może w prosty sposób wygen­erować ostrą lin­ię (np. w menu), zna­jąc rozdziel­czość ekranu oraz mnożnik. Było­by to utrud­nione sto­su­jąc potę­gi licz­by 10, do których obec­noś­ci się przyzwyczailiśmy.

Trze­cia kwes­t­ia to opty­mal­iza­c­ja — częs­to generu­jąc widok gry ‘rysu­je­my’ także poza tym, co widzi użytkown­ik — jak­iś ele­ment po pros­tu się na ekranie nie mieś­ci, innego ma być widać tylko kawałek itp. Dzię­ki okresle­niu co ma być widoczne OpenGL może w łatwy sposób zig­norować wszys­tko to, czego użytkown­ik nie zobaczy — ponown­ie oszczędza­jąc czas i pracę, nie pog­a­rsza­jąc efektu.

Co ważne — okno, w którym real­nie wyświ­et­lamy obraz może mieć dowolne wymi­ary, OpenGL wyskalu­je obraz tak, aby zawsze były widoczne 640x480 jed­nos­t­ki (w naszym przypadku).

GL11.glMatrixMode(GL11.GL_MODELVIEW);

Tutaj uży­wamy tej samej funkcji, co na początku, tym razem mówiąc, że chce­my pra­cow­ać z obiek­ta­mi widoku, tym, co fak­ty­cznie będzie widoczne.

W tym miejs­cu moze­my już gen­erować grafikę, przy każdym wyko­na­niu pętli wywołu­je­my jeszcze:

GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);

Funkc­ja ta czyś­ci bufory, a jej argu­men­ty mówią o tym, co należy wyczyś­cić — w naszym przy­pad­ku są to bufory koloru oraz głębii, czyli całość tego, co jest dla nas widoczne.

Ostat­nia funkc­ja, czyli

Display.update()

pobiera klatkę z bufo­ra OpenGL i wyświ­et­la ją w naszym oknie.

Wyświetlanie tekstu

Kole­jnym naszym krok­iem będzie wyświ­et­lanie tek­stu. Napo­tykamy tutaj na pier­wszy prob­lem — sys­te­my oper­a­cyjne. Fonty, które mogą być dostęp­ne w jed­nym sys­temie, niekoniecznie muszą być dostęp­ne w innym. Do tego kwest­ie licencji, drob­nych różnic w czcionkach o tej samej nazwie itp. Aby uniknąć tego bała­ganu, wszys­tki­mi fonta­mi będziemy zarządza­li w ramach pro­jek­tu — sko­rzys­tamy w tym celu z bezpłat­nych, otwartych fontów dostęp­nych w sieci.

Najwięk­szy­mi zbio­ra­mi bezpłat­nych fontów są Google Fonts oraz Font Library. Myśmy wybrali dwie z nich — Lato do nor­mal­nego tek­stu oraz Cook­ie do niek­tórych ele­men­tów (jak np. nazwa gry). Oczy­wiś­cie możesz wybrać inne, możesz wybrać więcej — ogranicza Cię tylko wyobraźnia ;)

Zwróć uwagę, że ‘font’ to nie tylko krój ale styl. Dlat­ego jeśli chcesz uży­wać określonego fonta zarówno w wer­sji nor­mal­nej, jak i pogru­bionej, musisz pobrać dwa pli­ki — dla sys­te­mu są one dwoma różny­mi fontami.

Uwa­ga: z jakiegoś powodu link do repozy­toriów Google w GitHub nie dzi­ała. Więk­szość fontów z Google Fonts moż­na znaleźć do pobra­nia na poprzed­nim, nie­ofic­jal­nym repozy­to­ri­um na GitHub.

Serwis vs metody klasy bazowej

Na wstępie parę słów o tym, jak zor­ga­nizu­je­my nasz kod, w szczegól­noś­ci metody związane z wyświ­et­laniem tek­stu. Teo­re­ty­cznie mogły­by to być metody klasy AbstractView, po których dziedz­iczył­by każdy widok, z pewnoś­cią zna­jdą się oso­by prefer­u­jące taką opcję. I będą miały rac­je, ale rację także będziemy mieli my decy­du­jąc się wyciągnąć je do osob­ne­go serwisu.

Uza­sad­nie­niem takiego pode­jś­cia jest fakt, że dla naszej gry wyświ­et­lanie tek­stu jest jed­ną z ofer­owanych przez warst­wę ser­wisów funkcjon­al­noś­ci — jest to na tyle duża część funkcji, że zasługu­je na wydzie­le­nie. Dodatkowo poz­woli to efek­ty­wniej zaim­ple­men­tować zarządzanie zasoba­mi — czcionka­mi itp.

Zwolen­ni­cy drugiego pode­jś­cia zwrócą zapewne uwagę, że jest to ode­jś­cie od pryn­cyp­iów OOP, z czym jesteśmy skłon­ni się zgodz­ić — ostate­cznie jest funkc­ja ‘widoku’, a więc w tej klasie powin­na znaleźć się ta metoda.

Jak już wspom­nieliśmy, oba pode­jś­cia są praw­idłowe — IT jest dziedz­iną w której jedyną poprawną odpowiedz­ią jest ‘to zależy’ na niemal każde pytanie. Każ­da decyz­ja jest pewnego rodza­ju kom­pro­misem — w tym wypad­ku poświę­camy trzy­manie się litery zasad pro­gramowa­nia obiek­towego na rzecz polep­szenia możli­woś­ci testowa­nia i zwięk­szenia wygody pro­gramowa­nia a także kładąc więk­szy nacisk na zasadę poje­dynczej odpowiedzial­noś­ci (sin­gle respon­si­bil­i­ty prin­ci­ple). Wybór jed­nej czy drugiej opcji nie jest błę­dem (i nigdy nie daj­cie sobie wmówić, że jest inaczej…), błę­dem (i to sporym) było­by zas­tosowanie różnych pode­jść do ana­log­icznego prob­le­mu w ramach jed­nego sys­te­mu. O ile nasza gra jest try­wial­na pod kątem złożonoś­ci, pracu­jąc w prawdzi­wym pro­jek­cie wielokrot­nie zmierzysz się z taką decyzją. Pamię­taj, że spójność pode­jś­cia jeśli chodzi o całość aplikacji (oczy­wiś­cie zakłada­jąc że wszys­tkie rozważane dro­gi pozwala­ją na imple­men­tac­je wszys­t­kich założeń) zawsze jest ważniejsza od pryn­cyp­i­al­nego trzy­ma­nia się zasad czy kon­wencji. Naj­gorsze co możesz zro­bić oso­bie pracu­jącej z daną aplikacją w przyszłoś­ci (a więc być może i sobie!) to namieszać w kon­cepc­jach uży­wanych do tworzenia aplikacji.

Wyświetlamy tekst

Na początku zad­bamy o to, żeby wyświ­etlić określony tekst. Defini­u­je­my więc nowy ser­wis, TextSer­vice, w którym doda­je­my jed­ną metodę:

public void renderText(String content, Float positionX, Float positionY, TextAlignment alignment, String fontName, Color color);

Uży­ty enum Tex­tAl­ign­ment deklaru­je­my następująco:

public enum TextAlignment {
LEFT, RIGHT, CENTER
}

Zadaniem zadeklarowanej metody będzie wyświ­etle­nie tekst w miejs­cu o okres­lonych współrzęd­nych, uży­wa­jąc czcion­ki o zadanej nazwie. Do obsłu­gi fontów wyko­rzys­tamy bib­liotekę slick-utils, która posi­a­da m.in. klasę True­Type­Font, którą wykorzystamy.

Nazwa fontu będzie odpowiadała nazwie pliku, w którym będzie on prze­chowywany. Aby uniknąć wielokrot­nego wczy­ty­wa­nia tych samych zasobów, raz wczy­taną czcionkę będziemy prze­chowywali w pamię­ci w celu ponownego wyko­rzys­ta­nia. W imple­men­tacji TextSer­vice tworzymy zatem logikę odpowiedzial­ną za wczy­ty­wanie czcionek, bazu­jąc na doku­men­tacji LWJGL:

public class TrueTypeFontTextService implements TextService {
    private Map<String, TrueTypeFont> fontsCache = new HashMap<>();

    private TrueTypeFont getFont(String fontName) {
        if (!fontsCache.containsKey(fontName)) {
            try {
                InputStream inputStream = getClass().getClassLoader().getResourceAsStream("fonts/"+fontName+".ttf");
                Font awtFont = Font.createFont(Font.TRUETYPE_FONT, inputStream);
                awtFont = awtFont.deriveFont(24f); //domyślny, stały rozmiar czcionki
                fontsCache.put(fontName, new TrueTypeFont(awtFont, true));         
            } catch (Exception e) {
                log.error("Nie można wczytać fontu", e);
            }
        }
        return fontsCache.get(fontName);
    }

    // ...

}

W powyższym frag­men­cie najpierw otwier­amy plik ttf jako Input­Stream, na pod­staw­ie którego następ­nie tworzymy obiekt typu Font (jest to klasa będą­ca częś­cią Javy), po czym uży­wamy tego obiek­tu do utworzenia instacji klasy True­Type­Font (będącej częś­cią bib­liote­ki Sick2D). Ponieważ pli­ki .ttf będą częś­cią pliku JAR z grą, a nie osob­ny­mi plika­mi w określonym miejs­cu na dysku, nie jest możli­we ich odczy­tanie za pomocą stan­dar­d­owego obiek­tu File. Java dostar­cza narzędzia pozwala­jące pra­cow­ać z plika­mi ‘wewnątrz’ pacz­ki JAR poprzez metody klasy Class­Loader — w tym wypad­ku metodę getRe­source­AsStream(…). Szer­sze omówie­nie tej metody zna­j­du­je się w ofic­jal­nej doku­men­tacji.

Uwa­ga: Spring także dostar­cza narzędzi pozwala­ją­cych na pracę z zasoba­mi — zarówno tymi wewnątrz pliku JAR, jak i np. dostęp­ny­mi z uży­ciem pro­tokołu HTTP (więcej infor­ma­cji zna­jdziesz w ofic­jal­nej doku­men­tacji) — w naszym przy­pad­ku nie potrze­bowal­iśmy dodatkowych możli­woś­ci i uży­cie stan­dar­d­owych narzędzi Javy bezpośred­nio było prost­szym rozwiązaniem.

W powyższym frag­men­cie na stałe deklaru­je­my rozmi­ar impor­towanej czcion­ki. W przyszłoś­ci poz­wolimy wybrać dowol­ny rozmi­ar, na ten moment wystar­czy nam uproszc­zony mechanizm.

Pozosta­je nam zaim­ple­men­tować metodę inter­fe­j­su renderText:

public void renderText(String content, Float positionX, Float positionY, TextAlignment alignment, String fontName, Color color) {
    TrueTypeFont font = getFont(fontName);
    Integer textWidth = font.getWidth(content);
    Float actualPositionX = positionX;
    switch (alignment) {
        case CENTER:
            actualPositionX -= textWidth/2;
            break;
        case RIGHT: 
            actualPositionX -= textWidth;
            break;
    }
    org.newdawn.slick.Color slickColor = new org.newdawn.slick.Color(color.getRed(), 
            color.getGreen(), color.getBlue(), color.getAlpha());
    org.newdawn.slick.Color slickColor = new org.newdawn.slick.Color(color.getRed(), 
                color.getGreen(), color.getBlue(), color.getAlpha());
    GL11.glEnable(GL11.GL_TEXTURE_2D);
    font.drawString(actualPositionX, positionY, content, slickColor);
    GL11.glDisable(GL11.GL_TEXTURE_2D);
}

Powyższa imple­men­tac­ja pobiera obiekt typu True­Type­Font (z pamię­ci podręcznej lub wczy­tu­jąc czcionkę z pliku), po czym przelicza położe­nie uwzględ­ni­a­jąc wybrane przez nas wyrów­nanie tek­stu do prawej, lewej lub środ­ka (w tym celu korzys­tamy z pomoc­niczej metody getWidth, która zwraca nam sze­rokość danego napisu z uży­cie tej czcion­ki). Mamy więc gotową metodę pozwala­jącą wyświ­et­lać tekst na ekranie.

Kod dookoła metody draw­String związany jest ze sposobem, w jaki napis jest wyświ­et­lany — aby dzi­ałały nasze proste ksz­tał­ty razem z tek­stem, musimy go otoczyć takim blok­iem (więcej o tym powiemy sobie w przyszłoś­ci). Aby wyświ­et­lony tekst był este­ty­czny, musimy dodać jeszcze jeden frag­ment do naszej klasy Game (zaraz po GL11.glMatrixMode(GL11.GL_MODELVIEW);):

GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

Pozwala on na praw­idłową obsługę tzw. kanału alfa — czyli w grafice infor­ma­cji o ‘kryciu’ danego ele­men­tu (lub też jego przezroczys­toś­ci, jeśli ode­jmiemy wartość alfa od 100%).

Budujemy menu

Nasza klasa ‘Main­Menu’ jest menu tylko z nazwy jak do tej pory — wyświ­et­la staty­czny tekst ;) Zajmiemy się więc zbu­dowaniem menu z prawdzi­wego zdarzenia.

Z punk­tu widzenia aplikacji każde menu jest takie samo — ma opc­je, które po wybra­niu wykonu­ją określone czyn­noś­ci, wyświ­et­la­ją się one w określonej kole­jnoś­ci, ma tło i grafikę przy­cisków. Jedyną ‘zmi­en­ną’ rzeczą są więc opc­je — sposób wyświ­etle­nia każdej z nich pozosta­je iden­ty­czny. Wydzielmy więc funkcjon­al­ność odpowiedzial­ną za ren­derowanie menu do klasy nadrzęd­nej, a w klasach menu pozostawmy jedynie ele­men­ty, które się zmieni­a­ją — opcje.

Pier­wszym krok­iem będzie więc imple­men­tac­ja klasy nadrzędnej:

public abstract class AbstractMenu implements View {

    private int currentElementIndex = 0;

    @Autowired
    private TextService textService;

    @Override
    public void renderFrame() {
        float initialPositionY = 50f;
        float incrementY = 50f;
        float positionX = 320f;
        List elements = getElements();
        for (int renderingElement = 0; renderingElement<elements.size(); renderingElement++) {
            MenuElement element = elements.get(renderingElement);
            Color color = (renderingElement==currentElementIndex) ? Color.BLUE : Color.GREEN;
            textServicerenderText(element.getLabel(), positionX, initialPositionY+renderingElement*incrementY, TextAlignment.CENTER, "lato", color)
        }
    }

    @Override
    @SuppressWarnings("incomplete-switch")
    public void handleKey(KeyboardKey key) {
        switch (key){
            case KEY_ENTER:
                getElements().get(currentElementIndex).getAction().doAction();
                break;
            case KEY_ESCAPE:
                closeMenu();
                break;
            case KEY_UP:
                currentElementIndex = Math.floorMod(currentElementIndex-1, getElements().size());
                break;
            case KEY_DOWN:
                currentElementIndex = Math.floorMod(currentElementIndex+1, getElements().size());
                break;
        }
    }

    protected abstract void closeMenu();

    protected abstract List getElements();
}

Powyższy kod opiera się o metodę abstrak­cyjną zwraca­jącą listę ele­men­tów menu. Każdy ele­ment menu to tekst opisu wraz z akcją wykony­waną po wybra­niu danego ele­men­tu. Poza listą ele­men­tów prze­chowu­je­my także ten, który jest aktu­al­nie akty­wny w postaci indek­su (gdzie 0 oznacza pier­wszy od góry ele­ment menu).

Uwa­ga: w kodzie uży­wamy met­dy Math.floorMod(x, y), ponieważ stan­dar­d­owy oper­a­tor mod­u­lo w Javie może zwracać licz­by ujemne (dyskusję dlaczego to jest matem­aty­cznie popraw­na imple­men­tac­ja zna­jdziesz np. na stackoverflow.com)

Dru­ga meto­da abstrak­cyj­na, close­Menu(), poz­woli obsłużyć ‘wyjś­cie’ z menu (naciśnię­cie klaw­isza ESC).

Kod obsługu­je strza­ł­ki w górę oraz w dół, a także klaw­isz enter do zatwierdza­nia wyboru i ESC do wyco­fy­wa­nia się w wybranego menu. Aby kod zadzi­ałał, konieczne jest rozsz­erze­nie naszego enu­ma o dodatkowe wartoś­ci (oczy­wiś­cie bazu­jąc na wartoś­ci­ach stałych z LWJGL):

public enum KeyboardKey {

    KEY_UNKNOWN(-1),
    KEY_ESCAPE(1), 
    KEY_ENTER(28),
    KEY_UP(200),
    KEY_DOWN(208);

    //...

}

dodatkowo konieczne będą dodatkowe klasy i inter­fe­jsy do reprezen­tacji ele­men­tów menu:

public interface MenuAction {
    public void doAction();
}
public class MenuElement {
    private String label;
    private MenuAction action;

    //... gettery i settery ...
}

To imple­men­tac­ja bard­zo pros­ta — zakła­da, że wszys­tkie ele­men­ty menu zmieszczą się na jed­nym ekranie, a jedynym ozdob­nikiem jest zmi­ana koloru. To jed­nak tylko wstęp i w przyszłoś­ci zro­bimy z niego perełkę ;)

Nie pozosta­je nam nic innego jak zaim­ple­men­tować naszą klasę Main­Menu jako dziedz­iczącą po Abstract­Menu — póki co dodaj do niej dowolne ele­men­ty, które mogą nie mieć żad­nej przyp­isanej akcji.

Podsumowanie

Choć nadal nie może­my jeszcze fak­ty­cznie grać, Footrza­s­ta ma już dzi­ała­jące menu i możli­wość wyświ­et­la­nia tek­stów. W kole­jnym kroku zajmiemy się nieco uporząd­kowaniem kodu oraz zasoba­mi gry, po czym zaczniemy pisać to, co najważniejsze — samą logikę oraz plansze!

Kod źródłowy

Przeglądaj kodPobierz ZIP

Kody źródłowe są dostęp­ne w ser­wisie GitHub — użyj przy­cisków po prawej aby pobrać lub prze­jrzeć kod do tego mod­ułu. Jeśli masz wąt­pli­woś­ci, jak posługi­wać się Git’em, instrukc­je i lin­ki zna­jdziesz w naszym wpisie na tem­at Git’a.