Programmierung einer Spiele-App

Wie programmiert man eine Spiele-App in Java?

In diesem Beispiel zeige ich, wie eine Spiele-App für Android in Java programmiert wird. Zunächst erstellen wir ein Layout in der Datei activity_main.xml, bestehend aus zwei Schaltflächen -Buttons- und dem Spielfeld. Die Buttons sollen nebeneinander über dem Spiefeld erscheinen. Dazu werden sie in ein horizontales LinearLayout gepackt. Dieses LinearLayout packen wir zusammen mit der Spielfläche in ein übergeordnetes LinearLayout mit vertikaler Ausrichtung. Das Spielfeld nennen wir zeichenFlaeche.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <Button
            android:id="@+id/buttonLinks"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="4dp"
            android:layout_marginTop="20dp"
            android:text="New Object" />

        <Button
            android:id="@+id/buttonRechts"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="4dp"
            android:layout_marginTop="20dp"
            android:text="Button 2" />
    </LinearLayout>

    <de.siebenberge.beispiel.ZeichenAnsicht
        android:id="@+id/zeichenFlaeche"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

Wir erstellen eine Klasse ZeichenAnsicht. Diese Klasse verwenden wir, um auf die Zeichefläche zu zeichnen. Hier stellen wir später unsere Spielobjekte dar. Momentan wird einfach nur eine blaue Kreisfläche in die Mitte gezeichnet. Die Klasse Zeichenansicht verwendet die Datei ZeichenAnsicht.java.


package de.siebenberge.beispiel;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class ZeichenAnsicht extends View {

    private Paint pinsel;

    // Konstruktoren, die Android Studio benötigt, um die View im Layout anzuzeigen
    public ZeichenAnsicht(Context context) {
        super(context);
        init();
    }

    public ZeichenAnsicht(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        pinsel = new Paint();
        pinsel.setColor(Color.BLUE); // Beispielfarbe für das Zeichnen
        pinsel.setAntiAlias(true);
        pinsel.setStrokeWidth(5f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // Hier wird gezeichnet!
        // Hintergrundfarbe der Zeichenfläche:
        canvas.drawColor(Color.LTGRAY);

        // Beispiel: Ein Kreis in der Mitte der Fläche zeichnen
        float x = getWidth() / 2f;
        float y = getHeight() / 2f;
        float radius = 100f;

        canvas.drawCircle(x, y, radius, pinsel);
    }
}
  

Nun kommen wir noch zur MainActivity. Wir definieren meineZeichenflaeche vom Typ ZeichenAnsicht und die Schaltflächen btnLinks und btnRechts vom Typ Button. In onCreate weisen wir dem ContentView unsere activity-main zu. Dann folgen die Zuweisungen für meineZeichenflaeche und die Schaltflächen. Sonst passiert zunächst noch nichts weiter. Dies werden wir in Beispiel 2 ändern, welches demnächst folgt.


package de.siebenberge.beispiel;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private ZeichenAnsicht meineZeichenFlaeche;
    private Button btnLinks, btnRechts;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        meineZeichenFlaeche = findViewById(R.id.zeichenFlaeche);
        btnLinks = findViewById(R.id.buttonLinks);
        btnRechts = findViewById(R.id.buttonRechts);
        
    }
}
Beispiel1
So sieht der Bildschirm nun aus.
Oben die zwei Schaltflächen, darunter die Zeichefläche mit einem blauen Punkt.
Im nächsten Beispiel kommt dann Bewegung ins Spiel!

Jetzt kommt Bewegung ins Spiel

Wir haben nun gesehen, wie wir auf die zeichenFläche zeichnen können. Sobald die View ZeichenAnsicht das erste Mal auf dem Bildschirm angezeigt wird, löst Android das onDraw-Ereignis aus und übergibt dabei ein Canvas, also eine Leinwand. Auf diese Leinwand können wir nun zeichen. Im ersten Beispiel haben wir zunächst den Hintergrund komplett in hellgrau gestrichen (canvas.drawColor(Color.LTGRAY);). Dann haben wir die Mitte der Leinwand ermittelt und dort einen Kreis mit Radius 100 Pixel gezeichnet. Zum Zeichnen haben wir einen Pinsel verwendet, der in init() definiert wurde. Um nun Bewegung ins Spiel zu bringen, müssen wir nur die Werte für x und y regelmäßig verändern und dafür sorgen, dass onDraw auch regelmäßig ausgelöst wird.

Da wir später mehrere Objekte im Spiel bewegen wollen, definieren wir erst mal eine neue Klasse Objekt:.


package de.siebenberge.beispiel;

import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.Log;

public class Objekt {
    public static int id = 0;   //Diese Variable wird später inkrementiert und
    //und jedem neuen Objekt als myId zugewiesen
    private int groesse = 20;
    private PointF position = new PointF(1f, 1f); //aktuelle Position des Objekts
    private Point speed = new Point(1, 1); // und Geschwindigkeit in Pixel/Sekunde
    private Objekt nextObject = null;           // Zeiger auf weiteres Objekt
    //So können wir mehrere Objekte nacheinander
    //abarbeiten
    private int myId;
    private int col = Color.RED;           //Farbe des Objekts

    // Zum Erzeugen eines neuen Objektes, wird Startosition, Geschwindigkeit, Groesse und Farbe
    //gleich mitgegeben
    public Objekt(PointF pos, Point speed, int groesse, int col) {
        this.position = pos;
        this.speed = speed;
        this.groesse = groesse;
        this.myId = id++;
        this.col = col;
    }

    public int getCol() {
        return col;
    }

    public void setNextObject(Objekt nextObject) {
        this.nextObject = nextObject;
    }

    public int getMyId() {
        return this.myId;
    }

    public Objekt getNextObject() {
        return this.nextObject;
    }

    public void setSpeed(Point speed) {
        this.speed = speed;
    }


    public Point getSpeed() {
        return this.speed;
    }

    public void setPosition(PointF pos) {
        this.position = pos;
    }

    public PointF getPosition() {
        return this.position;
    }

    public void update(float deltaTime) {
        // Neue Position berechnen: Position = Position + (Geschwindigkeit * Zeit)
        // Da Point nur int nimmt, runden wir das Ergebnis
        this.position.x += this.speed.x * deltaTime;
        this.position.y += this.speed.y * deltaTime;
        //Log.d("Beispiel", "MyId = " + this.myId);

        // Wichtig: Wenn es ein nächstes Objekt gibt, das auch updaten!
        if (this.nextObject != null) {
            this.nextObject.update(deltaTime);
        }
    }


    // Eine Hilfsmethode, um alle Objekte auf das Canvas zu zeichnen
    public void draw(android.graphics.Canvas canvas, android.graphics.Paint pinsel) {
        // Kreis an der aktuellen Position zeichnen
        pinsel.setColor(this.getCol());
        canvas.drawCircle(this.position.x, this.position.y, this.groesse * 1f, pinsel);

        // Nächstes Objekt zeichnen
        if (this.nextObject != null) {

            this.nextObject.draw(canvas, pinsel);
        }
    }
}

Jetzt haben wir die Möglichkeit, mehrere Objekte mit unterschiedlichen Eigenschaften zu erzeugen. Mit jedem Klick auf den linken Button soll ein weiteres Objekt erzeugt werden. Dazu erweitern wir MaunActivity.java:


package de.siebenberge.beispiel;

import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private ZeichenAnsicht meineZeichenFlaeche;
    private Button btnLinks, btnRechts;
    private Objekt firstObject, aktObject, lastObject;
    private float posX, posY;
    private boolean isRunning = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_rettung);

        meineZeichenFlaeche = findViewById(R.id.zeichenFlaeche);

        btnLinks = findViewById(R.id.buttonLinks);
        btnRechts = findViewById(R.id.buttonRechts);
        btnLinks.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                posX = meineZeichenFlaeche.getWidth() / 2f;
                posY = meineZeichenFlaeche.getHeight() / 2f;
                float x, y;
                int speedX, speedY, groesse, col;
                speedX = Zufall.getZufall(-200, 200);
                speedY = Zufall.getZufall(-200, 200);
                groesse = Zufall.getZufall(20, 200);
                col = Zufall.getRandomColor(0, 240);

                x = Zufall.getZufall(posX * .9f, posX * 1.1f);
                y = Zufall.getZufall(posY * .9f, posY * 1.1f);
                if (firstObject == null) {
                    firstObject = new Objekt(new PointF(x, y), new Point(speedX, speedY), 100, col);
                    aktObject = firstObject;
                    lastObject = firstObject;
                    if (!isRunning) {
                        meineZeichenFlaeche.starteThread(firstObject);
                        isRunning = true;
                    }
                } else {
                    aktObject = lastObject;
                    aktObject.setNextObject(new Objekt(new PointF(x, y), new Point(speedX, speedY), groesse, col));
                    lastObject = aktObject.getNextObject();
                }
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();

    }

    @Override
    protected void onPause() {
        super.onPause();
        // Thread stoppen, um Akku zu sparen, wenn die App im Hintergrund ist
        meineZeichenFlaeche.stoppeThread();
    }
}

Beim ersten Klick wird ein Thread gestartet, der alle 20ms die gesamte Objektkette abarbeitet und die einzelnen Objekte darstellt. Um unterschiedliche Objekte zu erzeugen, wird die Klasse Zufall verwendet, der Minimum und Maximum mit übergeben werden. So werden die Objekte in der Nähe vom Mittelpunkt erzeugt und unterscheiden sich in Größe, Farbe und Geschwindigkeit. Hier noch der Code von Zufall:


package de.siebenberge.beispiel;

import android.graphics.Color;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class Zufall {

    public static int getZufall(int min, int max) {
        return ThreadLocalRandom.current().nextInt(min, max + 1);
    }

    public static float getZufall(float min, float max) {
        return min + ThreadLocalRandom.current().nextFloat() * (max - min);
    }

    public static int getRandomColor(int min, int max){
        int r = ThreadLocalRandom.current().nextInt(min, max+1);
        int g = ThreadLocalRandom.current().nextInt(min, max+1);
        int b = ThreadLocalRandom.current().nextInt(min, max+1);
        return android.graphics.Color.rgb(r, g, b);
    }
}

Damit nun endlich Bewegung ins Spiel kommt, hier die geänderte Klasse ZeichenAnsicht. Die Klasse implementiert nun das Interface Runnable. Dadurch können wir den Thread starten, der zyklisch die Objektkette durchläuft und die Objekte an ihre aktuelle Position zeichnet. Die Spielschleife läuft, solange der Thread lebt. Zunächst wird die Zeit berechnet, die seit dem letzten Durchgang vergangen ist. Mit dieser Zeitspanne wird nun in erstesObjekt.update() die Position jedes Objekts neu berechnet. (Siehe oben in der Klasse Objekt). Dann wird durch postInvalidate() der Aufruf von onDraw ausgelöst. Hier wird nun nicht mehr direkt gezeichnet, sondern die Zeichenroutine der Klasse Objekt aufgerufen. So werden alle Objekte an ihrer neuen Position gezeichnet. Fertig ist die Bewegung. Dann pausiert der Thread für 20ms, bevor es oben mit der Schleife wieder von vorn anfängt.


package de.siebenberge.beispiel;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

public class ZeichenAnsicht extends View implements Runnable {

    private Paint pinsel;
    private Objekt erstesObjekt;
    private Thread spielThread;
    private boolean laeuft = false;
    private long letzteZeit;


    public ZeichenAnsicht(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PointF getSize() {
        return new PointF(getWidth() * 1f, getHeight() * 1f);
    }

    private void init() {
        pinsel = new Paint();
        pinsel.setColor(Color.RED); // Beispielfarbe für das Zeichnen
        pinsel.setAntiAlias(true);
        pinsel.setStyle(Paint.Style.FILL);
        pinsel.setStrokeWidth(5f);
    }

    // Startet den Thread (wird  von der MainActivity aufgerufen)
    public void starteThread(Objekt erstesObjekt) {
        this.erstesObjekt = erstesObjekt;

        if (!laeuft) {
            laeuft = true;
            letzteZeit = System.currentTimeMillis();
            spielThread = new Thread(this);
            spielThread.start();
        }
    }

    // Stoppt den Thread, wenn die App pausiert wird
    public void stoppeThread() {
        laeuft = false;
        try {
            if (spielThread != null) spielThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (laeuft) {

            // 1. Zeitdifferenz seit dem letzten Durchlauf berechnen (in Sekunden)
            long jetzt = System.currentTimeMillis();
            float deltaTime = (jetzt - letzteZeit) / 1000f;
            letzteZeit = jetzt;

            if (erstesObjekt != null) {
                erstesObjekt.update(deltaTime);
            }
            postInvalidate();

            try {
                Thread.sleep(20); // 20 Millisekunden = ca. 50 FPS
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.LTGRAY);
        if (erstesObjekt != null) {
            erstesObjekt.draw(canvas, pinsel);
            // in der Klasse Objekt, wird dann die gesamte Objektkette abgearbeitet
        }
    }
}




Beispiel2
So sieht der Bildschirm nun aus.
Mehrere unterschiedliche Objekt fliegen über den Bildschirm.
Daraus lässt sich doch wohl ein Spiel entwickeln.
Dazu demnächst mehr auf SoftwareAusDemSiebengebirge!