web-dev-qa-db-ja.com

Androidゲームでの迷惑なラグ/スタッター

私はAndroidでのゲーム開発を始めたばかりで、非常にシンプルなゲームに取り組んでいます。

ゲームは基本的にゆるい鳥のようなものです。

なんとかすべてが機能するようになりましたが、多くのスタッターとラグが発生します。

私がテストに使用している電話はLG G2なので、これよりもはるかに重くて複雑なゲームを実行する必要があります。

基本的に、画面全体の幅が互いに離れている4つの「障害物」があります。
ゲームが始まると、障害物は一定の速度で(キャラクターに向かって)動き始めます。プレイヤーキャラクターのx値はゲーム全体を通して一貫していますが、y値は変化します。

ラグは主に、キャラクターが障害物を通過するときに発生します(場合によっては、その障害物の少し後にも発生します)。何が起こるかというと、ゲーム状態の各描画に不均一な遅延があり、動きが途切れます。

  • GCはログに従って実行されません。
  • 吃音は速度が速すぎることが原因ではありません(ゲームの開始時に障害物が見えないときにキャラクターがスムーズに動くためです)
  • MAX_FPSフィールドが100に設定されている場合でも、スタッターが発生するため、問題はFPSに関連しているとは思いません。

私の考えでは、ある種の遅延が発生する(したがってフレームがスキップされる)1行または複数行のコードがあります。また、これらの行は、PlayerCharacterObstacle、およびMainGameBoardupdate()およびdraw()メソッドの周りにあるべきだと思います。

問題は、私はまだAndroid開発とAndroid game開発)に慣れていないので、何ができるのかわかりませんそのような遅延を引き起こします。

私は答えをオンラインで探してみました...残念ながら、私が見つけたすべては、GCが原因であると指摘しました。しかし、そのような私はそれが事実であるとは思わない(私が間違っている場合は私を訂正する)それらの答えは私には当てはまらない。 Android開発者のPerformance Tipsページですが、役立つものは見つかりませんでした。

だから、私がこれらの厄介な遅れを解決するための答えを見つけるのを手伝ってください!

いくつかのコード

MainThread.Java:

public class MainThread extends Thread {

public static final String TAG = MainThread.class.getSimpleName();
private final static int    MAX_FPS = 60;   // desired fps
private final static int    MAX_FRAME_SKIPS = 5;    // maximum number of frames to be skipped
private final static int    FRAME_PERIOD = 1000 / MAX_FPS;  // the frame period

private boolean running;
public void setRunning(boolean running) {
    this.running = running;
}

private SurfaceHolder mSurfaceHolder;
private MainGameBoard mMainGameBoard;

public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
    super();
    mSurfaceHolder = surfaceHolder;
    mMainGameBoard = gameBoard;
}

@Override
public void run() {
    Canvas mCanvas;
    Log.d(TAG, "Starting game loop");

    long beginTime;     // the time when the cycle begun
    long timeDiff;      // the time it took for the cycle to execute
    int sleepTime;      // ms to sleep (<0 if we're behind)
    int framesSkipped;  // number of frames being skipped 

    sleepTime = 0;

    while(running) {
        mCanvas = null;
        try {
            mCanvas = this.mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder) {
                beginTime = System.currentTimeMillis();
                framesSkipped = 0;


                this.mMainGameBoard.update();

                this.mMainGameBoard.render(mCanvas);

                timeDiff = System.currentTimeMillis() - beginTime;

                sleepTime = (int) (FRAME_PERIOD - timeDiff);

                if(sleepTime > 0) {
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {}
                }

                while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                    // catch up - update w/o render
                    this.mMainGameBoard.update();
                    sleepTime += FRAME_PERIOD;
                    framesSkipped++;
                }
            }
        } finally {
            if(mCanvas != null)
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}
}

MainGameBoard.Java:

public class MainGameBoard extends SurfaceView implements
    SurfaceHolder.Callback {

private MainThread mThread;
private PlayerCharacter mPlayer;
private Obstacle[] mObstacleArray = new Obstacle[4];
public static final String TAG = MainGameBoard.class.getSimpleName();
private long width, height;
private boolean gameStartedFlag = false, gameOver = false, update = true;
private Paint textPaint = new Paint();
private int scoreCount = 0;
private Obstacle collidedObs;

public MainGameBoard(Context context) {
    super(context);
    getHolder().addCallback(this);

    DisplayMetrics displaymetrics = new DisplayMetrics();
    ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
    height = displaymetrics.heightPixels;
    width = displaymetrics.widthPixels;

    mPlayer = new PlayerCharacter(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher), width/2, height/2);

    for (int i = 1; i <= 4; i++) {
        mObstacleArray[i-1] = new Obstacle(width*(i+1) - 200, height, i);
    }

    mThread = new MainThread(getHolder(), this);

    setFocusable(true);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    mThread.setRunning(true);
    mThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    Log.d(TAG, "Surface is being destroyed");
    // tell the thread to shut down and wait for it to finish
    // this is a clean shutdown
    boolean retry = true;
    while (retry) {
        try {
            mThread.join();
            retry = false;
        } catch (InterruptedException e) {
            // try again shutting down the thread
        }
    }
    Log.d(TAG, "Thread was shut down cleanly");
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    if(event.getAction() == MotionEvent.ACTION_DOWN) {
        if(update && !gameOver) {
            if(gameStartedFlag) {
                mPlayer.cancelJump();
                mPlayer.setJumping(true);
            }

            if(!gameStartedFlag)
                gameStartedFlag = true;
        }
    } 


    return true;
}

@SuppressLint("WrongCall")
public void render(Canvas canvas) {
    onDraw(canvas);
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.GRAY);
    mPlayer.draw(canvas);

    for (Obstacle obs : mObstacleArray) {
        obs.draw(canvas);
    }

    if(gameStartedFlag) {
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(100);
        canvas.drawText(String.valueOf(scoreCount), width/2, 400, textPaint);
    }

    if(!gameStartedFlag && !gameOver) {
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(72);
        canvas.drawText("Tap to start", width/2, 200, textPaint);
    }

    if(gameOver) {      
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(86);

        canvas.drawText("GAME OVER", width/2, 200, textPaint);
    }

}

public void update() {
    if(gameStartedFlag && !gameOver) {  
        for (Obstacle obs : mObstacleArray) {
            if(update) {
                if(obs.isColidingWith(mPlayer)) {
                    collidedObs = obs;
                    update = false;
                    gameOver = true;
                    return;
                } else {
                    obs.update(width);
                    if(obs.isScore(mPlayer))
                        scoreCount++;
                }
            }
        }

        if(!mPlayer.update() || !update)
            gameOver = true;
    }
}

}

PlayerCharacter.Java:

public void draw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, (float) x - (mBitmap.getWidth() / 2), (float) y - (mBitmap.getHeight() / 2), null);
}

public boolean update() {
    if(jumping) {
        y -= jumpSpeed;
        jumpSpeed -= startJumpSpd/20f;

        jumpTick--;
    } else if(!jumping) {
        if(getBottomY() >= startY*2)
            return false;

        y += speed;
        speed += startSpd/25f;
    }

    if(jumpTick == 0) {
        jumping = false;
        cancelJump(); //rename
    }

    return true;
}

public void cancelJump() { //also called when the user touches the screen in order to stop a jump and start a new jump
    jumpTick = 20;

    speed = Math.abs(jumpSpeed);
    jumpSpeed = 20f;
}

Obstacle.Java:

public void draw(Canvas canvas) {
    Paint pnt = new Paint();
    pnt.setColor(Color.CYAN);
    canvas.drawRect(x, 0, x+200, ySpaceStart, pnt);
    canvas.drawRect(x, ySpaceStart+500, x+200, y, pnt);
    pnt.setColor(Color.RED);
    canvas.drawCircle(x, y, 20f, pnt);
}

public void update(long width) {
    x -= speed;

    if(x+200 <= 0) {
        x = ((startX+200)/(index+1))*4 - 200;
        ySpaceStart = r.nextInt((int) (y-750-250+1)) + 250;
        scoreGiven = false;
    }
}

public boolean isColidingWith(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x && mPlayer.getLeftX() <= x+20)
        if(mPlayer.getTopY() <= ySpaceStart || mPlayer.getBottomY() >= ySpaceStart+500)
            return true;

    return false;
}

public boolean isScore(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x+100 && !scoreGiven) {
        scoreGiven = true;
        return true;
    }

    return false;
}
18
Asaf

Androidでゲームを作成したことはなく、CanvasとbufferStrategyを使用してJava/AWTで2Dゲームを作成しました...

ちらつきが発生した場合は、オフスクリーンの画像にレンダリングして手動のダブルバッファ(ちらつきを取り除く)を選択し、新しい事前レンダリングされたコンテンツを直接ページめくり/ drawImageすることができます。

しかし、アニメーションの「滑らかさ」についてもっと心配しているように感じます。その場合は、異なるアニメーションティックの間に補間を使用してコードを拡張することをお勧めします。

現在、レンダリングループは、レンダリングと同じペースで論理状態を更新(論理的に移動)し、参照時間で測定し、経過時間を追跡しようとします。

代わりに、コード内の「ロジック」が機能するために望ましいと思われる周波数で更新する必要があります。通常は10または25Hzで十分です(実際のFPSとは完全に異なる「更新ティック」と呼びます)。一方、レンダリングは、実際のレンダリングにかかる​​「時間」を測定するために高解像度の時間を追跡することによって行われます(私はnanoTimeを使用しましたが、これで十分ですが、currentTimeInMillisはかなり役に立たない...)、

このようにして、ティック間を補間し、最後のティックからの経過時間と2つの間に「あるべき」時間に基づいてきめ細かい位置を計算することにより、次のティックまでできるだけ多くのフレームをレンダリングできます。ティック(あなたは常にあなたがどこにいるのか-位置、そしてあなたが向かっている場所-速度を知っているので)

そうすれば、CPU /プラットフォームに関係なく同じ「アニメーション速度」が得られますが、CPUが高速であると、異なるティック間でより多くのレンダリングが実行されるため、多少滑らかになります。

編集

一部のコピー&ペースト/概念コード-ただし、これはAWTおよびJ2SEであり、Androidではないことに注意してください。ただし、概念として、またいくつかのAndroid化により、ロジック/更新で行われる計算が重すぎない限り、このアプローチはスムーズにレンダリングされるはずです(たとえば、衝突検出用のN ^ 2アルゴリズム、パーティクルシステムなどでNが大きくなるなど) )。

アクティブなレンダリングループ

ペイントを行うために再ペイントに依存する代わりに(OSの実行内容によっては時間がかかる場合があります)、最初のステップは、レンダリングループをアクティブに制御し、レンダリングしてからアクティブに「表示」するBufferStrategyを使用することです。 「終わったら、また戻る前の内容。

バッファ戦略

始めるには特別なAndroidのものが必要かもしれませんが、それはかなり簡単です。 bufferStrategyに2ページを使用して、「ページめくり」メカニズムを作成します。

try
{
     EventQueue.invokeAndWait(new Runnable() {
        public void run()
        {
            canvas.createBufferStrategy(2);
        }
    });
}    
catch(Exception x)
{
    //BufferStrategy creation interrupted!        
}

メインアニメーションループ

次に、メインループで、戦略を取得し、アクティブな制御を行います(再描画は使用しないでください)。

long previousTime = 0L;
long passedTime = 0L;

BufferStrategy strategy = canvas.getBufferStrategy();

while(...)
{
    Graphics2D bufferGraphics = (Graphics2D)strategy.getDrawGraphics();

    //Ensure that the bufferStrategy is there..., else abort loop!
    if(strategy.contentsLost())
        break;

    //Calc interpolation value as a double value in the range [0.0 ... 1.0] 
    double interpolation = (double)passedTime / (double)desiredInterval;

    //1:st -- interpolate all objects and let them calc new positions
    interpolateObjects(interpolation);

    //2:nd -- render all objects
    renderObjects(bufferGraphics);

    //Update knowledge of elapsed time
    long time = System.nanoTime();
    passedTime += time - previousTime;
    previousTime = time;

    //Let others work for a while...
    Thread.yield();

    strategy.show();
    bufferGraphics.dispose();

    //Is it time for an animation update?
    if(passedTime > desiredInterval)
    {
        //Update all objects with new "real" positions, collision detection, etc... 
        animateObjects();

        //Consume slack...
        for(; passedTime > desiredInterval; passedTime -= desiredInterval);
    }
}

上記のメインループで管理されているオブジェクトは、次のようになります。

public abstract class GfxObject
{
    //Where you were
    private GfxPoint oldCurrentPosition;

    //Current position (where you are right now, logically)
    protected GfxPoint currentPosition;

    //Last known interpolated postion (
    private GfxPoint interpolatedPosition;

    //You're heading somewhere?
    protected GfxPoint velocity;

    //Gravity might affect as well...?
    protected GfxPoint gravity;

    public GfxObject(...)
    {
        ...
    }

    public GfxPoint getInterpolatedPosition()
    {
        return this.interpolatedPosition;
    }

    //Time to move the object, taking velocity and gravity into consideration
    public void moveObject()
    {
        velocity.add(gravity);
        oldCurrentPosition.set(currentPosition);
        currentPosition.add(velocity);
    }

    //Abstract method forcing subclasses to define their own actual appearance, using "getInterpolatedPosition" to get the object's current position for rendering smoothly...
    public abstract void renderObject(Graphics2D graphics, ...);

    public void animateObject()
    {
        //Well, move as default -- subclasses can then extend this behavior and add collision detection etc depending on need
        moveObject();
    }

    public void interpolatePosition(double interpolation)
    {
        interpolatedPosition.set(
                                 (currentPosition.x - oldCurrentPosition.x) * interpolation + oldCurrentPosition.x,
                                 (currentPosition.y - oldCurrentPosition.y) * interpolation + oldCurrentPosition.y);
    }
}

すべての2D位置は、倍精度のGfxPointユーティリティクラスを使用して管理されます(補間された動きは非常に細かく、実際のグラフィックをレンダリングするまで丸めは通常必要ないため)。必要な数学を単純化し、コードを読みやすくするために、さまざまなメソッドも追加しました。

public class GfxPoint
{
    public double x;
    public double y;

    public GfxPoint()
    {
        x = 0.0;
        y = 0.0;
    }

    public GfxPoint(double init_x, double init_y)
    {
        x = init_x;
        y = init_y;
    }

    public void add(GfxPoint p)
    {
        x += p.x;
        y += p.y;
    }

    public void add(double x_inc, double y_inc)
    {
        x += x_inc;
        y += y_inc;
    }

    public void sub(GfxPoint p)
    {
        x -= p.x;
        y -= p.y;
    }

    public void sub(double x_dec, double y_dec)
    {
        x -= x_dec;
        y -= y_dec;
    }

    public void set(GfxPoint p)
    {
        x = p.x;
        y = p.y;
    }

    public void set(double x_new, double y_new)
    {
        x = x_new;
        y = y_new;
    }

    public void mult(GfxPoint p)
    {
        x *= p.x;
        y *= p.y;
    }



    public void mult(double x_mult, double y_mult)
    {
        x *= x_mult;
        y *= y_mult;
    }

    public void mult(double factor)
    {
        x *= factor;
        y *= factor;
    }

    public void reset()
    {
        x = 0.0D;
        y = 0.0D;
    }

    public double length()
    {
        double quadDistance = x * x + y * y;

        if(quadDistance != 0.0D)
            return Math.sqrt(quadDistance);
        else
            return 0.0D;
    }

    public double scalarProduct(GfxPoint p)
    {
        return scalarProduct(p.x, p.y);
    }

    public double scalarProduct(double x_comp, double y_comp)
    {
        return x * x_comp + y * y_comp;
    }

    public static double crossProduct(GfxPoint p1, GfxPoint p2, GfxPoint p3)
    {
        return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
    }

    public double getAngle()
    {
        double angle = 0.0D;

        if(x > 0.0D)
            angle = Math.atan(y / x);
        else if(x < 0.0D)
            angle = Math.PI + Math.atan(y / x);
        else if(y > 0.0D)
            angle = Math.PI / 2;
        else
            angle = - Math.PI / 2;

        if(angle < 0.0D)
            angle += 2 * Math.PI;
        if(angle > 2 * Math.PI)
            angle -= 2 * Math.PI;

        return angle;
    }
}
6

サイズについてはこれを試してみてください。キャンバスを同期してロックするのは最短時間だけであることに気付くでしょう。それ以外の場合、OSはA)遅すぎたためにバッファをドロップするか、B)スリープ待機が終了するまでまったく更新しません。

public class MainThread extends Thread
{
    public static final String TAG = MainThread.class.getSimpleName();
    private final static int    MAX_FPS = 60;   // desired fps
    private final static int    MAX_FRAME_SKIPS = 5;    // maximum number of frames to be skipped
    private final static int    FRAME_PERIOD = 1000 / MAX_FPS;  // the frame period

    private boolean running;


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

    private SurfaceHolder mSurfaceHolder;
    private MainGameBoard mMainGameBoard;

    public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
        super();
        mSurfaceHolder = surfaceHolder;
        mMainGameBoard = gameBoard;
    }

    @Override
    public void run()
    {
        Log.d(TAG, "Starting game loop");
        long beginTime;     // the time when the cycle begun
        long timeDiff;      // the time it took for the cycle to execute
        int sleepTime;      // ms to sleep (<0 if we're behind)
        int framesSkipped;  // number of frames being skipped 
        sleepTime = 0;

        while(running)
        {
            beginTime = System.currentTimeMillis();
            framesSkipped = 0;
            synchronized(mSurfaceHolder){
                Canvas canvas = null;
                try{
                    canvas = mSurfaceHolder.lockCanvas();
                    mMainGameBoard.update();
                    mMainGameBoard.render(canvas);
                }
                finally{
                    if(canvas != null){
                        mSurfaceHolder.unlockCanvasAndPost(canvas);
                    }
                }
            }
            timeDiff = System.currentTimeMillis() - beginTime;
            sleepTime = (int)(FRAME_PERIOD - timeDiff);
            if(sleepTime > 0){
                try{
                    Thread.sleep(sleepTime);
                }
                catch(InterruptedException e){
                    //
                }
            }
            while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                // catch up - update w/o render
                mMainGameBoard.update();
                sleepTime += FRAME_PERIOD;
                framesSkipped++;
            }
        }
    }
}
4
Simon

まず第一に、Canvasはパフォーマンスが低下する可能性があるため、あまり期待しないでください。 SDKのlunarlanderの例を試して、ハードウェアでどのようなパフォーマンスが得られるかを確認することをお勧めします。

最大fpsを30程度に下げてみてください。目標は、高速ではなくスムーズにすることです。

_ private final static int    MAX_FPS = 30;   // desired fps
_

また、スリープ呼び出しを取り除きます。キャンバスへのレンダリングはおそらく十分にスリープします。次のようなものを試してください。

_        synchronized (mSurfaceHolder) {
            beginTime = System.currentTimeMillis();
            framesSkipped = 0;

            timeDiff = System.currentTimeMillis() - beginTime;

            sleepTime = (int) (FRAME_PERIOD - timeDiff);

            if(sleepTime <= 0) {

                this.mMainGameBoard.update();

                this.mMainGameBoard.render(mCanvas);

            }
        }
_

必要に応じて、レンダリングよりも頻繁にthis.mMainGameBoard.update()を実行できます。

編集:また、障害物が現れると物事が遅くなると言うので。それらをオフスクリーンのCanvas /ビットマップに描画してみてください。一部のdrawSHAPEメソッドはCPUに最適化されており、ハードウェア/ GPUで高速化されていないため、オフラインのキャンバス/ビットマップに描画するパフォーマンスが向上すると聞きました。

Edit2:Canvas.isHardwareAccelerated()は何を返しますか?

1
Chase

ゲームの速度低下とスタッターの最も一般的な原因の1つは、グラフィックスパイプラインです。ゲームロジックは、(一般的に)描画するよりも処理がはるかに高速であるため、可能な限り最も効率的な方法ですべてを描画するようにします。以下に、これを実現するためのヒントをいくつか示します。

それをより良くするためのいくつかの提案

https://www.yoyogames.com/tech_blog/

0
Arjun Chaudhary