AuthorJohannes Borchardt

Tutorial: Android Spieleentwicklung – Theorie

Spiele: Hat wahrscheinlich jeder schonmal gemacht, die meisten wahrscheinlich auch schonmal auf dem Computer, einige auch auf ihrem Android-Device.

Spielen ist das Eine, Spiele entwickeln das Andere. Und Spiele machen für die Android-Plattform ist auch nochmal was anderes. Im wesentlichen kann man dabei Spiele mit Animationen, also alle Spiele, bei denen sich etwas bewegt, oder Spiele ohne Animationen, zum Beispiel textbasierte Spiele, unterscheiden. Ich will hier auf erste eingehen.

Animierte Spiele können in Android entweder in 2D mit Hilfe von Canvas und SurfaceViews oder in 3D mit Hilfe von OpenGL ES geschrieben werden. Dabei sind mit OpenGL ES auch 2D-Spiele ohne weiteres möglich. Bei der Entscheidung OpenGL ES oder nicht sollte man sich darüber im Klaren sein, dass OpenGL die wahrscheinlich anspruchsvollere Variante ist, da mehr mit Matrizen und Vektoren umgegangen werden muss als in Canvas-basierten Spielen und die dritte Dimension durchaus einen Unterschied macht. Für  OpenGL ES – basierte 2D – Spiele kann man sich zum Beispiel die andengine anschauen (sehr zu empfehlen).

Worüber ich jetzt schreiben werde hat aber weder mit OpenGL ES, noch mit Canvas-basierten Spielen zu tun, es ist etwas genereller, es geht um Loops und Threads.

Generell heißt es, dass ein Spiel eine main Loop haben soll. Diese main Loop ist eine Schleife in der alles geschieht, was sich um die Animation (Rendering), Simulation und Eingabenverarbeitung dreht und die so lange läuft wie das Spiel selber. Das heißt zum Beispiel: ein ball ist in der Mitte des handys, die main Loop macht eine weitere Iteration, stellt dabei fest, dass das Handy nach rechts geneigt wurde, weshalb in der Simulation berechnet wird, dass der Ball drei Pixel weiter rechts dargestellt werden soll, was dann in der Animation gezeichnet wird. Diese generelle Aussage ist falsch oder sollte zumindest so nicht umgesetzt werden.

Das Stichwort heißt Nebenläufigkeit: Anstatt in einem einzigen Thread alles auszuführen sollte klar zwischen den drei Spielelementen, Eingabeverarbeitung, Simulation und Rendering, getrennt werden.

Zunächst einmal zum Rendering-Thread: In diesem wird alles gezeichnet. In OpenGL macht man das ganz einfach, indem man das Interface GLSurfaceView.Renderer implementiert und dann alles in der onDraw()-Methode malen lässt, OpenGL ES kümmert sich dann um einen eigenen Thread. Bei SurfaceViews schaut man sich am besten auf dem offiziellen Blog und im SDK-Beispiel LunarLander um. Das Rendern auf normalen Views ist für viele Zwecke viel zu langsam, weshalb hier nicht näher darauf eingegangen wird.

Als nächstes benötigt man einen Thread, der sich um die Simulation kümmert, das heißt der alle Objekte, deren Verhalten und Position verwaltet.

Als drittes müssen noch die Eingabemethoden ausgelesen werden. Legt man die Werte aus diesen nicht in plain old dataobjects ab (was man aber oft tut), muss hier beim Zugriff auf die Threadsynchronisation geachtet werden.

Das gilt auch für den Zugriff auf die Simulationsobjekte: Auf diese wird sowohl vom Simulationsthread, der diese berechnet, als auch vom Renderingthread zugegriffen, es ist also für Synchronisation zu sorgen! Das bedeutet: Wenn ein Thread eine Methode oder ein Objekt, die oder das als synchronized gekennzeichnet ist, bearbeiten will, kann er das nur, wenn das kein anderer Thread tut.

Hier ein konkretes Beispiel, im Simulationsthread:

final List<GameObject> objects = simulation.objects;
for(int i=0; i++; i<objects.size()){
     GameObject object;
     synchronize(object){
        object.update(deltaTime);
     }
}

und im Rendering Thread:

final List<GameObject> objects = simulation.objects;
for(int i=0; i++; i<objects.size()){
     GameObject object;
     synchronize(object){
        //in diesem Fall wird eine GLSurface View übergeben.
        //es wäre auch möglich das Rendering außerhalb des
        //GameObjects durchzuführen, z.B. in einer Renderer-Klasse.
        object.render(gl);
     }
}

Eigentlich alles ziemlich einfach, oder?

Wir iterieren zum Einen im Simulationsthread einmal über alle Simulationselemente, dies machen wir mit einer normalen for-schleife, weil diese bei Listen performanter sind als foreach-Konstrukte. Wir übergeben einer update-Methode aller Spielelemente eine delta-Zeit und das war’s. Die Deltazeit bezeichnet dabei die Zeit, die seit dem letzten Zeichnen des Bildschirms vergangen ist. Somit kann gewährleistet werden, dass ein Objekt sich immer in der gleichen Zeit gleich weit bewegt, auch wenn unterschiedlich viele Frames pro Sekunde gezeichnet werden können. In der Update-Methode der jeweiligen Spielobjekte muss dann alles berechnet werden, was das Verhalten des Objekts ausmacht, wie zum Beispiel die Position.

Im Rendering-Thread das Selbe: Eine Iteration über alle Spielelemente, wobei jedes Element über eine render()- Methode verfügt, die mit einer übergebenen GLSurfaceView umgehen kann und sich selbst darauf zeichnet. Das funktioniert natürlich auch mit normalen Views oder SurfaceViews. Hier wird auch die Wichtigkeit von Synchronisation deutlich: Stellt Euch vor ein GameObject hätte eine Position{x,y,z}. Gerade als der Rendering-Thread ein Objekt zeichnen will und bereits x und y verarbeitet hat, manipuliert der Simulationsthread den z-Wert, weshalb das Objekt an einer ganz anderen Stelle gezeichnet wird als es eigentlich vorgesehen gewesen wäre. Darum wird das Objekt während es gezeichnet wird einfach von gelockt, weshalb die Simulation nicht mehr darauf zugreifen kann bis das Malen abgeschlossen ist.

Fehlt nur noch… Die Nutzereingabe, richtig! Diese wird normalerweise gepollt, d.h. man fragt zum Beispiel immer nach dem Ort, auf den der Nutzer das letzte mal gedrückt hat ab. Wenn sich dieser nicht verändert hat, bewegt man zum Beispiel eine Figur dort hin, wenn er sich verändert hat, bewegt man sie zu dem neuen Punkt.

So viel zur generellen Theorie der Spielentwicklung. Wirkt eigentlich gar nicht so schwer, muss es auch nicht immer sein. Teil zwei wird ein kurzes praktisches Beispiel der obigen Theorielektion auf Basis einer SurfaceView sein, lasst Euch überraschen.

How to: Serverkommunikation in Android (REST + JSON)

Es gibt viele Bücher über Android, mindestens drei davon sogar in der deutschen Sprache. Die meisten dieser Bücher sind sich ziemlich ähnlich: Sie erklären Android-Grundkonzepte und manchmal noch ein bisschen mehr (z.b. OpenGL/ES) und das wars dann. Die Frage, die sich mir in diesen Büchern immer gestellt hat, war: Wenn meine Anwendung mit mehr als dem Handy kommunizieren lassen will, was dann? Wie bringe ich meinen Android dazu, mit einem Server zu reden? Dieser Teil wurde in der Regel als “HTTP-Grundlagen” oder ähnliches ausgelassen, zumindest in den Büchern die mir untergekommen sind.

Um ein bisschen Abhilfe zu verschaffen erkläre ich  hier zwei Methoden, mit Hilfe derer Android-Clients REST-Server ansprechen und JSON-Format übertragen, bzw. verarbeiten können.

Zuerst der Theoretische Teil: REST steht für Representional State Transfer und wurde im Jahr 2000 von Roy Fielding vorgestellt. Wie der Name bereits sagt, können Stadien transferiert werden, sprich auf solche zugegriffen werden. Oder andersrum: Ein REST-konformer Server kennt keinen Zustand. Rest baut auf dem HTTP-Protokol auf und verwendet (mindestens) vier der Basiskomponenten:

GET für Datenanfragen,

POST für das Ablegen von Daten,

DELETE für das Löschen von Daten und

PUT für das Updaten von Daten.

Internetnutzer sollten zumindest mit GET-Befehlen vertraut sein. Internetseiten, die Daten anfordern, übertragen, wenn sie so programmiert wurden, wie es von den HTTP-Schaffern (zu denen auch Fielding gehört) gedacht war, in der URL Parameter. Diese Parameter werden mit einem ? eingeleitet (z.B. http://www.andforge.net/?m=201001).

Als Übertragungsformat hat sich auf mobilen Endgeräten aufgrund des geringen Datenoverheads die JavaScript Object Notation, kur JSON, als passend erwiesen. JSON stellt Objekte Beispielsweise wie folgt dar:

  1. {
  2. "Name" : "Mustermann",
  3. "Vorname"      : "Max",
  4. "Adresse"       :
  5. {
  6. "Strasse"        : "Musterstrasse",
  7. "Nummer"     : "123",
  8. "PLZ"  : "98765",
  9. "Ort"   : "Musterstadt
  10. }
  11. }

Das Format ist also durchaus noch lesbar, aber in seinen Zusatzzeichen, die eine Interpretierbarkeit ermöglichen, sehr sparsam. Dies ist für die eventuell schwachen Verbindungen, die es auf Mobiltelefonen geben kann, hilfreich.

Nach dieser kurzen Einführung wenden wir uns DER Frage zu: Wie geht das in Android?

HTTP GET

Zunächst einmal wird ein REST-konformer Webservice, der im JSON-Format antwortet benötigt. Hat man diesen, kann es losgehen

  1. public class Xxx{
  2. //Bezeichnung der Klasse:
  3. private final String TAG="Xxx";
  4. public void getJSONObject(String url)
  5. {
  6. HttpClient httpClient = new DefaultHttpClient();
  7. HttpGet httpGet = new HttpGet(url);
  8. HttpResponse response;
  9. try {
  10. response = httpClient.execute(httpGet);
  11. // TODO: HTTP-Status (z.B. 404) in eigener Anwendung verarbeiten.
  12. Log.i(TAG,response.getStatusLine().toString());
  13. HttpEntity entity = response.getEntity();
  14. if (entity != null) {
  15. InputStream instream = entity.getContent();
  16. BufferedReader reader = new BufferedReader(new InputStreamReader(instream));
  17. StringBuilder sb = new StringBuilder();
  18. String line = null;
  19. while ((line = reader.readLine()) != null)
  20. sb.append(line + "n");
  21. String result=sb.toString();
  22. Log.i(TAG,result);
  23. instream.close()
  24. JSONObject <b style="color:black;background-color:#a0ffff">json</b>=new JSONObject(result);
  25. JSONArray nameArray=<b style="color:black;background-color:#a0ffff">json</b>.names();
  26. JSONArray valArray=<b style="color:black;background-color:#a0ffff">json</b>.toJSONArray(nameArray);
  27. for(int i=0;i<valArray.length();i++)
  28. {
  29. //TODO: Inhalte der Arrays verarbeiten.
  30. }
  31. }
  32. catch (ClientProtocolException e) {
  33. // TODO Auto-generated catch block
  34. e.printStackTrace();
  35. } catch (IOException e) {
  36. // TODO Auto-generated catch block
  37. e.printStackTrace();
  38. } catch (JSONException e) {
  39. // TODO Auto-generated catch block
  40. e.printStackTrace();
  41. } catch (Exception e){
  42. e.printStackTrace();
  43. }finally{
  44. httpGet.abort();
  45. }
  46. }

Zuerst erzeugen wir einen HTTPCLient. Dann wird ein HTTPGet-Objekt erzeugt. Diesem Objekt wird eine URL mitgegeben, die bereits alle Parameter der Anfrage enthält. Da Wir eine Antwort vom Server erwarten, erzeugen wir zudem ein HTTPResponse-Objekt. Nun führen wir den HTTPGet-Request aus und schreiben die Antwort in unser HTTPResponse-Objekt.

Nun machen wir uns an die angehängten Daten. Diese befinden sich in der HTTPEntity der Antwort. Wir lesen sie mit Hilfe eines buffered readers und eines stringbuffers (aus Performancegründen) aus und erzeugen aus dem so generierten String ein JSONObjekt, welches alle übertragenen Daten enthält, sofern sie im JSON-Format waren. Mit diesen Daten kann nun gearbeitet werden.

So einfach lassen sich Daten mittels REST und JSON in Android abfragen.

HTTP POST

Die nächste Frage, die sich aufdrängt: Daten abfragen funktioniert, aber wie ist das mit den drei anderen Dingern?

Fangen wir mit einer Methode zum Abschicken eines HTTP POST- Objektes an:

  1. public void postJSONObject(String url, JSONObject data, String objectName)
  2. {
  3. HttpPost postMethod = new HttpPost(url);
  4. try {
  5. HttpParams params = new BasicHttpParams();
  6. params.setParameter(objectName, data.toString());
  7. postMethod.setParams(params);
  8. httpClient.execute(postMethod);
  9. Log.i(TAG, "Post request, data: " + params.toString());
  10. } catch (ClientProtocolException e) {
  11. // TODO Auto-generated catch block
  12. e.printStackTrace();
  13. } catch (IOException e) {
  14. // TODO Auto-generated catch block
  15. e.printStackTrace();
  16. } catch (Exception e) {
  17. // TODO Auto-generated catch block
  18. e.printStackTrace();
  19. } finally {
  20. postMethod.abort();
  21. }
  22. }

Wie zuvor wird der Methode eine Ziel-URL übergeben. Des Weiteren wird ein JSONObject mit den zu übertragenden Daten und die Bezeichnung der Daten (z.B. “User”) übergeben. Da wir einem HTTPPost nicht einfach so ein JSONObject anhängen können, wandeln wir dieses zuerst in HTTP-Parameter um und hängen diese dann an.  Nun führen wir den POST aus und alles ist gut (hoffentlich). Natürlich sollten an dieser Stelle noch eventuelle Fehlercodes oder -Meldungen abgefangen werden, dies kann analog zur ersten Methode geschehen, wobei Fehlermeldungen natürlich für jeden Server individuell behandelt werden müssen (üblicherweise gillt: wenn ein JSON-Objekt mit “error:” beginnt, ist was schief gelaufen).

HTTP DELETE und HTTP PUT

DELETE und PUT können so wie POST implementiert werden, nur dass der Objekttyp von postMethod in HTTPDelete, bzw. HTTPPut, geändert wird.

Et voilà: So schnell geht’s und schon fangen die Server an zu flüstern, mehr als das, sie hören sogar zu =)

droid-blog.net is born!

Today, on may 5th 2011 droid-blog.net has come to life.

Born out of the ashes of andforge.net, a popular german Android blog.

The first three articles will be taken over from andforge and hence be in german. All other future articles will be writen in English.

The topics of this blog will all be Android related and probably much about development and money.

Please enjoy! :-)

© 2025 Droid-Blog

Theme by Anders NorenUp ↑