0

I have the start of an android game. It has a GameLoop (thread) class which keeps track of an fps value and a GameView (surfaceview) which instatiates a GameLoop object and sets it running and so on. Now i have a separate FpsText object that knows how to draw itself to the screen but to update i need to do something like fpsText.setText(gameloop.getFps());

This isnt a problem if i do it in the GameView class but i wanted to try and have the an FpsText object that could take care of updating itself. I don't have any other experience with threads and I thought it might backfire but none the less i tried passing fpsText the gameloop reference as an argument. Unsurprisingly i get different unwanted results everytime i run the application so i wanted to know what you think the best way to handle this situation would be?

As an alternative to updating fpsText in the GameView i could always just make the fps value in the gameloop class public but i know that's bad practice! Neither sound like good solutions, perhaps there's a better way?

For reference here's my gameloop, with the second change suggested by RedOrav implemented:

package biz.hireholly.engine;

import android.graphics.Canvas;
import android.util.Log;
import android.view.SurfaceHolder;
import biz.hireholly.gameplay.FpsText;

import java.util.ArrayList;


/**
 * The GameLoop is a thread that will ensure updating and drawing is done at set intervals.
 * The thread will sleep when it has updated/rendered quicker than needed to reach the desired fps.
 * The loop is designed to skip drawing if the update/draw cycle is taking to long, up to a MAX_FRAME_SKIPS.
 * The Canvas object is created and managed to some extent in the game loop,
 * this is so that we can prevent multiple objects trying to draw to it simultaneously.
 * Note that the gameloop has a reference to the gameview and vice versa.
 */

public class GameLoop extends Thread {

    private static final String TAG = GameLoop.class.getSimpleName();
    //desired frames per second
    private final static int MAX_FPS = 30;
    //maximum number of drawn frames to be skipped if drawing took too long last cycle
    private final static int MAX_FRAME_SKIPS = 5;
    //ideal time taken to update & draw
    private final static int CYCLE_PERIOD = 1000 / MAX_FPS;

    private SurfaceHolder surfaceHolder;
    //the gameview actually handles inputs and draws to the surface
    private GameView gameview;

    private boolean running;
    private long beginTime = 0; // time when cycle began
    private long timeDifference = 0; // time it took for the cycle to execute
    private int sleepTime = 0; // milliseconds to sleep (<0 if drawing behind schedule) 
    private int framesSkipped = 0; // number of render frames skipped

    private double lastFps = 0; //The last FPS tracked, the number displayed onscreen
    private ArrayList<Double> fpsStore = new ArrayList<Double>(); //For the previous fps values
    private long lastTimeFpsCalculated = System.currentTimeMillis(); //used in trackFps
    FpsText fpsText;

    public GameLoop(SurfaceHolder surfaceHolder, GameView gameview, FpsText fpsText) {
        super();
        this.surfaceHolder = surfaceHolder;
        this.gameview = gameview;
        this.fpsText = fpsText;
    }

    public void setRunning(boolean running) {
        this.running = running;     
    }
    @Override
    public void run(){

        Canvas c;

        while (running) {
            c = null;
            //try locking canvas, so only we can edit pixels on surface
            try{
                c = this.surfaceHolder.lockCanvas();
                //sync so nothing else can modify while were using it
                synchronized (surfaceHolder){ 

                    beginTime = System.currentTimeMillis();
                    framesSkipped = 0; //reset frame skips

                    this.gameview.update();
                    this.gameview.draw(c);

                    //calculate how long cycle took
                    timeDifference = System.currentTimeMillis() - beginTime;
                    //good time to trackFps?
                    trackFps();

                    //calculate potential sleep time
                    sleepTime = (int)(CYCLE_PERIOD - timeDifference);

                    //sleep for remaining cycle
                    if (sleepTime >0){
                        try{
                            Thread.sleep(sleepTime); //saves battery! :)
                        } catch (InterruptedException e){}
                    }
                    //if sleepTime negative then we're running behind
                    while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS){
                        //update without rendering to catch up
                        this.gameview.update();
                        //skip as many frame renders as needed to get back into
                        //positive sleepTime and continue as normal
                        sleepTime += CYCLE_PERIOD;
                        framesSkipped++;
                    }

                }

            } finally{
                //finally executes regardless of exception, 
                //so surface is not left in an inconsistent state
                if (c != null){
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }

    }

    /* Calculates the average fps every second */
    private void trackFps(){
        synchronized(fpsStore){
            long currentTime = System.currentTimeMillis();

            if(timeDifference != 0){
                fpsStore.add((double)(1000 / timeDifference));
            }
            //If a second has past since last time average was calculated,
            // it's time to calculate a new average fps to display
            if ((currentTime - 1000) > lastTimeFpsCalculated){


                for (Double fps : fpsStore){
                lastFps += fps;
                }

                lastFps /= fpsStore.size();

                synchronized(fpsText){ //update the Drawable FpsText object
                fpsText.setText(String.valueOf((int)lastFps));
                }

                lastTimeFpsCalculated = System.currentTimeMillis();

                //Log.d("trackFPS()", " fpsStore.size() = "+fpsStore.size()+"\t"+fpsStore.toString());
                fpsStore.clear();


            }
        }
    }

}
4

1 に答える 1

1

あなたの望ましくない結果は何ですか?fps 変数を public にすることと getfps() を設定することは、fps 値の取得に関する限りまったく同じことであり、GameView で fps 値を変更しても意味がありません。

GameLoop で行っていることは、秒単位で fps 変数を変更していると思われます。つまり、GameView もおそらく読み取っている中間値を取得しています。毎秒のみ fps を変更し、計算を行うために時間変数をその間に保持することをお勧めします。最初に 0 fps があるとします。GameLoop が計算を行い、1 秒が経過すると、処理されたフレーム数と時間 (1 秒) がすでに収集されています。それまでは、GameLoop の動作に関係なく、GameView は常に 0 を読み取っていました。その秒の後、60 fps を達成したとします。fps 変数は変更され、次の秒まで変更されません。したがって、次の 1 秒間は、GameView が変数をさらに多く読み取ったとしても、常に 60 になります。

// Inside GameLoop

private int fps; // Variable you're going to read
private int frameCount; // Keeps track or frames processed
private long lastUpdated;

while(running) {

   if(System.currentTimeMillis() - lastUpdated > 1000) { // 1 second elapsed
      fps = frameCount;
      frameCount = 0;
   }
   frameCount++;
}

public int getFps() {
   return fps;
}

これを行う別の方法は、fpsText への参照を GameLoop に渡すことです。これは、毎秒経過した後にのみ変更されます。そうすれば、レンダリング スレッドはレンダリングだけを気にする必要があり、GameLoop は 1 秒あたりのフレーム数を気にする必要があります。ただし、競合状態 (render メソッドの途中で GameLoop の setText() を使用する場所) が発生する可能性があることに注意してください。他の方法では心配する必要はありません。レンダリング時と変更時に fpsText オブジェクトを同期ブラケットで囲むと、うまくいくはずです。

それが役に立ったかどうか教えてください!

于 2012-08-04T11:20:19.637 に答える