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