Ruckelfreie Bewegung

  • 15 Antworten
  • Letztes Antwortdatum
A

Alexfedel

Neues Mitglied
0
Kleine Einstiegsfrage bei der Android Spiele Entwicklung:
Ich bin dabei ein Spiel mit Java zu programmieren. Ohne auf die Details des Codes einzugehen, sollen sich bei mir Objekte und der Hintergrund von unten nach oben bewegen.
Ich benutze einfache Bitmap Objekte und lasse sie in der draw-Methode mit "canvas.drawBitmap(bild, x, y, null);" zeichnen, dabei wird die y-Koordinate runtergezählt mit jedem Methodenaufruf. Ich habe darauf geachtet, dass in der draw-Methode nicht unnötig Speicher allokiert wird.

Leider laufen die Bewegungen des Hintergund Bildes und der Objekte nicht flüssig, es sieht einfach nicht gut aus wie man es bei normalen Spielen kennt. Mein Handy hat ein Quad Core Prozessor, daran liegt es wohl kaum.
Meine Frage ist falls jemand Erfahrung mit Java hat, wie man eine saubere Bewegung hinbekommt? Muss man sich bei Java auf die draw-Methode mit den x,y-Koordinaten beschränken oder gibt es da Alternativen? Funktioniert es mit anderen Programmiermöglichkeiten außer Java besser? Außer Java und C++ habe ich nichts weiter bisher gemacht und weiß daher nicht ob ich mich auf die Schnelle umstellen kann.

Über Ratschläge würde ich mich freuen.
 
Wie oft wird draw denn pro Sekunde aufgerufen?

Gesendet von meinem Galaxy Nexus mit der Android-Hilfe.de App
 
Sehr flüssige Animationen laufen mit 24 FPS.
Achte darauf, dass du onDraw ähnlich oft aufrufst!
 
Unten mal der Code, wo die draw-Methode aufgerufen wird, diesen habe ich mir mehr oder weniger in einem Tutorial abgeschaut. Jedenfalls soll er mit der sleepTime dafür sorgen, dass sich die Objekte auf jedem Gerät gleich schnell bewegen und schließlich arbeitet jedes Gerät die Schleife unterschiedlich schnell ab. Die Variable FPS hat dabei wenig Einfluss auf die Bewegung, ob 24 oder 50 oder 10000, alles bringt keine Änderung.

Die Frage ist, wie sorge ich dafür, dass die Schleife unten 24 Mal pro Sekunde durchlaufen wird. So wie ich das sehe, muss doch "while (isRunning)" in 1/24 Sekunden durchlaufen werden oder schneller. Ob schneller oder genau 24 Frames pro Sekunde ist doch egal? Ich muss wohl mal nachschauen wie lange es braucht die Schleife einmal komplett zu durchlaufen. Wenn es länger als 1/24 Sekunde dauert liegt es am Code, aber ich wüsste sonst auch nicht wie ich einstellen kann, dass es immer genau 1/24 Sekunde dauert die komplette draw-Methode zu durchlaufen, zumal es doch nicht immer gleich lange dauert.

Code:
package com.example.game2;

import android.graphics.Canvas;

public class GameLoopThread extends Thread {                                   
    
    static final long FPS = 24;
    private GameView theView;
    private boolean isRunning = false;
    long TPS = 1000 / FPS;
    long startTime, sleepTime;
    
    public GameLoopThread(GameView theView) {
        this.theView = theView;
    }
    
    public void setRunning(boolean run) {
        isRunning = run;
    }
    
    @Override
    public void run() {
        

        while (isRunning) {
           
            Canvas theCanvas = null;
            startTime = System.currentTimeMillis();
            
            try {
                theCanvas = theView.getHolder().lockCanvas();
                synchronized (theView.getHolder()) {
                    theView.draw(theCanvas);        //Hier wird draw aufgerufen 
                }
            } finally {
                if (theCanvas != null) {
                    theView.getHolder().unlockCanvasAndPost(theCanvas);
                }
            }
            
            sleepTime = TPS - (System.currentTimeMillis() - startTime);
            try {
                if (sleepTime > 0)
                    sleep(sleepTime);
                else
                    sleep(10);
            } catch (Exception e) {
 
            }
            
        }
    }
}
 
Benötigt deine Spiellogik eine konstante FPS? Wenn nein, solltest du kein Thread.sleep() nutzen. Bewege alle Sprites abhängig davon, wie viel Zeit für das Zeichnen des letzten Frames benötigt wurde.

Beispiel:
Code:
long last = System.currentTimeMillis();
int delta;

@Override
public void run() {
    while (isRunning) {
        long time = System.currentTimeMillis();
        delta = (float) (time - last);
        last = time;

        Canvas theCanvas = null;
        try {
            theCanvas = theView.getHolder().lockCanvas();
            synchronized (theView.getHolder()) {
                theView.draw(theCanvas);
            }
        } finally {
            if (theCanvas != null)
                theView.getHolder().unlockCanvasAndPost(theCanvas);
        }
    }
}
In deiner draw()-Methode kann dann etwas wie y += delta * speed; stehen.
 
Eigentlich soll es schon eine konstante Framerate haben, weil sich bei variabler Framerate die Objekte doch auf unterschiedlichen Geräten unterschiedlich schnell bewegen würden. Mein Spiel läuft vom Prinzip wie das Spiel "NinJump", von der Performence soll sich daher alles genauso gut bewegen, ohne Ruckler. Unten ist mal meine Gameview Klasse mit der draw-Methode. Als Beispiel bewegt sich das Objekt kontinuierlich von unten nach oben.

Allerdings kommt es mir da schon so vor, dass es nicht flüssig läuft. Gelegentlich gibt es Ruckler. Wenn ich jetzt noch weitere Bilder zur draw Methode hinzufügen will, wie zum Beispiel das Hintergrundbild und später noch jedes Mal auf Kollision geprüft werden soll, dann läuft es noch unsauberer als jetzt schon.

Liegt es an meinem Code, liegt es an Java oder wie bekommen die Leute bei NinJump und anderen Spielen eine fehlerfreie Bewegung hin, da stottert auf meinem Handy nichts. Wäre es doch sinnvoller eine andere ProgrammierSprache zu wählen oder geht es bei Java auch irgendwie.


Code:
public class Gameview extends SurfaceView{
     
    private SurfaceHolder surfaceHolder;
    private Bitmap bmp;
    private GameLoopThread theGameLoopThread;

    DisplayMetrics metrics = this.getResources().getDisplayMetrics();
    int width = metrics.widthPixels;
    int height = metrics.heightPixels;

    int x = 200;
    int y = height-200;
   
    public Gameview(Context context) {
        super(context);
        theGameLoopThread = new GameLoopThread(this);
        surfaceHolder = getHolder();
        
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
 
            public void surfaceDestroyed(SurfaceHolder holder) {
                boolean retry = true;
                theGameLoopThread.setRunning(false);
                while(retry){
                    try {
                        theGameLoopThread.join();
                        retry=false;
                    }catch(InterruptedException e){
 
                    }
                }
 
            }
 
            public void surfaceCreated(SurfaceHolder holder) {
                theGameLoopThread.setRunning(true);
                theGameLoopThread.start();
            }
 
            public void surfaceChanged(SurfaceHolder holder, int format,
                    int width, int height) {
 
            }
        });
        
        
        bmp = BitmapFactory.decodeResource(getResources(),
                R.drawable.ic_launcher);
    }
 
    @Override
    public void draw(Canvas canvas) {
  
        canvas.drawColor(Color.DKGRAY);

        canvas.drawBitmap(bmp, x, y, null);
       
        y = y-10;
        if(y==0){y=height;}
        
    }

}
 
Nur wegen der unterschiedlichen FPS muss das Spiel nicht unterschiedlich schnell laufen. Fast alle Shooter die du bis jetzt gespielt hast laufen wahrscheinlich mit variabler FPS.

Deshalb wird ja gemessen, wie viel Zeit für das letzte Frame benötigt wurde.
Mach es mal wie in meinem Code unten und du wirst feststellen, dass sich die Sprites auf jedem Handy gleich schnell bewegen.

Achte auf das y += delta * speed;
 
Okay, ich habe deine Idee ausprobiert und ich verstehe auch die Mathemetik dahinter, dass sich dann alles auf jedem Gerät gleich schnell bewegen sollte.

Das hilft mir trotzdem nicht. Das Problem ist immer noch die Performance. Da das "delta" stets unterschiedlich groß ausfällt, sind die Sprünge in y-Richtung auch unterschiedlich und diese bemerkt man. Mal bleibt er für den Bruchteil einer Sekunde stehen, weil die Schleife länger gedauert hat und um das auszugleichen, macht er eine größeren Sprung, da delta größer ausfällt. Das sieht auch nicht gut aus. Mein Ziel ist es eine saubere Bewegung hinzubekommen ohne Ruckler.

Ich glaube es liegt an sich am Code, aber mir fällt nichts ein, um es schneller zu machen. Mehr als die draw-Methode aufrufen, mache ich nicht. Ich hätte höchstens noch die Idee die ganzen Frames nicht in Echtzeit abzuspielen, sondern vorher zwischenzuspeichern und wie ein Video quasi abzuspielen. Da sind die Bilder dann vorgeladen und müssten ohne Ruckler abgespielt werden. Allerdings weiß ich nicht wie man zwischenspeichert und passend zeitlich versetzt abspielt. Ich bräuchte mal einen Java-Code nach dem die Bewegung theoretisch flüssig läuft
 
Wie viele Sprites zeichnest du denn und wie groß sind die Texturen?
Du könntest natürlich auch OpenGL nutzen.
 
Das ist ja das Problem. Bei deiner Methode habe ich nur ein Sprite, also eine bmp-Datei mit 72x72 Pixel bewegen lassen. Hintergrund ist nur grau bemalt. Eigentlich hatte ich schon vor das in HD-Qualität zeichnen zu lassen, da Handys schon 1920x1080 Pixel haben, ich kann natürlich auch kleinere Auflösung wählen, wenn es anders nicht geht. Nur wenn nicht mal ein kleines Objekt mit 72x72 sich ordentlich bewegt, wie sieht es dann mit bewegtem Hintergrund und ein paar mehr Sprites aus? Unten mal mein kompletter Code mit deiner Methode. Läuft es den mit OpenGL anders und besser als bei Java, letztendlich wird da doch auch irgendwie mit draw gezeichnet?

Code:
public class GameLoopThread extends Thread {                                    //Klasse für flüssige Bewegung 
    
    private Gameview theView;
    private boolean isRunning = false;
    long last = System.currentTimeMillis();
    long delta = 0;
    long time;
    

    public GameLoopThread(Gameview theView) {
        this.theView = theView;
    }
    
    public void setRunning(boolean run) {
        isRunning = run;
    }
    
    
    @Override
    public void run() {
        while (isRunning) {
            time = System.currentTimeMillis();
            delta = (time - last);
            last = time;

            Canvas theCanvas = null;
            try {
                theCanvas = theView.getHolder().lockCanvas();
                synchronized (theView.getHolder()) {
                    theView.draw(theCanvas);
                }
            } finally {
                if (theCanvas != null)
                    theView.getHolder().unlockCanvasAndPost(theCanvas);
            }
        }
    }
}
Code:
public class Gameview extends SurfaceView{
    
    private SurfaceHolder surfaceHolder;
    private Bitmap bmp;
    private GameLoopThread theGameLoopThread;

    DisplayMetrics metrics = this.getResources().getDisplayMetrics();
    int width = metrics.widthPixels;
    int height = metrics.heightPixels;

    int x = 200;
    int y = height-200;
   
    public Gameview(Context context) {
        super(context);
        theGameLoopThread = new GameLoopThread(this);
        surfaceHolder = getHolder();
        
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
 
            public void surfaceDestroyed(SurfaceHolder holder) {
                boolean retry = true;
                theGameLoopThread.setRunning(false);
                while(retry){
                    try {
                        theGameLoopThread.join();
                        retry=false;
                    }catch(InterruptedException e){
 
                    }
                }
 
            }
 
            public void surfaceCreated(SurfaceHolder holder) {
                theGameLoopThread.setRunning(true);
                theGameLoopThread.start();
            }
 
            public void surfaceChanged(SurfaceHolder holder, int format,
                    int width, int height) {
 
            }
        });
        
        
        bmp = BitmapFactory.decodeResource(getResources(),
                R.drawable.ic_launcher);
    }
    @Override
    public void draw(Canvas canvas) {
  
        canvas.drawColor(Color.DKGRAY);

        canvas.drawBitmap(bmp, x, y, null);
       
        
        y = (int) (y- (theGameLoopThread.delta *0.5));        //Höhere Zahlen als 0.5 lassen ihn zu schnell bewegen
        if(y<=0){y=height;}
        
    }

}
Code:
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Gameview(this));
            
        
    }
 
}
 
Ich hab das mal mit der hier beschriebenen Methode gemacht. Ist schon etwas umfangreicher, funktioniert bei mir aber auf jeden Fall flüssig und ohne Ruckler.
 
Danke, ich werde es mir mal anschauen
 
Läuft leider auch mit der dort beschriebenen Methode nicht flüssig. Im Anhang der Code und die apk. Obwohl oben eine Framerate von über 30 Frames angezeigt werden, ist das keine saubere Bewegung, das sieht man. Und wie gesagt, es ist nur ein Objekt, das sich bewegt, es sollen noch mehr werden.

Langsam frage ich mich echt, ob ich eine Augenstörung habe. Es kann doch nicht sein, dass jede Möglichkeit einfache Objekte auf dem Canvas zu zeichnen für mich nicht gut genug aussieht. Ich nehme normale Spiele aus dem Store und vergleiche die Performance mit der Bewegung, die ich fabriziere und es sieht einfach schlechter aus.

Ich glaube, ich muss mir mal OpenGL ansehen, wenn sich keine Lösung für Java findet. Laut Internet soll da alles besser laufen, angeblich.
 

Anhänge

  • Test2.apk
    231 KB · Aufrufe: 108
  • test2.zip
    4 KB · Aufrufe: 82
Hallo,
Guck mal in auf www.java-forum.org da gibt es von Qualix ein spiele tutorial für Anfänger was recht gut gemacht ist :) dort werden auch mathematische Grundlagen und Konzepte behandelt. Du solltest dir dies mal angucken und ggf nach Android portieren für den Anfang.

LG. Dagobert

Gesendet von meinem GT-I9300 mit der Android-Hilfe.de App
 
Das sieht eigentlich alles genau richtig aus.

Was Du mal probieren kannst ist ob Du eventuell Rundungsungenauigkeiten hast die zu den Sprüngen führen.

Ersetze doch mal die Zeile:
y = (int) (y- (theGameLoopThread.delta *0.5));

durch

y= Math.round(y -(float)theGameLoopThread.delta/2f);

Wenn Du Deine App nachher auf Geräten mit unterschiedlichen Display-Größen laufen lässt musst Du hier noch die Displaygröße als Bezugsparameter mit einsetzen.

Z.B.: y= Math.round(y -(float)theGameLoopThread.delta*(float)theCanvas.getHeight()/1600f);

Was Du auch noch versuchen kannst ist Deine theCanvas Variable ausserhalb der run-Schleife zu instantiieren. So wie das momentan läuft wird die Variable bei jedem durchlauf neu erzeugt, also auf einem Handy mit 60fps 60 mal in der Sekunde. In anderen Worten, lass das "Canvas" in der Zeile "Canva theCanvas=null" weg, und deklariere die Variable vor dem while(isRunning) mit Canvas theCanvas.

Schau mal im Log ob trotzdem noch irgendwo gehäuft garbage collections laufen. Die sind in der Regel die Hauptursache für Ruckler. Sollte bei Deinem Code aber eigentlich kein Thema sein.
 
Mit der "theCanvas" Variable außerhalb der run-Schleife und der Rechnung um Rundungsungenauigkeiten zu vermeiden läuft es auch nicht besser. Eindeutig Sprünge wahrzunehmen und keine gleichmässige Bewegung. Ich habe unten den ganzen Code und die apk, um zu zeigen dass es wirklich nicht sauber ist. Dazu das manifest.xml. Vielleicht sind da Fehler drin, die für die ungleichmässige Bewegung sorgen.

Ich bin die ganze Zeit am Googlen, wie man die Performance noch bei Java verbessern kann. An meinem Code wüsste ich spontan nicht, was man ändern kann. Wenn sich doch nur ein vernünftiger Code finden lassen könnte, wo sich ein Objekt kontinuierlich gleichmässig, flüssig bewegt. Daran könnte ich mich orientieren und weiterarbeiten


Code:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test1"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:hardwareAccelerated="true">
        <activity
            android:name="com.example.test1.MainActivity"
            android:label="@string/app_name" 
            android:hardwareAccelerated="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
Code:
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Gameview(this));
   
    }
 
}
Code:
public class Gameview extends SurfaceView{
    
    private SurfaceHolder surfaceHolder;
    private Bitmap bmp;
    private GameLoopThread theGameLoopThread;

    DisplayMetrics metrics = this.getResources().getDisplayMetrics();
    int width = metrics.widthPixels;
    int height = metrics.heightPixels;

    int x = 200;
    int y = height-200;
   
    public Gameview(Context context) {
        super(context);
        theGameLoopThread = new GameLoopThread(this);
        surfaceHolder = getHolder();
        
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
 
            public void surfaceDestroyed(SurfaceHolder holder) {
                boolean retry = true;
                theGameLoopThread.setRunning(false);
                while(retry){
                    try {
                        theGameLoopThread.join();
                        retry=false;
                    }catch(InterruptedException e){
 
                    }
                }
 
            }
 
            public void surfaceCreated(SurfaceHolder holder) {
                theGameLoopThread.setRunning(true);
                theGameLoopThread.start();
            }
 
            public void surfaceChanged(SurfaceHolder holder, int format,
                    int width, int height) {
 
            }
        });
        
        
        bmp = BitmapFactory.decodeResource(getResources(),
                R.drawable.ic_launcher);
    }
    @Override
    public void draw(Canvas canvas) {
  
        canvas.drawColor(Color.DKGRAY);

        canvas.drawBitmap(bmp, x, y, null);
       
        //y = y-10;
        //y = (int) (y- (theGameLoopThread.delta *0.5));
        //y= Math.round(y -(float)theGameLoopThread.delta*(float)canvas.getHeight()/1600f);  Funktionieren nicht flüssig
        
        y= Math.round(y -(float)theGameLoopThread.delta/2f);
        
        if(y<=0){y=height;}
        
    }

}
Code:
public class GameLoopThread extends Thread {                                
    
    private Gameview theView;
    private boolean isRunning = false;
    long last = System.currentTimeMillis();
    long delta = 0;
    long time;
    Canvas theCanvas = null;
    

    public GameLoopThread(Gameview theView) {
        this.theView = theView;
    }
    
    public void setRunning(boolean run) {
        isRunning = run;
    }
    
    
    @Override
    public void run() {
        while (isRunning) {
            time = System.currentTimeMillis();
            delta = (time - last);
            last = time;

            
            try {
                theCanvas = theView.getHolder().lockCanvas();
                synchronized (theView.getHolder()) {
                    theView.draw(theCanvas);
                }
            } finally {
                if (theCanvas != null)
                    theView.getHolder().unlockCanvasAndPost(theCanvas);
            }
        }
    }
}
 

Anhänge

  • Test1.apk
    917,6 KB · Aufrufe: 127
Zurück
Oben Unten