在安卓游戏中令人讨厌的滞后/口吃

时间:2014-02-17 20:19:19

标签: java android surfaceview game-loop flappy-bird-clone

我刚刚开始在Android中进行游戏开发,而我正在开发一款超级简单的游戏。

游戏基本上就像飞扬的小鸟。

我设法让一切工作,但我得到了很多口吃和滞后。

我用于测试的手机是LG G2,所以它应该并且确实运行的游戏比这更重,更复杂。

基本上有4个“障碍物”是相互分开的全屏宽度 当游戏开始时,障碍物开始以恒定速度移动(朝向角色)。玩家角色的x值在整个游戏中是一致的,而其y值会发生变化。

滞后主要发生在角色穿过障碍物时(有时也会在障碍物之后)。发生的事情是,每个游戏状态图中都存在不均匀的延迟,导致运动中出现断断续续的情况。

  • GC不会根据日志运行。
  • 口吃不是由速度过高造成的(我知道因为在游戏开始时障碍物不在视线范围内,角色移动顺畅)
  • 我不认为问题也与FPS有关,因为即使MAX_FPS字段设置为100,仍然会有口吃。

我的想法是,有一行或多行代码会导致某种延迟发生(因此会跳过帧)。我还认为这些行应该围绕update()draw()PlayerCharacter的{​​{1}}和Obstacle方法。

问题是,我还是专门针对Android开发和Android 游戏开发的新手,所以我不知道是什么原因导致这种延迟。

我试着在网上寻找答案......不幸的是,我发现所有这些都指向了GC。但是,我不相信这种情况(如果我错了,请纠正我)这些答案对我不适用。我还阅读了android开发人员的MainGameBoard页面,但找不到任何帮助。

所以,请帮助我找到解决这些令人讨厌的滞后的答案!

一些代码

MainThread.java:

Performance Tips

MainGameBoard.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);
        }
    }
}
}

PlayerCharacter.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;
    }
}

}

Obstacle.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;
}

5 个答案:

答案 0 :(得分:20)

答案 1 :(得分:6)

如果没有在Android上制作游戏,我确实使用Canvas和bufferStrategy在Java / AWT中制作了2D游戏......

如果您遇到闪烁,您可以通过渲染到屏幕外的图像,然后直接使用新的预渲染内容进行翻页/绘图,来获取手动双缓冲(摆脱闪烁)。

但是,我觉得你更关心动画中的“平滑度”,在这种情况下,我建议你在不同的动画刻度之间用插值扩展你的代码;

目前,渲染循环以与渲染相同的速度更新逻辑状态(逻辑移动),并使用一些参考时间进行测量并尝试跟踪传递的时间。

相反,您应该以您认为代码中“逻辑”所需的频率更新 - 通常10或25Hz就好了(我称之为“更新刻度”,这与实际完全不同FPS),而渲染是通过保持高分辨率的时间跟踪来测量你的实际渲染“需要多长时间”(我已经使用过nanoTime并且已经足够了,而currentTimeInMillis相当无用......),< / p>

通过这种方式,您可以在刻度线之间进行插值并渲染尽可能多的帧,直到下一次刻度,根据自上次刻度后经过的时间计算细粒度位置,与“应该”的时间相比较在两个刻度之间(因为你总是知道你在哪里 - 位置,以及你前进的位置 - 速度)

这样,无论CPU /平台如何,您都将获得相同的“动画速度”,但或多或​​少的平滑度,因为更快的CPU将在不同的刻度之间执行更多渲染。

修改

一些复制粘贴/概念代码 - 但要注意这是AWT和J2SE,没有Android。然而,作为一个概念和一些Android化,我确信这种方法应该顺利呈现,除非你的逻辑/更新中的微积分太重(例如,用于碰撞检测的N ^ 2算法和N随着粒子系统等而变大) )。

有效渲染循环

而不是依赖重绘为你做绘画(可能需要不同的时间,取决于操作系统正在做什么),第一步是主动控制渲染循环并使用BufferStrategy渲染,然后在你完成之后,主动“显示”内容,然后再回到它。

缓冲策略

可能需要som特殊的Android内容才能开始,但这是相当直接的。我使用2页的bufferStrategy来创建一个“页面翻转”机制。

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);
    }
}

使用具有双精度的GfxPoint实用程序类来管理所有2D位置(因为插值运动可能非常精细,并且在渲染实际图形之前通常不需要舍入)。为了简化所需的数学内容并使代码更具可读性,我还添加了各种方法。

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;
    }
}

答案 2 :(得分:4)

尝试使用此尺寸。您会注意到您只在最短的时间内同步并锁定画布。否则操作系统将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++;
            }
        }
    }
}

答案 3 :(得分:1)

首先,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 / Bitmap。我听说有些drawSHAPE方法是经过CPU优化的,你可以更好地将它们绘制到离线画布/位图,因为它们不是硬件/ gpu加速的。

Edit2:Canvas.isHardwareAccelerated()为您返回什么?

答案 4 :(得分:0)

游戏中减速和口吃的最常见原因之一是图形管道。游戏逻辑的处理速度比绘制(通常)快得多,因此您需要确保以最有效的方式绘制所有内容。您可以在下面找到有关如何实现此目标的一些提示。

一些建议使其更好

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