1. Footrzasta – wyświetlamy grafikę

By 9 January 2016Footrzasta

W drugim odcinku serii o Footrza­stej stworzymy pod­waliny pod dal­szą pracę — będziemy rozróż­ni­ać pomiędzy ‚pozioma­mi’ oraz ‚menu’, a także skon­fig­u­ru­je­my Springa tak, aby wygod­nie nam się pra­cow­ało dalej. 

Póki co to, co wyświ­etlimy będzie bardziej przy­pom­i­nało powrót do zło­tych lat 90’tych, ale będziemy na tym budować dalej :)

Konfiguracja

Naszą grą będziemy zarządzać jako pro­jek­tem Mavenowym. Aby nie prze­j­mować się wer­s­ja­mi naszych zależnoś­ci sko­rzys­tamy z ciekawego pod­pro­jek­tu Spring o nazwie Spring IO.

Spring IO

Spring IO w swo­jej pod­sta­wowej wer­sji to przede wszys­tkim zarządzanie wer­s­ja­mi zależnoś­ci — wer­s­ja plat­formy określa wer­sje dla wielu pop­u­larnych bib­liotek i narzędzi, które moż­na impor­tować do pro­jek­tu. Dzię­ki temu nasze pom’y są wolne od numerów wer­sji (o ile oczy­wiś­cie dana bib­liote­ka jest zarządzana przez Spring IO), a aktu­al­iza­c­ja wer­sji sprowadza się do zak­tu­al­i­zowa­nia numeru wer­sji Spring IO (oczy­wiś­cie być może będziemy musieli dos­tosować nasz kod — mamy nato­mi­ast pewność, że bib­liote­ki i ich wer­sje są ze sobą zgodne i współpracu­ją bez prob­le­mu).

Korzystamy ze Spring IO w naszym projekcie

Zaczy­namy od utworzenia nowego pro­jekt (tutaj przeczy­tasz, jak moż­na to zro­bić w Eclipse) i w głównym pom.xml doda­je­my poniższy frag­ment:

<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 wystar­czy, abyśmy mogli korzys­tać w plikach pom.xml z depen­den­cy bez podawa­nia numerów wer­sji (odby­wa się to za pomocą mech­a­niz­mu Mave­na o nazwie depen­den­cy man­age­ment — pro­jekt, który pobier­amy to tak naprawdę <a>długi pom.xml</a> z wielo­ma taga­mi <depen­den­cy­Man­age­ment> określa­ją­cy­mi praw­idłową wer­sję okres­lonej bib­liote­ki; pobier­a­jąc jed­ną z nich jako <depen­den­cy> w naszym pro­jek­cie, jeśli nie podamy wer­sji wyko­rzysty­wana jest ta zdefin­iowana tutaj). Utworzymy sobie pięć mod­ułów w naszym pro­jek­cie:

  • game-mod­el — tutaj będzie mod­el naszej aplikacji, wszys­tkie inter­fe­jsy i klasy, których będziemy uży­wać
  • game-resources — mod­uł ten będzie nam służył jedynie do prze­chowywa­nia zasobów gry — grafik, plików dźwiękowych itp
  • game-run­time — tutaj trafią imple­men­tac­je wszys­t­kich inter­fe­jsów, ser­wisy, kon­fig­u­rac­ja Spring’a, głów­na klasa służą­ca do uruchami­a­nia itp
  • game-views — w tym mod­ule umieścimy wszys­tkie wido­ki, czyli zarówno plan­sze jak i menu
  • game-per­sis­tence — traf­fic tutor kid odpowiedzial­ny za utr­walanie obiek­tów, zapis stanu gry itp

Taki podzi­ał ma charak­ter głównie porząd­kowy i jest on dość arbi­tral­ny — w przy­pad­ku aplikacji desk­topowych ‚kon­wenc­je’ związane z tym jak dzielić pro­jekt na mod­uły nie są tak wykrys­tal­i­zowane i nat­u­ralne jak w przy­pad­ku aplikacji webowych, dodatkowo nasza gra nie jest też stan­dar­d­ową aplikacją CRUDową. Moż­na oczy­wiś­cie umieś­cić wszys­tko w jed­nym, głównym mod­ule Mavenowym — my prefer­u­je­my jed­nak podzi­ał na mniejsze pod­mod­uły.

W tym miejs­cu mała dygres­ja — dość częst dosta­je­my pyta­nia ‚dlaczego zostało to podzielone na takie mod­uły a nie inne’, lub ‚czy moż­na to podzielić tak i tak’. Odpowiedź najczęś­ciej brz­mi: tak, moż­na to podzielić inaczej, a zas­tosowany przez nas podzi­ał okazał się prak­ty­czny i sprawdz­ił się w innych pro­jek­tach. To nie jest wiedza mag­icz­na, czy jak­iś stan­dard — jeśli zas­tosowany przez nas podzi­ał wyda­je Ci się niel­og­iczny lub zbyt dro­bi­az­gowy, nic nie stoi na przeszkodzie abyś zas­tosowała włas­ny, który będzie dla Ciebie bardziej intu­icyjny. Jest to jed­na z tym rzeczy, kiedy więk­szość rozwiązań nie jest ‚dobra’ czy ‚zła’, a najważniejszym kry­teri­um powin­no być dopa­sowanie do pref­er­encji osób pracu­ją­cych z pro­jek­tem.

Poniżej przed­staw­iamy dia­gram naszych mod­ułów oraz zależnoś­ci pomiędzy nimi dla zobra­zowa­nia jak zor­ga­ni­zowana będzie aplikac­ja.

Zależności pomiędzy modułami w projekcie

Zależnoś­ci pomiędzy mod­uła­mi w pro­jek­cie

Model aplikacji

Przede wszys­tkim podzielmy naszą grę na częś­ci pier­wsze, dzię­ki czemu będziemy wiedzieli jak będzie ona dzi­ałać oraz jakie mech­a­nizmy i klasy będą nam potrzeb­ne.

Widoki i elementy graficzne

Przede wszys­tkim to, co będzie widoczne dla użytkown­i­ka — czyli po pros­tu wido­ki ;) Widok­iem dla nas będzie zarówno konkretne menu, jak i konkret­na plan­sza / poziom, na którym będziemy grać. Każdy widok musi mieć możli­wość obsłu­gi klaw­iatu­ry oraz wygen­erowa­nia grafi­ki, dlat­ego nasz główny inter­fe­js będzie wyglą­dał następu­ją­co:

public interface View {
    public void renderFrame();
    public void handleKey(KeyboardKey key);
}

Od widoku zależy to, co wyświ­et­lamy, oraz to, jak reagu­je­my na konkretne klaw­isze. Stąd nasz inter­fe­js będzie posi­adał dwie metody: ren­der­Frame() odpowiedzial­ną za wyświ­et­lanie (metody odpowiedzialne za wyświ­et­lanie są staty­czne, dlat­ego nie przekazu­je­my żad­nego obiek­tu w para­me­tra­ch) oraz handleKey(KeyboardKey key), która posłuży nam do przekaza­nia infor­ma­cji o naciśnię­ciu klaw­isza.

Wido­ki te oczy­wiś­cie mogą się zmieni­ać, menu może prowadz­ić do konkret­nej plan­szy lub do innego menu — słowem, potrze­bu­je­my sposobu, który poz­woli nam poruszać się pomiędzy widoka­mi. Stworzymy do tego klasę, która będzie Springowym Ser­wisem — View­Man­ag­er — która poz­woli nam zmienić widok (wyświ­etlić inny, poprzez metodę displayView(String view­Name) i będzie odpowiadała za wywoły­wanie odpowied­nich jego metod (ren­der / han­dleKey).

Sko­ro jesteśmy już przy widokach — jak zapewne pamię­tasz ze wstepu, zde­cy­dowal­iśmy że wszys­tkie zaso­by (grafi­ki, dźwię­ki itp) prze­chowywać będziemy w pamię­ci, i wczy­tamy je przed uru­chomie­niem konkret­nego poziomu czy menu. Do zarządza­nia nimi stworzymy klasę Resources­Man­ag­er — zajmie się ona obsługą plików, wczy­ty­waniem ich i prze­chowywaniem w pamię­ci. Na ten moment będziemy potrze­bować przede wszys­tkim grafik, w przyszłoś­ci dodamy obsługę pozostałych typów w razie potrze­by.

Stan gry

Drugą ważną grupą obiek­tów jest stan gry, czyli bieżą­cy sta­tus w jakim jest nasza gra. Mod­el tej klasy jest ściśle związany z mechaniką gry, a więc jeśli piszesz włas­ną wer­sję, może się ona różnić od naszej. W naszym przy­pad­ku musimy obsłużyć:

  • Sta­tus bohat­era
    • lokaliza­cję (x, y)
    • kierunek porusza­nia się (lewo-pra­wo)
    • bieżą­ca czyn­ność (czy stoi, idzie, skacze itp — wyko­rzys­tamy to do ani­macji w kole­jnych częś­ci­ach)
    • ilość zdrowia (tudzież karmy w naszym wypad­ku)
    • poziom ‚złoś­ci’ na kot­ka
    • akty­wne bonusy
  • Sta­tus gry
    • zdobyte osiąg­nię­cia
    • ilość punk­tów w sum­ie i w podziale na plan­sze
    • stan lev­eli — czy zostały poko­nane, z jakim wynikiem, jakie osiąg­nię­cia zdobyliśmy itp

Zamod­elu­je­my to za pomocą 2 klas — GameS­ta­tus oraz HeroSta­tus, z który­mi będziemy pra­cow­ać.

Aby umożli­wić funkcjon­al­ność wczy­ty­wa­nia gry i jej zapisy­wa­nia, ułatwimy sobie to poprzez dodatkowy ser­wis, który stworzymy: GameS­ta­tus­Man­ag­er. Jego inter­fe­js na ten moment będzie wyglą­dał następu­ją­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();

}

Pod­czas gry potrze­bu­je­my też prze­chowywać bieżą­cy sta­tus plan­szy — tzn. pozy­c­je wrogów, ele­men­ty otoczenia, bonusy itp. Infor­ma­c­je te będziemy zapisy­wać w obiek­tach typu GameLev­el z wyko­rzys­taniem osob­nych klas dla każdego typu obiek­tu na plan­szy.

Uwa­ga: założe­nie, z jakiego wys­zliśmy pod­czas tworzenia gry jest takie, że stan gry ‚w trak­cie’ plan­szy się nie zapisu­je — zapis stanu gry dokony­wany jest po zakończe­niu plan­szy. Z tego powodu stan konkret­nej plan­szy mogliśmy pozostaw­ić w obiek­cie GameLev­el. Moglibyśmy to zmienić przenosząc te infor­ma­c­je do GameS­ta­tus — pole­camy taki zabieg jako ciekawe zadanie rozsz­erza­jące we włas­nym zakre­sie. My jed­nak w trak­cie kur­su będziemy pra­cow­ali z założe­niem, że infor­ma­c­je te ‚przepada­ją’ jeśli zakończymy grę w trak­cie jakiejś plan­szy.

Uwa­ga 2: część z opisanych ele­men­tów uleg­nie zmi­an­ie w trak­cie pisa­nia naszej gry wraz z dodawaniem kole­jnych funkcji i wiążą­cy­mi się z tym konieczny­mi mody­fikac­ja­mi. Pode­jś­cie takie jest bez­pieczniejsze i łatwiejsze niż pró­ba przewidzenia wszys­tkiego na początku i dos­tosowywa­nia mod­elu już od początku do pełnej funkcjon­al­noś­ci — wyma­gania się zmieni­a­ją z cza­sem, może­my mieć inne pomysły w przyszłoś­ci, a częś­ci może nam się nie udać zre­al­i­zować. Przy­ros­towe pro­jek­towanie i budowanie aplikacji pozwala bard­zo elasty­cznie pod­chodz­ić do nowych wyma­gań i zmi­an, ale warunk­iem powodzenia jest pisanie aplikacji w sposób, który zakła­da jak najm­niej i nie bloku­je nam określonych zmi­an. W ogól­nym przy­pad­ku oznacza to korzys­tanie w jak najsz­er­szym zakre­sie z ‚automagii’ — wiąza­nia obiek­tów jedynie poprzez ich inter­fe­jsy, unika­nia zapisy­wa­nia rzeczy ‚na szty­wno’ w kodzie oraz czer­panie garś­ci­a­mi z wzor­ca Inver­sion of Con­trol (w tym także z wstrzyki­wa­nia zależnoś­ci, które w naszym wypad­ku ‚weżmiemy’ od Springa).

Automagia ze Springiem

Jak wspom­i­nal­iśmy we wstepie, pobaw­imy się także Springiem, wyko­rzys­tamy trochę więcej jego ‚automagii’. Najpierw jed­nak musimy skon­fig­urować naszą aplikację tak, aby uruchami­ała kon­tekst Springa przy star­cie aplikacji. Będziemy więc potrze­bowali metody pozwala­jącej nam uru­chomić naszą grę — w tym wypad­ku stworzymy klasę Game ze staty­czną metodą main. Oczy­wiś­cie aplikac­ja ta jeszcze nic nie robi, poza uruchami­an­iem się. Aby zainicjować Springa musimy utworzyć kon­tekst — może­my to zro­bić za pomocą XMLa (będzie to wyglą­dało podob­nie jak w naszym kur­sie pisa­nia aplikacji webowych), a może­my pokusić się o kon­fig­u­rację tylko za pomocą adno­tacji. W tym wypad­ku sko­rzys­tamy z tej drugiej opcji — naszą metodę main mody­fiku­je­my tak, aby inicjowała kon­tekst Springa i szukała odpowied­nich adno­tacji w naszych paki­etach:

@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że­my uru­chomić naszą aplikację, na kon­soli powin­niśmy zobaczyć już infor­ma­c­je od Springa*.

*) pamię­taj o kon­fig­u­racji dla bib­liote­ki logu­jącej — w tym przy­pad­ku sko­rzys­tamy z bib­liote­ki log4j; wystar­czy dodać plik log4j.properties z poniższą kon­fig­u­racją do kat­a­logu src/main/resources w mod­ule game-run­time:

# 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 wyko­rzys­tamy też do rozwiąza­nia ciekawego prob­le­mu: mamy w naszej aplikacji klasę, która będzie zarządza­ła widoka­mi. Musi ona wiedzieć o wszys­t­kich widokach, które są osob­ny­mi klasa­mi. Oczy­wiś­cie może­my to zro­bić ręcznie — po pros­tu wpisać w kodzie listę klas-widoków. Jest to jed­nak żmudne, narażone na błędy, ale przede wszys­tkim nieau­tomaty­czne — a prze­cież jesteśmy pro­gramis­ta­mi ;) Z pomocą przy­chodzi nam funkcjon­al­ność @Autowired ze Springa oraz kon­tekst aplikacji.

Z @Autowired korzys­tal­iśmy już w naszym kur­sie, i jeśli potrze­bu­jesz przy­pom­nienia jak ona dzi­ała odsyłamy do lekcji #09.

Do tej pory wyko­rzysty­wal­iśmy ją jed­nak tylko po to, aby pobrać jeden obiekt określonego typu. Adno­tac­ja ta pozwala nam też na pobranie kolekcji beanów — w tym wypad­ku wszys­t­kich, które imple­men­tu­ją określony inter­fe­js / dziedz­iczą po określonej klasie. Wystar­czy, że pole nad którym umieścimy tą adno­tację (lub ana­log­icznie — argu­ment kon­struk­to­ra czy set­ter) będzie typu np. List, Set czy po pros­tu będzie tablicą. Spring uzu­pełni je stosownym zbiorem zaw­ier­a­ją­cym wszys­tkie beany, które ‚pasu­ją’ do określonego typu. Dzię­ki temu zabiegowi doda­jąc nowy widok nie musimy pamię­tać, aby ‚poin­for­mować’ o nim naszego View­Man­agera. W ten sposób nie znamy jed­nak id związanego z danym obiek­tem. Moglibyśmy w klasie widoku dodać pole, które by takie id prze­chowywało, ale leni­wy pro­gramista to efek­ty­wny pro­gramista ;) Trzy­ma­ją się tej maksymy sko­rzys­tamy z Appli­ca­tion­Con­text. Jest to kon­tekst naszej aplikacji i domyśl­nie jest także beanem. Imple­men­tu­je on inter­fe­js Listable­Bean­Fac­to­ry, który zaw­iera ciekawą metodę: getBeansOfType(Class klass). Zwraca ona dokład­nie to, czego potrze­bu­je­my — mapę, w której kluczem jest id, a wartoś­cią obiekt. Sko­ro lista dostęp­nych widoków nie jest już prob­le­mem, imple­men­tu­je­my pozostałe metody w najprost­szy możli­wy 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);
        }
    }
    //...    
}

Uwa­ga! Aby kod ten dzi­ałał, konieczne jest wywołanie metody ini­tial­ize­ViewsMap po uru­chomie­niu aplikacji. Może­my to zro­bić np. z uży­ciem adno­tacji @Bean:

@Configuration
public class ViewsConfiguration {

	@Bean(initMethod="initializeViewsMap")
	public ViewManager gameViews() {
		return new MapViewManager();
	}

}

Para­metr init­Method (ist­nieje także ana­log­iczny para­metr destroyMethod) określa nazwę metody, która zostanie wywołana po zainicjowa­niu całego kon­tek­stu Springa. Jest to wygod­ny (i bez­pieczny) sposób na wykony­wanie wszel­kich czyn­noś­ci inicju­ją­cych w aplikacji.

Główny widok

Mamy już View man­agera, który poz­woli nam zmieni­ać wido­ki, ale nadal nie określil­iśmy co się dzieje po uru­chomie­niu gry — który widok jest ‚pier­wszy’. Ponown­ie — moglibyśmy na szty­wno wskazać w kodzie, który widok należy trak­tować jako główny, ale ponown­ie — będziemy spry­t­niejsi ;)

Oczy­wiś­cie musimy prze­chowywać infor­ma­c­je o tym, który widok jest ‚bieżą­cy’ — najwygod­niej będzie nam dodać pole typu View w naszym View­Man­agerze, nazy­wa­jąc je np. cur­rentView. Jeśli dodamy nad nim adno­tację @Autowired, Spring powinien je uzu­pełnić automaty­cznie. Nieste­ty tak się nie stanie, jeśli mamy więcej niż jed­ną imple­men­tację inter­fe­j­su View, ponieważ Spring nie będzie wiedzi­ał, którą ma wybrać (o czym nas poin­for­mu­je wyjątkiem pod­czas uruchami­a­nia aplikacji). Są jed­nak co najm­niej dwa sposo­by na to, aby powiedzieć Springowi który widok jest ważniejszy od innych.

Pier­wszy to inter­fe­js Ordered, który zaw­iera metodę getOrder. Jeśli w jakiejkol­wiek sytu­acji Spring musi ‚uporząd­kować’ wiele beanów, to o ile imple­men­tu­ją one ten inter­fe­js, jest on wyko­rzysty­wany aby określić kole­jność. Ta meto­da ma jed­nak pewną wadę — wyma­ga od nas imple­men­tacji dodatkowej metody w każdym widoku, na dodatek takiej, która zwraca to samo dla więk­szoś­ci z nich. To zde­cy­dowanie nie jest clean code, i o ile moglibyśmy ‚wyciągnąć’ ją do klasy nadrzęd­nej AbstractView, to sko­rzys­tamy z przy­jem­niejszego rozwiąza­nia.

Jest nim adno­tac­ja @Primary, którą może­my umieś­cić nad całą klasą. Jeśli kiedykol­wiek Spring napot­ka na kon­flikt pod­czas uzu­peł­ni­a­nia zależnoś­ci, i dokład­nie jeden z ‚możli­wych’ beanów będzie posi­adał tą adno­tac­je, to on zostanie wybrany. Wyko­rzys­tamy tą właś­ci­wość do wyświ­etle­nia widoku Main­Menu jako pier­wszego. Deklaru­je­my więc nasze pole w następu­ją­cy sposób:

@Autowired
private View currentView;

oraz tworzymy dwie klasy imple­men­tu­jące inter­fe­js View — Main­Menu oraz GameLev­el, nad obiema umieszcza­jąc adno­tację @Component oraz umieszcza­jąc adno­tację @Primary nad klasą Main­Menu. Imple­men­tacją tych widoków zajmiemy się w dal­szej częś­ci.

Pierwsze pixele

Sko­ro wiemy już, jak będzie wyglą­dała nasza aplikac­ja od strony orga­ni­za­cji kodu, wiemy z jakich tech­nologii będziemy korzys­tać, nie pozosta­je nam nic innego jak zaprząc te wszys­tkie ele­men­ty do wspól­nej pra­cy :) Na ten moment zad­owolimy się potwierdze­niem, że nasza aplikac­ja ‘dzi­ała’ — może­my zmienić widok, wyświ­etlić wybrany widok itp.

Uruchamiamy LWJGL

Pier­wszy krok to oczy­wiś­cie uru­chomie­nie naszego sil­ni­ka graficznego — w tym wypad­ku bib­liote­ki LWJGL. Aby nasza gra dzi­ałała, musimy zainicjować tryb graficzny, a następ­nie uru­chomić pętlę, która będzie odpowiedzial­na za wyświ­et­lanie i obsługę klaw­iszy.

Aby zainicjować tryb graficzny musimy określić m.in. rozdziel­czość ekranu, sposób wyświ­et­la­nia, pro­jekcję itp — póki co skopi­u­jmy poniższy frag­ment, a szczegółowy sposób jego dzi­ała­nia omówimy w kole­jnej lekcji. Potrzeb­ny nam będzie cen­tral­ny punkt aplikacji — obiekt, który będzie zarządzał pętlą wyświ­et­la­jącą wido­ki. Wyko­rzys­tamy do tego klasę Game, która zaw­iera naszą metodę main. Klasa ta także może być beanem i to w niej zaim­ple­men­tu­je­my główną pętlę gry. Aby zro­bić z Game bean Springowy, doda­je­my adno­tację @Component — poz­woli nam to znaleźć za pomocą Springa naszą klasę, a następ­nie uruchami­ać jej metody. Zmody­fiku­jmy więc naszą klasę Game aby wyglą­dała w poniższy sposób:

Naszą grę docelowo będziemy uruchami­ać w try­bie peł­noekra­nowym, potrze­bu­je­my więc jakiegoś sposobu na jej zamknię­cie. Ponieważ nie powin­niśmy po pros­tu zakończyć aplikacji w momen­cie np. naciśnię­cia klaw­isza Esc (być może przed zamknię­ciem musimy wykon­ać jakieś czyn­noś­ci — zwol­nić zaso­by, zapisać stan itp), musimy zbu­dować mech­a­nizm który poz­woli tym zarządzać. Przed chwilą wyko­rzys­tal­iśmy klasę Game do obsłu­gi głównej pętli pro­gra­mu — zatem tam też musimy zaim­ple­men­tować wyłączanie. Zro­bimy to poprzez warunek pętli while i zmi­en­ną typu boolean. Zmi­en­na ta będzie inicjowana wartoś­cią false, i dopó­ki pozostanie ona nieprawdzi­wa, pęt­la będzie dzi­ałała nadal. Meto­da closeGame ustawi wartość zmi­en­nej na false — nie zakończy to dzi­ała­nia aplikacji od razu, ale spowodu­je prz­er­wanie pętli, a więc zakończe­nie aplikacji po wyko­na­niu bieżącej iter­acji do koń­ca. W przyszłoś­ci wyko­rzys­tamy ten mech­a­nizm także do ‚powiadami­a­nia’ innych kom­po­nen­tów o tym, że aplikac­ja będzie zamykana. Klasę Game mody­fiku­je­my w następu­ją­cy sposób (to, co się dzieje w dodanym kodzie omówimy w kole­jnej 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ól­nych widokach.

Budujemy widoki

Na ten moment nasze wido­ki będą real­i­zować dwie funkc­je: na ekranie pojawi się pros­tokąt, dzię­ki które­mu będziemy wiedzieli, że wyświ­et­lany jest konkret­ny widok, oraz będziemy obsługi­wać klaw­isze pozwala­jące prze­chodz­ić do drugiego widoku. Dlaczego nie napisze­my jakiegoś tek­stu? Otóż wyma­ga to trochę więcej pra­cy — musimy wczy­tać czcionkę, zamienić ją na grafikę i następ­nie narysować. Zajmiemy się tym w przyszłoś­ci, a póki co idziemy najszyb­szą drogą do celu — rysu­je­my pros­tokąt.

Rysowanie na ekranie

Pier­wsze, co musimy zro­bić, to określić kolor, którym będziemy rysować. Wywołu­je­my poniższą metodę:

GL11.glColor3f(0.5f, 0.5f, 1.0f);

Kolor wybier­amy poda­jąc jego trzy skład­owe — r, g oraz b, każ­da w zakre­sie od 0 do 1. Jeśli nie wiesz, jak wybrany przez Ciebie kolor jest reprezen­towany, możesz to sprawdz­ić np na stron­ie w3c, pamię­ta­jąc jed­nak aby przeliczyć wartoś­ci RGB dzieląc każdą z nich przez 255.

Następ­nie rysu­je­my ksz­tałt. W OpenGL jed­ną z metod rysowa­nia na ekranie jest tworze­nie wielokątów, które mogą być wypełnione lub samy­mi krawędzi­a­mi itp. Każdy ksz­tałt inicju­je­my wywołu­jąc metodę glBe­gin, następ­nie wywołu­je­my glVertex2f (dla dwuwymi­arowej grafi­ki, może­my w iden­ty­czny sposób tworzyć grafikę trójwymi­arową — meto­da ma w nazwie ‘3’ zami­ast ‘2’) dla każdego z punk­tów poda­jąc jego współrzędne, a na końcu wywołu­je­my glEnd. Narysowanie pros­toką­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śnie­nie jakie są inne dostęp­ne opc­je zna­jdziemy w doku­men­tacji OpenGL — LWJGL stanowi jedynie ‘most’ pomiędzy bib­lioteką OpenGL (napisaną w języku C) oraz kodem w Javie.

Jak pewnie zauważyłaś, wszys­tkie metody, których tutaj uży­wamy, są staty­czne. To jed­na z niewielu sytu­acji, w której metody staty­czne są uza­sad­nione — w sys­temie zawsze będzie jed­na klaw­iatu­ra (w każdym razie nawet jeśli będzie ich więcej, to są obsługi­wane iden­ty­cznie), jeden wyświ­et­lacz itp.

Obsługujemy klawisze

Klaw­iaturę może­my obsługi­wać na dwa sposo­by — pier­wszy to sprawdza­jąc, czy wybrany klaw­isz został naciśnię­ty. To mało wygod­ny sposób, jeśli chce­my zbu­dować uni­w­er­sal­ny mech­a­nizm (każdy nasz widok może korzys­tać z innych klaw­iszy). Dru­gi sposób to uży­cie bufo­ra klaw­iatu­ry — każde naciśnię­cie klaw­isza spowodu­je dodanie odpowied­niego zdarzenia do kole­j­ki, którą następ­nie może­my odczy­ty­wać. Ponieważ jest to bardziej elasty­czne rozwiązanie, zas­to­su­je­my je w naszej grze. Oznacza to, że reje­strowane są wszys­tkie naciśnię­cia klaw­iszy pomiędzy kole­jny­mi wyświ­etle­ni­a­mi ramek, a więc dla każdego jed­nego wywoła­nia metody ren­der­Frame() w naszym widoku, meto­da han­dleKey może być wywołana zero, jeden lub więcej niż jeden raz. W naszym przy­pad­ku nie będzie to robiło różni­cy, miej to jed­nak na uwadze pisząc włas­ną grę — być może będzie to istotne w Twoim przy­pad­ku.

Przyjmi­jmy następu­jące zasady obsłu­gi klaw­iszy na ten moment — przy­cisk Esc na widoku GameLev­el prze­chodzi do głównego menu, z kolei na widoku Main­Menu zamy­ka aplikację. Na widoku Main­Menu ‚enter’ spowodu­je prze­jś­cie do widoku GameLev­el.

Ponieważ LWJGL ope­ru­je na kodach klaw­iszy jako liczbach typu int, dla uproszczenia dodamy enum, w którym zapisze­my wszys­tkie klaw­isze w sposób bardziej czytel­ny. Pon­ad­to obsłu­ga klaw­iatu­ry generu­je dwa zdarzenia — naciśnię­cie klaw­isza oraz jego zwol­nie­nie. Zakładamy, że na ten moment intere­su­je nas tylko zwol­nie­nie klaw­isza (to się zmieni w przyszłoś­ci, ale zmody­fiku­je­my to w swoim cza­sie).

Enum, którego będziemy uży­wać do przekazy­wa­nia infor­ma­cji o klaw­iszach:

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);
    }
}

Imple­men­tac­ja metody loop w View­Man­agerze:

public void loop() {
    while (Keyboard.next()) {
        if (!Keyboard.getEventKeyState()) { //reagujemy tylko na zwolnienie klawisza
            currentView.handleKey(KeyboardKey.findByCode(Keyboard.getEventKey()));
        }
    }
    currentView.renderFrame();
}

Imple­men­tac­ja metody han­dleKey dla Main­Menu:

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

Grat­u­lu­je, napisałaś właśnie dzi­ała­jącą grę ;) Być może fabuła jeszcze nie pory­wa, ale coś się już wyświ­et­la i ogól­nie ‚coś dzi­ała’. W kole­jnych częś­ci­ach będziemy dodawać kole­jne ele­men­ty aż nasza gra będzie w pełni gry­wal­na !

Kod źródłowy tylko do tej lekcji możesz pobrać z repozy­to­ri­um na GitHub lub tam go pode­jrzeć.

Aktu­al­ny kod Footrza­stej zna­jdziesz na naszym repozy­to­ri­um na GitHub.

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.

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.

Zasoby

  •  
  •  
  •  
  •  
  •