Erste Schritte mit LibGDX – Android
Von Bibiana Bayer am 31.01.2015
LibGDX ist ein Open Source Gaming Framework und erlaubt Game Development für iOS, Android, Blackberry und HTML5. Laut Homepage beinhaltet LibGDX nicht nur eine Physics Engine sondern liefert auch wichtige Tools für effektives Game Development, wie einen z.B. TexturePacker. Außerdem ist die Community riesig und die Dokumentation umfangreich und ausgiebig – deshalb kein Wunder, dass ich mich bei der Suche nach einem passenden Framework für mein Android Game für LibGDX entschieden habe. In dem Artikel werde ich mich hauptsächlich mit den ersten Schritten beschäftigen die für ein Game-Development nötig sind: erkennen der Struktur des Frameworks und Aufbau eines Interfaces mit einem kleinen Einblick in Animationen. Unten findet ihr noch weitere praktische Links bezüglich Gesturedetection oder MapRendering.
LibGDX in Android Studio
Wie oben erwähnt beschäftigt sich dieser Artikel ausschließlich mit LibGDX für Android. Der Erstellungsvorgang der Projektdateien zur Integration in Android Studio ist denkbar einfach. Von der Homepage erhält man eine .jar Datei – welches die ganze Arbeit bezüglich Ordnerstruktur erledigt.
Box2d ist die Physics Engine die von LibGDX mitgeliefert wird. Sollte man im Nachhinein noch weitere Extensionen hinzufügen, kann man das ganz einfach über das Gradle File in Android Studio erledigen. Später mehr dazu! Nachdem die wunderbare Ordnerstruktur im Ordner der Wahl erstellt wurde, kann man im Android Studio über File->Import Projects das Projekt importieren. Einfach in der Ordnerhierachie im Ordner libs build.gradle auswählen – fertig. Die App sollte bereits auf dem Smartphone laufen – sieht zwar (noch) nicht sehr sexy aus aber ein rascher Anfang!
Game Aufbau – erste Schritte
Eins soll dem Java-Programmierer hier klar sein: der normale App-Aufbau mit Activities usw. findet mit LibGDX nicht statt. Hier wird das Game vielmehr in einzelne Screens aufgeteilt, die wiederum den Spieleinhalt je nach Framerate rendern. Aber eins nach dem anderen.
Vorgehensweisen variieren hier – genaue Vor- und Nachteile der einzelen Herangehensweisen sind mir nicht ganz klar geworden, ich denke es hängt hier einfach vom Programmierstil ab. Ich habe mich dafür Entschieden eine Art Main zu erstellen welches die diversen Screens beinhaltet und sich um den Screenwechsel kümmert. Die Screens wiederum kümmern sich um die Darstellung, Rendern und wenn nötig um die Spielelogik. Natürlich kann man das je nach Spielekomplexität anders schachteln – bei einigen wird vielleicht die Main-Klasse nicht nötig sein.
Ein grober Aufbau wäre:
Wichtig: Eure Main Klasse muss von der Klasse Game erben und erhält so die Funktion create – in der sich dann auch der Einstieg in euren Programmiercode befinden wird. Hier ist ein Beispiel für die Funktionen des Screen-Wechsels, dazu wird einfach die Methode setScreen der Game-Klasse verwendet. Zur statischen Klasse AssetLoader kommen wir später.
public class GameMain extends Game { private StartScreen startScreen; private GameScreen gameScreen; private IntroductionScreen introductionScreen; @Override public void create() { AssetLoader.load(); //load all assets!! this.setStartScreen(); } public void setStartScreen() { startScreen = new StartScreen(this); setScreen(startScreen); } public void setGameScreen() { gameScreen = new GameScreen(this); setScreen(gameScreen); } }
Eure verschiedenen Screens – also zum Beispiel der Anfangs-Screen, der Extras-Screen und natürlich der InGame-Screens, müssen das Interface Screen implementieren – denn das liefert euch gleich die sechs heiligen Methoden des Game-Development mit LibGDX:
- show() – wird aufgerufen wenn dieser Screen durch setScreen aufgerufen wird – hier sollte der Grundaufbau der graphischen Elemente stattfinden
- render() – wird jedes Frame aufgerufen und rendert die Grafiken – es kann immer nur ein Screen gleichzeitig gerendert werden. In die ersten Zeilen der render() Methode gehören diese zwei kryptischen Zeilen (diese dienen zum löschen des Screens vor dem erneuten Zeichnen – und in diesem Fall würde der Hintergrund blau gemalt werden):
Gdx.gl.glClearColor(34 / 255f, 187 / 255f, 255 / 255f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
- pause() – wird aufgerufen, wenn die App in den Hintergrund gelegt wird (sollte man auch händisch aufrufen, wenn man das Spiel manuell pausiert)
- resume() – wird aufgerufen, wenn die App vom Hintergrund wieder in den Vordergrund gerufen wird (sollte man auch händisch aufrufen, wenn man das Spiel manuell fortsetzt)
- hide() – wird aufgerufen, wenn durch die setScreen Methode ein anderer Screen aufgerufen wird
- resize()
- dispose() – wird von selbst nicht aufgerufen – sollte aufgerufen werden wenn der Screen alle Elemente freigeben soll (Stages, Tables, Sprites)
Wie man sich denkt ist während eines laufenden Spiels das Herzstück des Programmiercodes die render() Methode der GameScreen Klasse (oder wie auch immer euer In-Game Screen heißen mag).
Sprites, Buttons, Skins, … – alles rund um ums Interface
Das Look’n’Feel ist bei einem Game natürlich das A und O. LibGDX liefert mit dem Nightlies build einige nützliche Tools rund ums Interface und Grafik erstellen. Beginnen wir mit einigen wichtigen Elementen.
Exkurs AssetLoader
Bevor wir mit dem Erstellen von Grafiken und Buttons weitermachen, kommen wir zu der mysteriösen Code-Zeile
AssetLoader.load();
in der oben gezeigten GameMain Klasse. Wie gewohnt sind beim Android-Development Assets alles “externe” was währen der Laufzeit der Applikation verwendet werden soll. Grafiken, text, xml, json, videos und quasi alles Multimedia-mäßige gehört in den asset-folder der Projektes. Der AssetLoader ist meine selbst erstellte statische Klasse, die am Anfang alle für das Spiel benötigten Assets in eine statische Variable lädt – damit man einfach von überall aus auf diesen asset zugreifen kann. Ein einfaches Beispiel wäre:
tree = new TextureRegion(graphic_atlas.findRegion("tree"));
Nun habe ich die Möglichkeit in jeder meiner Klassen mit AssetLoader.tree einen Baum zu zeichnen. Quasi alle Elemente, die wir nachher erstellen werden, werden mit dem AssetLoader geladen. Mehr dazu jedoch später.
Bitmap Font
Um alle Schriftarten ohne Probleme anzeigen zu können werden Bitmap Fonts verwendet. Hierfür gibt es diverse Tools. Diese erstellen im Prinzip ein großes Bild aller zu verwendeten Zeichen und einem File, in dem steht bei welchen Koordinaten welches Zeichen zu finden ist. Im LibGDX Wiki werden einige Tools empfohlen – unter anderem Hiero oder GlyphDesigner. Ich habe mich für Hiero entschieden – verschiedene Möglichkeiten es zum Laufen zu bringen (sei es über die Konsole oder über Gradle) findet man hier.
Das Erstellen der Font ist denkbar einfach: die Zeichen, die benötigt werden auswählen, Größe einstellen, Farbe auswählen und eventuelle Effekte hinzufügen, speichern und fertig. Das .png File und das .fnt File müssen natürlich beide sicher in den asset Ordner der Projekts abgelegt werden – damit die Applikation auch darauf zugreifen kann.
Skins
Um unsere Bitmap Font nun in Labels und Buttons zu verwenden, erstellt man im besten Fall einen Skin für alle Styles, die man verwenden will. Ein Skin ist im Prinzip eine JSON-Datei in der alle Styles definiert werden. Als erstes erstellt man eine BitmapFont, hernach kann man Farben definieren, dann gewünschte Labels und Buttons:
{ "com.badlogic.gdx.graphics.g2d.BitmapFont": { "cooperBlackSmall": { "file": "fonts/cooperBlack_small.fnt" }, }, "com.badlogic.gdx.graphics.Color": { "white": { "r": 1, "g": 1, "b": 1, "a": 1 }, "black": { "r": 0, "g": 0, "b": 0, "a": 1 }, }, "com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle": { "textWhiteSmall": { "font": cooperBlackSmall ,"fontColor": white }, }, "com.badlogic.gdx.scenes.scene2d.ui.TextButton$TextButtonStyle": { "menuButton": { "up": menu_button_blue_normal, "down": menu_button_blue_pressed, "font": cooperBlackSmall}, }, }
Die JSON-Datei wird in den asset Ordner gespeichert. Wie wir zu den Grafiken für die Button-States kommen wird im nächsten Teil erklärt – also keine Sorge.
Buttons und Labels
Für einen Button fehlt uns noch etwas essentielles: die Grafiken. Für die simpelste Version wären dies zwei Grafiken: eine für den Button selbst und eine für den “down”- Modus – also wenn dieser geklickt oder getoucht wird. Damit sich die Buttons, dann auch optimal an den Text anpassen können, sollte man die Grafiken vorerst einmal 9patchen. Dies bedeutet, dass man die Bereiche definiert, welche im Button dehnbar sein sollten und auch die Bereiche in denen Inhalt stehen darf. Die Android-sdks kommen mit einem einfachen grafischen Tool dafür. Es befindet sich im sdk-Ordner/tools/draw9patch und sieht in etwa so aus:
Die schwarzen Linien an der oberen und der linken Seite definieren den Dehnbereich die Linien an der rechten und unteren Seite den Content-Bereich. Wenn man alles Speichert sollten die Grafiken die Endung 9.png haben. Der nächste Schritt wäre nun, beide Grafiken bzw. alle Interface Grafiken mit einem Texturepacker in ein großes .png zu packen (ähnlich wie bei der Bitmap-Font). Dies bietet performance- und speichertechnisch große Vorteile. Anstatt tausend Grafiken laden zu müssen, lädt die Applikation nur die gepackte Datei und erhält von einer externen Datei wieder die Positinen der einzelnen Grafiken im Bild. Auch einen TexturePacker findet man in der Nightlies-Distribution von LibGDX. Da bei mir die Graphishe Oberfläche allerdings nicht funktionier hat, habe ich das Ganze dann über die Konsole gemacht. Man könnte auch über Gradle die Tools zur IDE hinzufügen und von dort aus laufen lasse. Eine genaue Beschreibung wie ihr eure Grafiken mit dem TexturePacker packt findet ihr hier. Herauskommen sollte eine Text-Datei meist mit der Endung .pack oder .atlas und eine .png-Datei, die in etwa so aussehen könnte:
Ab damit in den asset-Folder! Weiter geht es mit dem Laden des TextureAtlas und des Skins im AssetLoader:
TextureAtlas buttons_atlas = new TextureAtlas("skins/buttons_stuff.pack"); Skin skin = new Skin(Gdx.files.internal("skins/skin.json"),buttons_atlas);
Da wir nun unsere Styles in einem Skin definiert haben, die Grafiken in mit einem Texturepacker gepackt, die passende Schrift als Bitmap-Font gespeichert und alles im AssetLoader geladen haben, können wir endlich loslegen. Ein Label wird zum Beispiel folgendermaßen instanziert:
private Label highscore_label = new Label("Highscore: 40",AssetLoader.skin,"textWhiteSmall");
Unser Highscore-Label wird als Klassenvariable instanziert, der erste Parameter ist der Text, der zweite greift auf unser JSON-Skin-File über den AssetLoader zu und der dritte Parameter zeigt auf den Namen des Styles den wir im Skin definiert haben. Ein Beispiel für einen Funktionierenden Button hätten wir hier:
private TextButton buttonPlay = new TextButton("Go!", AssetLoader.skin,"menuButton");
Damit der Button allerdings noch klickbar wird müssen wir zwei Dinge tun. Erstens: In dem Konstruktor des Screens müssen wir angeben, dass die Stage (Erklärung zu Stages findet ihr im nächsten Abschnitt) auf die der Button hinzugefügt wird, auf die Eingaben des Users reagieren soll.
Gdx.input.setInputProcessor(stageName);
Zweitens: einen ActionListener auf den Button hängen (meist in der show() Methode):
buttonPlay.addListener(new ClickListener(){ @Override public void clicked(InputEvent event,float x, float y) { //HIER DEIN CODE WENN AUF DEN BUTTON GEKLICKT WURDE } });
Anordnen der Elemente
Die Andordnung der Elemente erfolgt durch Stages und Tables. Die Elemente werden in einem Table angeordnet – dieser übernimmt dann die Ausrichtung am Screen des Smartphones. Das Table wird wiederum zur Stage hinzugefügt. Der Aufbau und die Anordnung erfolgt meist in der show() Methode des jeweiligen Screens. In der render() Methode wird das Ganze dann noch in jedem Frame gerendert bzw. gezeichnet. Will ich jetzt zum Beispiel ein Label und einen Button untereinander in der Mitte des Screens anordnen, könnte das so aussehen:
startTable.add(highscore_label).padBottom(30).row(); startTable.add(buttonPlay).width(500).padBottom(30).row(); startTable.setFillParent(true); startStage.addActor(startTable);
Es gibt diverse Funktionen für die Ausrichtung und Anordnung der Elemente. row() schließt eine Zeile ab. Weiteres zum Table findet man hier. Der obere Code-Auszug würde in der show() Methode stehen. Damit man den Aufbau auch dann zur Laufzeit bewundern kann müsste man noch in die render() Methode folgendes schreiben:
startStage.act(); startStage.draw();
Animations
Da wir nun wissen, wie man statische Elemente zeichnet, kommen wir zum spannenden Teil – die Animationen. Einface Animationen – zb eine konitnuierliche Fortbewegung entlang der x-Achse erfolgt in der render() Methode indem man zu dem jeweiligen Element einfach immer einen bestimmten Wert zur x-Position dazuaddiert. Allerding sollten dynamische Elemente nicht mit einem Table positioniert werden sondern über einen sogenannten Spritebatch immer neu gezeichnet werden. Auch dies erfolgt natürlich jedes Frame und steht deshalb in der render() Methode des jeweiligen Screens. Hier ein einfaches Beispiel:
batch = new SpriteBatch(); //instanzierungen erfolgen natürlich im Konstruktor tree = new Sprite(new TextureRegion(AssetLoader.tree)); tree.setPosition(400,400);
batch.setProjectionMatrix(camera.combined); //ab hier alles in der render() Methode batch.begin(); tree.setPosition(tree.getX()+5,tree.getY()); tree.draw(batch); batch.end();
Die ersten drei Zeilen dienen der Definition des SpriteBatches und einem Sprite. Der Sprite zeigt eine TextureRegion aus einem TextureAtlas an. Dies bedeutet einfach er zeigt auf eine spezielle Grafik in einem gepackten .png. Praktischerweise können wir alle unsere einzelnen Grafiken (besonders wenn man sie öfter als einmal verwendet) vorher im AssetLoader laden:
TextureAtlas graphic_atlas = new TextureAtlas("skins/gameGraphics.pack"); tree = new TextureRegion(graphic_atlas.findRegion("tree"));
Zuerst wir die gesamte .atlas oder .pack Datei geladen und nachher mit der Funktion .findRegion kann die gewünschte Region bzw. das gewünschte Einzelbild gefunden werden. Der Name in der Klammer bezieht sie auf den Dateinamen der Grafik bevor sie gepackt wurde.
Bevor gezeichnet wird muss der Batch auf die Kamera ausgerichtet werden. In LibGDX benötigt jedes Spiel eine Kamera – sie zeigt auf einen speziellen Teil der gesamten Spielewelt – eben das was man am Smartphone dann auch sieht. Mehr zu Kameras findet ihr hier. Danach wird mit begin() der Anfang der Zeichensequenz eröffnet. Der Baum wird um jeweils 5 Einheiten auf der x-Achse verschoben und dann gezeichnet. Nicht auf das Beenden der Sequenz mit .end() vergessen!
Sprite Animations
Da aber nicht immer eine einfache Verschiebung eines Elemente für realistische Ergebnisse sorgt ist es auch wichtig, die Elemente selbst animieren zu können. Soll eine Figur laufen können ist natürlich der erste Schritt alle benötigten Grafiken einer Gehbewegung zu malen und diese zu packen. Im primitivsten Fall wären dies zwei Grafiken:
Diese zwei Grafiken müssen auch im AssetLoader wieder als TextureRegion definiert werden. Nun zur eigentlichen Animation. Man benötigt ein TextureRegion Array in dem alle States der Animation gespeichert werden. In diesem Fall hätte das Array eine Länge von 2. Hernach muss die Animation definiert werden – als Parameter werden die gewünschte Framerate (also Schnelligkeit) und das Array übergeben.
walkFrames = new TextureRegion[2]; walkFrames[0] = AssetLoader.pug_walking1; walkFrames[1] = AssetLoader.pug_walking2; walkAnimation = new Animation(0.25f,walkFrames);
In der render() Methode des jeweiligen Screens kann dann gezeichnet werden, den Keyframe der angezeigt werden soll errechnet sich LibGDX aus der vergangenen Laufzeit des Spiels:
batch.start(); batch.draw(walkAnimation.getKeyFrame(Gdx.graphics.getDeltaTime(),true),posX,posY); batch.end();
Andere interessante Features – weiterführende Links
Mir ist klar, dass dieser Artikel nur der allererste kleinste Schritt in Richtung Game-Development ist. Es geht vorwiegend um die Interfacegestaltung und das Verständnis des Grundgerüsts von LibGDX. Deshalb liste ich hier noch einige Tipps und Tricks und Tutorials die mir sehr weitergeholfen haben:
Sollte man auch Schriften, Buttons, einfache Sprites mit Tween-Animations versehen wollen, stößt man nur mit LibGDX leider an Grenzen. Dafür empfehle ich die Klasse Tween Universal, welche einfach über Gradle zu LibGDX hinzuzufügen ist und auf JEDE! Klasse anwendbar ist. Eine Beschreibung für die Integration in euer LibGDX Projekt findet ihr hier und ein Tutorial für die Anwendung mit LibGDX Objekten hier.
Wer Lust hat einen Shooter oder ein Jump’n’Run zu programmieren sollte auf die Physics Engine Box2D nicht verzichten. Ein super Tutorial zum Einstieg damit findet ihr hier.
Was wäre ein Android-Game ohne gute und effiziente Gestenerkennung. Die Doku dafür ist zwar super, doch wer kein großer Mathematiker ist kann sich beim Ausrechnen der swipe-Gesten schnell verlieren. Hier ist ein großartiger Post darüber wie man schnell erkennen kann ob eine Swipegeste nach oben, unten links oder rechts ausgeführt wurde und bietet eine tolle Grundlage dafür noch komplexere Gestenerkennung umzusetzen.
Eine tolle Seite für allerhand gute Tutorials bezüglich LibGDX: http://libdgxtutorials.blogspot.co.at/
Auch eine sehr tiefgehende Tutorialreihe (inklusive TileMaps mit Level-Editor!) mit ausführlichen Erklärungen: http://www.gamefromscratch.com/page/LibGDX-Tutorial-series.aspx
1 Kommentar
Hej du erklärst das ziemlich gut 🙂
könntest du mir vielleicht sagen oder einen Code zeigen, der ein Menü darstellen soll(einfach ein oder zwei button). Wenn man auf einen Button klickt soll ein anderer Screen angezeigt werden. Wäre sehr dankbar
The comments are closed.