RSS

09.05.2014

libGDX - den DebugRenderer ersetzen: Projektionsmatrizen

TL;DR die OrthographicCamera rechnet uns mit den project() und unproject() Methoden ganz von alleine die Welt-Koordinaten in Screen-Koordinaten um und zurück.

Um Animationen und Sprites an der korrekten Stelle zu zeichnen empfehle ich 2 SpriteBatches, von denen einer mit worldBatch.setProjectionMatrix(camera.combined); für die automatische Umrechnung von Welt auf Screen-Koordinaten genutzt wird.


Mit dem DebugRenderer von Box2D lassen sich schnell und leicht die Entities anzeigen, damit hat man zumindest einen guten Überblick.

Sobald man nun aber Texturen auf die rohen Skelette legen will, muss man feststellen, dass die SpriteBatches Pixel Werte verlangen, relativ zum Bildschirm, Box2D nutzt aber Meter als Einheit. Nun bin ich mir nicht sicher, ob ich einfach die Doku nicht genau gelesen hatte, aber die Tatsache ist: ich habe dann doch viel zuviel Zeit in die händische Umrechnung von Worldunits (also Meter) in Pixel-Koordinaten auf dem Screen vergeudet. Leider war Google auch wenig hilfreich, denn immer wieder liest man von der manuellen Umrechnung mit PIXEL_PER_METER Faktoren etc. etc.

Die erfreuliche Nachricht: libGDX will unser Freund sein und nimmt uns das alles ab!

Nachdem ich nochmal die Doku gewälzt hatte, fielen mir die project/unproject Methoden der orthographischen Kamera in die Hände. Der erste Satz aus den Java-Docs war vielversprechend: "Projects the Vector3 given in world space to screen coordinates."

Genau das nachdem ich so lange gesucht hatte. Also wurde kurzerhand das Renderable Interface um die Kamera und einen SpriteBatch erweitert, und es wurde ganz problemlos die Demo-Animation an der richtigen Stelle gerendert.

Da der coole Teil noch kommt, hier nur im Groben wie project() funktioniert.

//screenPosition = Vector3
screenPosition.set(body.getPosition().x, body.getPosition().y , 0);
//calculate screen coordinates
camera.project(screenPosition);
batch.begin();
batch.draw(currentFrame, 
        screenPosition.x, 
        screenPosition.y, 
        width, 
        height );
batch.end();

Nun haben wir aber in einem realen Fall mehrere Entities auf dem Screen. Die oben gezeigte Methode funktioniert auch damit, kam mir aber nicht sehr komfortabel vor (wenn auch sehr viel eleganter als die manuelle Berechnung zuvor), denn wir brauchen für jede Entity einen zusätzlichen Vektor in dem wir für jeden Frame in jeweils 2 Schritten die Pixel-Koordinaten berechnen müssen.

Schon zuvor habe ich den Code des DebugRenderers durchforstet, weil ich wissen wollte, wofür dieser dieses camera.combined benötigt.

//camera.combined = the combined projection and view matrix 
debugRenderer.render(world, camera.combined);

Nach genauerem Nachlesen und dem weiteren Durchsuchen der Doku und der Java-Docs habe ich dann den "korrekten" Weg gefunden. Das erwähnte camera.combined ist eine 4x4 Matrix die alle Transformations-Daten über unsere Kamera und Welt enthält. Wer sich mit dem Thema etwas genauer auseinandersetzen möchte, dieser Artikel hat mir sehr beim Verständnis geholfen.

Der SpriteBatch bietet die Funktion setProjectionMatrix an. Damit können alle unsere Welt-Objekte automatisch auf unsere Kamera projeziert werden. Da ich eine Möglichkeit gefunden habe die gesetzte Projektionsmatrix wieder zu entfernen, wurden alle UI Elemente wie z.B. die Meteranzeige ebenso projeziert, also irgendwo weit außerhalb des sichtbaren Bereichs gezeichnet. Um dieses Verhalten zu umgehen habe ich einen zweiten SpriteBatch in meinem GameScreen angelegt, also benutze ich einen screenBatch und einen worldBatch. Mit dem screenBatch werden UI Elemente und Hintergründe gezeichnet, und mit dem worldBatch lassen wir uns von libGDX alle Box2D Entities an die korrekte Screenposition zeichnen.

//gameScreen
@Override
public void render(float delta) {
	screenBatch.begin();
        // drawing backgrounds
        background.render(delta, screenBatch, camera);
        //fonts
        font.draw(screenBatch, highscore + " m", 20, hardwareHeight);
        screenBatch.end();

	worldBatch.setProjectionMatrix(camera.combined);
	//render all box2D Entities
}

Da der SpriteBatch eines der schwereren Objekte ist, empfiehlt es sich aus Sicht der Performance die beiden Objekte einmal zu erstellen und dannach in alle render Aufrufe zu übergeben. Dabei können innerhalb eines render-Loops beliebig oft begin() und end() aufgerufen werden.

Ich werde immer begeisterter von libGDX! Wohoooo!


30.04.2014

Der Maulwurf lebt!

Noch ohne Namen aber mit freundlichem Gesicht. Wir präsentieren: unseren Protagonisten!


21.04.2014

libGDX Entity-Klassen

Vorweg:

libGDX bezeichnet sich als "Java game development framework", es ist also keine fertige Spiele-Engine und bietet für die einzelnen Aufgaben keine fixe Lösung, sondern lässt dem Entwickler viel Freiheit, bietet aber für jede Entscheidung die richtigen Werkzeuge. Daher sind alle beschriebenen Implementierungsdetails auch nur eine Lösung von Mehreren, und mehr als Vorschlag als DER einzig richtige Weg zu sehen.

Entities

Da wir es mit einer eher überschaubaren Anzahl an Objekten in der Welt zu tun haben, habe ich mich gegen ein fertiges Entity Framework wie zB Artemis enschieden, unter Anderem auch, da wir Box2D einsetzen, und schon eine physikalische Welt mit unseren Objekten existiert.

Das Ziel: ein Aufbau bei dem ich ohne weiteres Eingreifen auf eine render() Funktion in meiner Entity Zugriff habe und auf meine Spielewelt zeichnen kann.

Dazu habe ich eine kleine Klasse geschrieben, die mir alle Daten zusammenhält die ich sowieso in jeder Entity brauche

public abstract class Entity implements Renderable{
    protected World world;
    protected Body body;
    protected Fixture fixture;
    public Entity(World _world){
        world = _world;
    }
    protected void createBody(BodyDef bodyDef){
        body = world.createBody(bodyDef);
        body.setUserData(this);
    }
    protected void createFixture(FixtureDef fixtureDef){
        fixture = body.createFixture(fixtureDef);
        fixture.setUserData(this);
    }
}

Die Referenz zur Box2D Welt, sowie den Body und die Fixture brauche ich in jedem Fall für die spätere Kollisionsabfrage. Damit ich dann über die Fixtures auch wieder Zugriff auf das Entity Objekt habe, wird das komplette Objekt in den dafür vorgesehenen Platzhalter "userData" geschrieben.

Das Interface "Renderable" zwingt dabei nur die erbende Klasse, meine render() Funktion zu implementieren.

Die Welt selbst wird in meinem GameScreen be - und verarbeitet. Dafür hole ich mir alle Bodies aus der Welt, und prüfe ob das Renderable Interface vorhanden ist.

public class GameScreen implements Screen {

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
    
        //rendering bodies
        Array bodies = new Array();
        world.getBodies(bodies);
        for (Body b : bodies) {
            Object userData = b.getUserData();
            if (userData != null) {
                // call all render methods from renderable bodies
                if (userData instanceof Renderable) {
                    ((Renderable) userData).render( );
                }
            }
        }
        // TODO remove debug renderer 
        debugRenderer.render(world, camera.combined);

        // step phisycal world
        world.step(1 / 45f, 6, 2);
    }
}

Der debugRenderer ist hier ein eigener Renderer von Box2D, der die Entities nur anhand der Fixtures zeichnet. Nicht hübsch, aber man kommt schnell zu einem Ergebnis.

Da in libGDX es sowas wie eine Game-Loop nicht gibt, werden auch alle Berechnungen und Logik-Updates in der render() Funktion gehandelt. Im Einsatz könnte es also so aussehen:

public class Player extends Entity {
    public Player(World _world) {
        super(_world);
        
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyType.DynamicBody;
        bodyDef.position.set(0, 1);

        createBody(bodyDef);
        
        CircleShape circle = new CircleShape();
        circle.setRadius(2);

        FixtureDef fixtureDef = new FixtureDef();
        fixtureDef.shape = circle;
        fixtureDef.density = 0.5f;
        fixtureDef.friction = 0.4f;

        createFixture(fixtureDef);

        circle.dispose();
    }
    
    @Override
    public void render() {
        //wohooo, i can do all my stuff
    }
    
}

Wunderbar. Ich bin zufrieden, glücklich und kann wunderschöne Entity Klassen bauen :-)


11.04.2014

UUAA gestartet

Wie schon angekündigt, wird hier ausschließlich über unser neues Spieleprojekt "Up, up and away" geschrieben.

Die Idee: eine Mischung aus Icy Tower, etwas NinJump und vielleicht ein ganz kleines bisschen Doodle Jump .

Für unsern Protagonisten wollen wir im Tierreich bleiben. In der engeren Auswahl standen:

  • Pangolin
  • Ameisenigel
  • "Eigentlicher Greifstachler"
  • Maulwurf

Am Ende konnte ich mich (und meine Frau) mit der Maulwurf-Idee durchsetzen. Erste Skizzen existieren auch bereits

Für die Umsetzung habe ich mich für libGDX entschieden, ein Cross-Plattform OpenGL Framework. Im Gegensatz zu vielen anderen Cross-Plattform Compilern, bietet libGDX die Möglichkeit, eine komplett native App um das Spiel zu bauen, also jeweils Plattform-spezifische Menüs, Highscores etc. Beim Starten des eigentlichn Spiels, wird eine View erzeugt in der das Spielt ausgeführt wird.

Das erste Ziel wird: eine hübsche Android App mit nativen Menüs und Highscores.


error success