2D Spiel, draw zeichnet Objekte nicht gleichzeitig

  • Antworten:8
Teano
  • Forum-Beiträge: 5

04.01.2011, 11:49:25 via Website

Hallo,
ich habe um meine Programmierfähigkeiten etwas aufzubessern und dabei vllt. was sinnvolles zu schaffen angefangen ein Tower Defence Spiel für Android zu schreiben. Dabei bin ich soweit, dass es möglich ist über eine Karte zu scrollen und Türme zu platzieren. Auf der Karte existiert ein Raster, bei dem jede Zelle einfach einen bool Wert hat, ob ein Turm gesetzt wurde oder nicht und entsprechen einen Turm (bis jetzt in Form einer Ellipse) zeichnet. Zum Scrollen wird ein Offset entsprechend der Fingerbewegung gespeichert und das Hintergrundbild mit resize so geschnitten, dass es aussieht als wenn man darüber scrollt. Außerdem wird berechnet welche Zellen zur Zeit sichtbar sind und deren Draw Methode wird aufgerufen.

Der Code der onDraw Methode sieht so aus:
1public void onDraw(Canvas canvas) {
2 Bitmap resizedBackground = Bitmap.createBitmap(background, XOffset, YOffset, getWidth(), getHeight());
3 canvas.drawBitmap(resizedBackground, 0, 0, null);
4 int actViewRow = 0;
5 int actViewCell;
6 int X;
7 int Y;
8 for(int i = startRow; i < maxRow; i++){
9 actViewCell = 0;
10 Row = MapCells.get(i);
11 Y = actViewRow * CellSize - YScrollOffset;
12 for(int j = startCell; j<maxCell ;j++){
13 X = actViewCell * CellSize - XScrollOffset;
14 Row.get(j).draw(canvas, Paint, X, Y,showGrid);
15 actViewCell++;
16 }
17 actViewRow++;
18 }

Das Problem ist jetzt wenn ich scrolle, dann verschieben sich Türme und Hintergrund kurz gegeneinander. Es sieht also aus als wenn sich die Türme auf dem Hintergrund bewegen. Das passiert nur während des Verschiebens, also wird scheinbar immer erst der Hintergrund und dann die Türme neu gezeichnet und das im Abstand von ca 1/2 Sekunde oder weniger, aber deutlich sichtbar.
Meine Fragen sind:
1. Gibt es eine Möglichkeit den Hintergrund und die Türme gleichzeitig zu zeichnen, so das sie sich nicht gegeneinander verschieben ? ( aus einem Buffer oder so )
oder 2. liegt das Problem daran, dass ich das Bild jedes mal neu mit resize erzeuge und dadurch das Programm so stark ausbremse, dass es zur verschiebung kommt ? Wie kann ich das besser machen ?
3. Würde das ganze wesentlich besser laufen, wenn ich keinen Hintergrund zeichne, sondern jede Zelle ein Bitmap zeichnen lasse ?

Ich bin wie man sicherlich sieht kein erfahrener Javaprogrammierer und würd mich über jeden kleinen Hinweis freuen.
Vielen Dank im voraus.

— geändert am 04.01.2011, 11:52:46

Antworten
Mac Systems
  • Forum-Beiträge: 1.727

04.01.2011, 12:41:49 via Website

Da diese Methode vom UI Thread durchlaufen wird und du keinen Hinweis gibst wo du sonst noch Zeichen Code liegen hast sehe ich das Problem nicht direkt.

PS: Es ist ineffektiv bei jedem Aufruf der onDraw ein neues Bitmap zu erzeugen! "Cachen" und wiederverwenden!

Windmate HD, See you @ IO 14 , Worked on Wundercar, Glass V3, LG G Watch, Moto 360, Android TV

Antworten
Teano
  • Forum-Beiträge: 5

04.01.2011, 14:51:30 via Website

Danke für die schnelle Antwort. Sonst gibt es keinen weiteren Code außer in den Cells. Da wird aber nur abgefragt ob showGrid == true, dann wird ein Quadrat um die Zelle gezeichnet und es wird geprüft ob ein Tower gesetzt ist, dann eine Ellipse. Sollte beides nicht sehr anspruchsvoll sein für das Desire.
Zu dem Cachen und wiederverwenden: Verstehe ich das richtig, dass dann ein Bild für jede mögliche Scrollposition also jede mögliche OffsetX und OffsetY Kombination gespeichert wird ? Das was würde doch sicher viel Speicher verbrauchen und beschleunigt auch erst wenn ich zum 2.Mal über die gleiche Position scrolle oder ?
Mir ist grad so aufgefallen, dass ja viele auch mit OpenGL auf Android arbeiten. Kann man damit auch gut 2D-Spiele Zeichnen und würde das mein Problem beheben ?

Antworten
Tobias Eckert
  • Forum-Beiträge: 155

04.01.2011, 17:08:55 via Website

Mit der onDraw() Variante ist es sehr schwer flüssige Animationen hin zu bekommen. Viel einfacher geht das wenn Du eine SurfaceView verwendest. Das geht prinzipiell so:

Definiere eine View die von SurfaceView erbt. Setze diese View dann als Haupt-View Deiner App. In der View überschreibst Du die Methoden onSurfaceCreate(), onSurfaceDestroyed() und onSurfaceChanged(). Diese werden aufgerufen wenn die View zum ersten mal sichtbar wird, resized wird etc. Dann baust Du Dir einen Thread der die SurfaceView updated.

In diesem Thread wird dann bei jedem Durchlauf der komplette Screen neu gezeichnet, ähnlich Deiner onDraw() Methode. Wenn Du mit der Zeichenoperation fertig bist wird der Screen released und somit angezeigt. Damit wird effektiv verhindert dass halbfertig gezeichnete Screens dargestellt werden (was bei Dir der Grund für die Verschiebung ist).

Das genau mit Bespiel-Code zu Beschreiben wäre ein Tutorial für sich. Einfach mal nach SurfaceView googeln, dann findest Du da jede Menge Beispielcode zu.

Als zweites: die Speicherverwaltung bei Android ist ein Thema für sich. Speziell wenn es um Bitmaps geht. Deine onDraw Method wird ja tausende Male aufgerufen. D.h. Du allokierst auch tausende Male Speicher mit der Bitmap.createBitmap() Methode. Da wirst Du sehr schnell in eine Out of Memory Exception laufen.

Besser wäre es die Bitmap ausserhalb von onDraw, nur einmal zum Programmstart zu erzeugen, und dann immer die gleiche Bitmap wieder zu verwenden.

Wenn Du sie nicht mehr brauchst mit .recycle() freigeben und mit System.gc() eine Garbage Collection anstossen.

Teano

Antworten
Teano
  • Forum-Beiträge: 5

04.01.2011, 20:37:46 via Website

Danke für deine Hilfe. Ich habe jetzt alles so geschrieben wie ich es verstanden habe. Das sieht wie folgt aus:

Ich habe eine Class TowerActivity extends Activity in dessen onCreate setze ich mit setContentView(new TowerMapSurfaceView(this)) diese SurfaceView als Haupt-View.
In der Klasse class TowerMapSurfaceView extends SurfaceView implements SurfaceHolder.Callback siehts ungefähr so aus:
1@Override
2 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
3 }
4 @Override
5 public void surfaceCreated(SurfaceHolder holder) {
6 if (!MapThread.isAlive()) {
7 MapThread = new MapThread(this);
8 }
9 MapThread._run = true;
10 MapThread.start();
11 }
12 @Override
13 public void surfaceDestroyed(SurfaceHolder holder) {
14 boolean retry = true;
15 MapThread._run = false;
16 while (retry) {
17 try {
18 MapThread.join();
19 retry = false;
20 } catch (InterruptedException e) {
21 }
22 }
23 }
Dazu noch die onTouch behandlung zum Scrollen und eine onDraw() Methode.
MapThread sieht so aus:
1public class MapThread extends Thread {
2 private TowerMapSurfaceView _map;
3 public boolean _run = false;
4
5 public MapThread(TowerMapSurfaceView map) {
6 _map = map;
7 }
8
9 /** Gameloop **/
10 @Override
11 public void run() {
12 Canvas c;
13 while (_run) {
14 c = null;
15 try {
16 c = _map.getHolder().lockCanvas(null);
17 synchronized (_map.getHolder()) {
18 _map.onDraw(c);
19 }
20 } finally {
21 if (c != null) {
22 _map.getHolder().unlockCanvasAndPost(c);
23 }
24 }
25 }
26 }
27}

Ich habe jetzt nicht ganz verstanden was du meintest mit der Thread updatet immer die SurfaceView. Das passiert doch durch _map.onDraw(c) in der While-Schleife oder ?
Jedenfalls läuft zwar alles, aber das Problem besteht auch so weiterhin. Es wird trotzdem nichts gleichzeitig gezeichnet.

Zu der Speichersache: Ich möchte ja über ein großes Bild scrollen. Dafür hole ich mir ja per public Bitmap background = BitmapFactory.decodeResource(getResources(), R.drawable.background); das Hintergrundbild als ganzes. Davon möchte ich je nach Offset nur einen Teil zeigen. Also am Anfang das Quadrat von (0,0) bis (100,100) und wenn ich 2 pixel nach rechts Scrolle eben (0,2) bis (100,102). Dafür schneide ich mit resize immer ein Stück aus background aus und erzeuge daraus ein neues Bitmap. Dass das sehr ineffizient ist weiß ich jetzt, aber wie erzeuge ich mir dann diese Ausschnitte ? Gibt es eine Funktion die einfach nur Ausschnitte von bitmaps zeichnet ohne jedes mal ein neues Bitmap zu erzeugen ? Oder ist der Ansatz schon komplett falsch ?
Danke für den Tipp mit recycle und gc, darauf hät ich garnicht geachtet.

Antworten
Tobias Eckert
  • Forum-Beiträge: 155

04.01.2011, 23:25:01 via Website

Teano
Danke für deine Hilfe. Ich habe jetzt alles so geschrieben wie ich es verstanden habe.

Das ging ja flott!


Teano
Ich habe jetzt nicht ganz verstanden was du meintest mit der Thread updatet immer die SurfaceView. Das passiert doch durch _map.onDraw(c) in der While-Schleife oder ?

Genau.

Teano

Jedenfalls läuft zwar alles, aber das Problem besteht auch so weiterhin. Es wird trotzdem nichts gleichzeitig gezeichnet.

Dann ist da irgendwo ein logischer Fehler. In Deinem vorherigen Post verwendest Du einmal XOffset und einmal XScrollOffset. Könnte es daran liegen?

Teano

Zu der Speichersache: Ich möchte ja über ein großes Bild scrollen. Dafür hole ich mir ja per public Bitmap background = BitmapFactory.decodeResource(getResources(), R.drawable.background); das Hintergrundbild als ganzes. Davon möchte ich je nach Offset nur einen Teil zeigen. Also am Anfang das Quadrat von (0,0) bis (100,100) und wenn ich 2 pixel nach rechts Scrolle eben (0,2) bis (100,102). Dafür schneide ich mit resize immer ein Stück aus background aus und erzeuge daraus ein neues Bitmap. Dass das sehr ineffizient ist weiß ich jetzt, aber wie erzeuge ich mir dann diese Ausschnitte ? Gibt es eine Funktion die einfach nur Ausschnitte von bitmaps zeichnet ohne jedes mal ein neues Bitmap zu erzeugen ? Oder ist der Ansatz schon komplett falsch ?

Nein, das ist genau richtig so. Das kannst Du aber auch hinkriegen ohne jedesmal eine neue Bitmap für den zu zeichnenden Ausschnitt zu erzeugen. Das geht einfach durch canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint).

bitmap ist Dein Hintergrundbild. src ist dann Dein Source Rect, d.h. der Ausschnitt des Hintergrundbildes den Du zeichnen möchtest. Du startest mit (0,0,100,100). Wenn Du zwei Pixel nach rechts scrollst wird es dann zu (2,0,102,100). dst ist Dein Destination Rect. Das bleibt dann immer gleich der kompletten Größe Deiner Ziel-Bitmap, d.h. der Größe der canvas.

Noch zwei Performance Tips:
Nummer 1: Garbage Collection vermeiden. Achte vom Speicher-Handling her darauf dass Du in der on_draw Methode möglichst keinerlei Variablen neu instantiierst. Z.B. das Source Rect außerhalb der Methode erzeugen, und in der on_draw immer nur mit Rect.set(left, top, right, bottom) die Werte neu setzen. Ansonsten erzeugst Du immer neue Variablen, die immer neuen Speicher allokieren. Sobald eine Anzahl dieser Variablen zusammen kommt schlägt die Garbage Collection zu. Die braucht immer ein paar Zehntelsekunden und unterbricht währenddessen Dein Spiel, was vom Nutzer als deutliches Ruckeln wahrgenommen wird.

Nummer 2: Bitmap Resize Operationen vermeiden. Du wirst feststellen dass die Methode canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) wesentlich schneller läuft wenn src und dst exakt die gleichen Ausmasse haben. Dann muss Android die Grafik nicht skalieren. Wenn Deine Background Grafik z.B. maximal doppelt so gross ist wie Dein sichtbarer Bildschirm, erzeuge gleich am Anfang eine skalierte Bitmap die genau die passenden Dimensionen hat (also doppelt so groß wie der Bildschirm), so daß Du in der on_draw Methode nicht mehr skalieren musst. Solche Vor-Skalierungen machst Du am besten ausserhalb des GUI Threads, also z.B. in Deinem Game-Thread.

Teano

Antworten
Teano
  • Forum-Beiträge: 5

05.01.2011, 13:45:55 via Website

Danke, genau solche Informationen brauche ich. Hab heute keine Zeit mehr, aber werd mich da morgen gleich ran setzen. Bin schon gespannt, obs flüssig läuft und daraus doch noch ein fertiges Spiel wird.
Das ScrollOffset ist übrigens nur das Offset für die Zellen, falls welche zum Teil außerhalb des Bildes sind. Dieses Offset ist also ein Bruchteil von der Zellengröße und unterscheidet sich von dem Offset für das Hintergrundbild. Das war hier nicht zu erkennen, aber ich bin positiv überrascht wie genau du dir den Code angeguckt hast.
Noch mal vielen Dank, du hast mir sehr geholfen.

— geändert am 05.01.2011, 13:46:32

Antworten
Mac Systems
  • Forum-Beiträge: 1.727

05.01.2011, 17:31:41 via Website

Ich kann das nicht nachvollziehen. Der GC hat einen Thread die APP den Main Thread, der GC arbeitet nicht deterministisch daher ist sicher das erzeugen von kurzlebigen Objecten schlecht das wars aber dann auch erstmal.
Das Problem des Zeichnens ist dennoch nicht gelöst meiner meinung nach da die onDraw alles aus einem Thread erhält dieser das ganze auch präsentiert. Sicher ist der SurefaceView eine sache die zu bevorzugen ist.

Windmate HD, See you @ IO 14 , Worked on Wundercar, Glass V3, LG G Watch, Moto 360, Android TV

Antworten
Teano
  • Forum-Beiträge: 5

06.01.2011, 15:20:18 via Website

Ich verstehe nicht ganz was du meinst, aber das Problem scheint tatsächlich behoben zu sein. Es gibt keine sichtbare Verschiebung mehr. Ich hoffe die tritt auch bei sehr vielen Objekten im späteren Spielverlauf nicht auf, wenn ich weiter keine neuen Variablen im onDraw anlege. Bis jetzt habe ich mich nur darum gekümmert, dass überhaupt etwas zu sehen und eine rudimentäre Bedienung möglich ist. Berechnungen für Gegnerrouten, Trefferabfrage usw. werde ich versuchen in einen anderen Thread zu stecken falls du das meinst. Da muss ich mir mal angucken wie die dann miteinander kommunizieren können.

— geändert am 06.01.2011, 15:21:01

Antworten