安卓游戏中的烦人卡顿/延迟问题

19

我刚开始在Android上进行游戏开发,并且正在制作一个超级简单的游戏。

这个游戏基本上类似于“Flappy Bird”。

我设法让所有东西都能运行,但是我遇到了很多卡顿和延迟。

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

基本上有4个“障碍物”,它们彼此距离一个全屏幅度。
当游戏开始时,障碍物开始以恒定的速度移动(朝向角色)。玩家角色的x值在整个游戏中保持不变,而其y值会发生变化。

当角色通过障碍物时(有时甚至在障碍物之后一点),就会出现卡顿。问题在于每次绘制游戏状态时存在不均匀的延迟,导致运动产生卡顿。

  • 根据日志,GC没有运行。
  • 卡顿不是由于速度过高造成的(我知道这一点,因为在游戏开始时,当障碍物超出视野时,角色的移动非常流畅)
  • 我认为这个问题也与FPS无关,因为即使将MAX_FPS字段设置为100,也仍然会出现卡顿。

我的想法是,有一行或多行代码导致某种延迟发生(从而跳过了帧)。 我也认为这些行应该在PlayerCharacterObstacleMainGameBoardupdate()draw()方法附近。

问题是,我还是新手,在Android开发和特别是Android游戏开发方面,我不知道可能会导致这种延迟的原因。

我尝试在线寻找答案...不幸的是,我找到的所有答案都指向GC有责任。 但是,由于我不认为它是这种情况(如果我错了,请纠正我),所以这些答案对我没有用。我还阅读了Android开发人员的性能提示页面,但未找到任何有用的信息。

所以,请帮助我找到解决这些烦人卡顿的答案!

一些代码

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

}

玩家角色.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;
}

使用例如DDMS方法分析的配置文件:http://software.intel.com/en-us/articles/performance-debugging-of-android-applications 或其他技术。无论是日志还是某些神奇的最大帧率(最大并不意味着它总是以那么快的速度运行,只是达到了上限)都不能告诉你一个合适的原因。 - zapl
你的 Obstacle.java 和 PlayerCharacter.java 类缺少构造函数。 - tpbapp
1
当主线程正在睡眠时,肯定不应该使用synchronize并锁定画布。这样做会引发各种问题。你的动画循环并不是很合理。 - Gene
5个回答

21

更新:虽然这篇文章详细,但仅仅触及了表面。现在有更加详细的解释。游戏循环建议在附录A中。如果你真的想理解发生了什么,就从那里开始。

以下是原始帖子...


I'm going to start with a capsule summary of how the graphics pipeline in Android works. You can find more thorough treatments (e.g. some nicely detailed Google I/O talks), so I'm just hitting the high points. This turned out rather longer than I expected, but I've been wanting to write some of this up for a while.
SurfaceFlinger Your application does not draw on The Framebuffer. Some devices don't even have The Framebuffer. Your application holds the "producer" side of a BufferQueue object. When it has completed rendering a frame, it calls unlockCanvasAndPost() or eglSwapBuffers(), which queues up the completed buffer for display. (Technically, rendering may not even begin until you tell it to swap and can continue while the buffer is moving through the pipeline, but that's a story for another time.)
缓冲区被发送到队列的“消费者”端,本例中为SurfaceFlinger,即系统表面合成器。 缓冲区通过句柄传递;内容不被复制。 每次显示刷新(称为“VSYNC”)开始时,SurfaceFlinger查看所有各种队列以查看可用的缓冲区。 如果找到新内容,则从该队列中获取下一个缓冲区。 如果没有,则使用先前获取的任何内容。
具有可见内容的窗口集合(或“图层”)然后组合在一起。 这可以通过SurfaceFlinger(使用OpenGL ES将图层渲染到新缓冲区中)或通过硬件组合器HAL完成。 硬件组合器(在大多数最新设备上可用)由硬件OEM提供,并且可以提供多个“叠加”平面。 如果SurfaceFlinger有三个要显示的窗口,并且HWC有三个叠加平面可用,则将每个窗口放入一个叠加平面,并在帧被显示时执行组合。 没有保持所有数据的缓冲区。 这通常比在GLES中执行相同操作更有效率。(顺便说一句,这就是为什么不能通过简单打开framebuffer dev条目并读取像素来在大多数最新设备上抓取屏幕截图的原因。)
这就是消费者端的样子。您可以使用adb shell dumpsys SurfaceFlinger自行欣赏。现在让我们回到生产者(即您的应用程序)。

生产者

您正在使用SurfaceView,它有两个部分:一个透明的视图与系统 UI 一起存在,以及一个单独的 Surface 层。 SurfaceView 的 Surface 直接传输到 SurfaceFlinger,这就是为什么它比其他方法(如TextureView)具有更少的开销。 SurfaceView 的 surface 的 BufferQueue 是三重缓冲的。这意味着您可以有一个正在扫描显示器的缓冲区,一个在 SurfaceFlinger 等待下一个 VSYNC 的缓冲区,以及一个供您的应用程序绘制的缓冲区。拥有更多的缓冲区可以提高吞吐量并平滑颠簸,但会增加触摸屏幕和看到更新之间的延迟。在此基础上添加整个帧的额外缓冲区通常不会对您有太大好处。
如果你的绘制速度比显示器渲染帧率快,那么你最终会填满队列,而你的缓冲区交换调用(unlockCanvasAndPost())将会暂停。这是让你的游戏更新速率与显示速率相同的简单方法 -- 尽可能快地绘制,让系统减速。每一帧,你根据经过的时间推进状态。(我在Android Breakout中使用了这种方法。)虽然不完美,但在60fps下你不会真正注意到缺陷。如果你不睡眠足够长的时间,你也可以通过sleep()调用获得相同的效果 -- 你只会在等待队列上醒来。在这种情况下,睡眠没有优势,因为在队列上睡眠同样有效。
如果你的绘制速度比显示器渲染帧的速度慢,队列最终会耗尽,并且SurfaceFlinger将在连续两次显示刷新中显示相同的帧。如果你试图使用sleep()调用来控制游戏节奏并且睡眠时间过长,这种情况会定期发生。由于理论原因(没有反馈机制很难实现PLL)和实际原因(刷新率可能随时间变化,例如,在给定设备上,我看到它的变化从58fps到62fps),因此无法精确匹配显示器的刷新率。
在游戏循环中使用sleep()调用来控制动画节奏是一个不好的想法。
不使用sleep()的方法
你有几个选择。你可以使用“尽可能快地绘制,直到缓冲区交换调用被阻塞”的方法,这是许多基于GLSurfaceView#onDraw()的应用程序所采用的方法(无论他们是否知道)。或者,你可以使用Choreographer
Choreographer允许您设置一个回调函数,该函数在下一次VSYNC时触发。重要的是,回调函数的参数是实际的VSYNC时间。因此,即使您的应用程序不立即唤醒,您仍然可以准确地了解显示刷新开始的时间。这在更新游戏状态时非常有用。
更新游戏状态的代码不应设计为推进“一个帧”。由于设备的多样性和单个设备可以使用的刷新率的多样性,您无法知道“帧”是什么。您的游戏将略微变慢或略微加快 - 或者如果你很幸运,有人尝试在通过HDMI锁定为48Hz的电视上播放它,你将会非常缓慢。您需要确定前一帧和当前帧之间的时间差,并适当地推进游戏状态。
这可能需要进行一些思维重组,但这是值得的。
您可以在 打砖块游戏 中看到这一操作,它根据经过的时间来推进球的位置。为了保持碰撞检测的简单性,它将大的时间跳跃分成较小的部分。然而,打砖块游戏存在一个问题,即采用了“填满队列”的方法,时间戳会受SurfaceFlinger执行工作所需时间变化的影响。此外,当缓冲队列最初为空时,您可以非常快地提交帧。(这意味着您计算出近乎零时间差的两个帧,但它们仍以60fps的速度发送到显示器。实际上,您看不到这一点,因为时间戳差异非常小,只是看起来像同一帧被绘制了两次,并且只发生在从非动画转换为动画时,因此您看不到任何卡顿。)
使用Choreographer,您可以获得实际的VSYNC时间,因此您可以获得一个漂亮的正则时钟来基于时间间隔。因为您使用显示刷新时间作为时钟源,所以您永远不会与显示器失去同步。
当然,您仍然必须准备好丢帧。

没有落下的帧

之前,我在Grafika中添加了一个屏幕录制演示("Record GL app"),它展示了非常简单的动画 -- 只有一个平面阴影反弹矩形和一个旋转三角形。它在Choreographer信号时提升状态并绘制。我编码运行它...并开始注意到Choreographer回调备份。
通过systrace挖掘之后,我发现框架UI偶尔会进行一些布局工作(可能与UI层中的按钮和文本有关,该层位于SurfaceView表面之上)。通常需要6ms,但如果我没有在屏幕上活动手指,我的Nexus 5会减缓各种时钟以减少功耗并提高电池寿命。重新布局需要28ms。请记住,60fps帧是16.7ms。
GL渲染几乎是即时的,但Choreographer更新被传递到UI线程,而UI线程正在处理布局,因此我的渲染器线程直到很久以后才收到信号。(您可以让Choreographer直接将信号传递给渲染器线程,但如果这样做,Choreographer中会出现一个导致内存泄漏的错误。) 解决方法是在当前时间超过VSYNC时间15ms以上时丢帧。应用程序仍然进行状态更新——碰撞检测非常简单,如果时间间隔变得太大,就会发生奇怪的事情——但它不会向SurfaceFlinger提交缓冲区。
在运行应用程序时,您可以通过Grafika闪烁边框并更新屏幕上的计数器来确定何时丢帧。您无法通过观察动画来判断。因为状态更新基于时间间隔而不是帧数,所以无论是否丢帧,一切都像帧数没有丢失一样快,并且在60fps下,您不会注意到单个丢帧。(在一定程度上取决于您的眼睛、游戏和显示硬件的特性。)
主要教训:
  • 帧率降低可能是由于外部因素引起的--依赖于另一个线程、CPU时钟速度、后台gmail同步等。
  • 你无法避免所有的帧率降低。
  • 如果你正确设置了你的绘图循环,没有人会注意到。

绘画

如果硬件加速,渲染到Canvas可以非常高效。如果不是,并且你在软件中进行绘制,可能需要一段时间--特别是如果你触摸了很多像素。

两个重要的阅读材料:了解硬件加速渲染使用硬件缩放器来减少应用程序需要触摸的像素数量。Grafika中的“硬件缩放器练习器”将让你了解当你缩小绘图表面的大小时会发生什么--你可以变得非常小,直到效果变得明显为止。(我觉得看GL在100x64的表面上渲染旋转的三角形有点有趣。)

你也可以直接使用OpenGL ES来消除渲染的一些神秘感。学习如何工作可能会有些困难,但是Breakout(以及更为复杂的Replica Island)展示了制作简单游戏所需的一切。

非常好的解释和精彩的文章。谢谢。您知道在渲染线程上使用Choreographer回调时内存泄漏问题是否仍然存在吗? - Maria
另外,您如何将Choreographer回调与使用NativeAcivity和本地OpenGL ES渲染的NDK应用程序结合起来?我想回调应该在Java代码中的looper线程上到达,并通过JNI转发到本机,从而应该向本机渲染线程发出信号? - Maria
@Maria:我不知道内存泄漏的状态。它可能已经被Android 5.0修复了,但在旧设备上仍然存在问题。Grafika(https://github.com/google/grafika/blob/master/src/com/android/grafika/ChorTestActivity.java)中的“{~ignore} Chor test”是为了测试其行为而编写的。如果我没记错的话,它会防止Activity在切换时被释放。对于NDK,正如你所说,你需要在Java代码中编写回调并向本地线程发出信号。 - fadden

6

虽然我从未在Android上制作过游戏,但我已经使用Canvas和bufferStrategy在Java / AWT中制作了2D游戏...

如果您遇到闪烁问题,您可以通过渲染到离屏Image,然后直接进行页面翻转/ drawImage以新预先呈现的内容来手动双缓冲(消除闪烁)。

但是,我感觉你更关心动画的“流畅性”,在这种情况下,我建议您在不同动画间隔之间扩展代码以进行插值;

目前,您的渲染循环在相同的速度下更新逻辑状态(逻辑移动),并测量一些参考时间并尝试跟踪经过的时间。

相反,您应该以您认为适合“逻辑”工作的频率进行更新--通常10或25Hz就足够了(我称之为“更新ticks”,这与实际FPS完全不同),而渲染是通过保持高分辨率跟踪时间来完成的,以测量您的实际渲染需要多长时间(我使用了nanoTime,这已经足够了,而currentTimeInMillis则相当无用...),

通过计算自上一个tick以来经过的时间与两个tick之间应该经过多长时间(因为您总是知道您在哪里-位置和您正在前往的地方-速度)之间的差异,可以在ticks之间进行插值,并在下一个tick之前尽可能多地呈现帧,通过计算基于时间的精细位置来计算精细位置。

这样,您将获得相同的“动画速度”,而不受CPU /平台的限制,但更或者不会有更多的平滑性,因为更快的CPU将在不同的tick之间执行更多的渲染。

编辑

一些复制/粘贴/概念代码--但请注意,这是AWT和J2SE,而不是Android。然而,作为一个概念,并且使用一些Android化,我相信这种方法应该可以平稳地呈现,除非您在逻辑/更新中进行的计算过重(例如,N ^ 2碰撞检测算法以及N增长大型粒子系统等)。

活动渲染循环

与依赖repaint为您绘制不同(取决于操作系统正在执行什么),第一步是对渲染循环进行积极控制,并使用BufferStrategy,在完成渲染后主动“显示”内容,然后再次开始。

缓冲策略

可能需要一些特殊的Android内容才能启动,但这是相当简单的。我使用2页缓冲策略来创建“翻页”机制。

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

主要动画循环

然后,在你的主循环中,获取策略并进行主动控制(不要使用repaint)!

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

你好,你的回答听起来很可行!你能否进一步编辑并提供一些例子呢?我没有真正理解第五六段。谢谢! - Asaf
2
嗨 - 我在上面编辑了我的答案,其中包含我在“那时候”游戏中使用的代码/概念方法 ;) 我将动画设置为10Hz(逻辑步骤/滴答声),而渲染则以60FPS流畅无比,并且我还进行了一些严格的碰撞检测/弹跳计算。这种方法是好的,我敢说,并且已经被证明作为插值的概念在所有“专业”游戏中都得到了应用,以将逻辑位置与渲染位置分离,并在慢速和快速平台上实现相同的“游戏速度”。祝你好运;) - Markus Millfjord
我明白了,谢谢!那似乎是一个非常好的方法,但如果没有BufferStrategy,它就行不通了,对吧?据我所知,在Android中没有相当于它的东西...至少在我和Google所知道的范围内。 - Asaf
出于好奇我自己进行了一些谷歌搜索,没错——你是对的。似乎OpenGL是正确的选择——你可以采用相同的插值概念来实现非常平滑的动画,但是设置活动渲染循环的“如何”逻辑需要以不同的方式完成。https://dev59.com/mGoy5IYBdhLWcg3wfOB4 - Markus Millfjord

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

谢谢。在测试您的代码后,我注意到现在我的对象速度变得不一致了。我猜这是因为屏幕更新尽可能快而不是在固定间隔内更新的原因。 - Asaf
1
@xTCx 你应该存储屏幕最后更新的时间,并使用当前时间减去上次更新时间来获取一个增量时间,作为计算的基础。此外,您当前的逻辑以1像素的精度计算位置,您应该提供子像素精度(可以通过在计算时将所有值向左移10位来实现)。这样绘制的对象就不会例如每隔一次刷新就多移动1个像素。更多信息:http://stackoverflow.com/questions/7923349/slick2d-game-speed-changing - Kai
避免在onDraw方法中进行任何对象分配和计算。onDraw方法只应该用于绘制操作。在onDraw方法内部进行的任何操作都会减慢绘制速度。预先缓存Paint对象和尺寸。 - Gaskoin

1
首先,Canvas 的性能可能会比较差,所以不要期望太高。你可以尝试从 SDK 中获取 lunarlander 示例,并查看在你的硬件上的表现如何。
尝试将最大帧率降低到 30 左右,目标是流畅而非快速。
 private final static int    MAX_FPS = 30;   // desired fps

也要摆脱sleep调用,向画布进行渲染可能足够休眠。尝试使用类似于以下的东西:
        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(),而不是渲染。
编辑:另外,由于你说当障碍物出现时会变慢。尝试将它们绘制到离屏画布/位图上。我听说一些drawSHAPE方法是CPU优化过的,如果将它们绘制到离线画布/位图上,你会获得更好的性能,因为这些不是硬件/GPU加速的。
编辑2:Canvas.isHardwareAccelerated()对你返回了什么?

0
游戏中最常见的卡顿和减速原因之一是图形管道。通常情况下,游戏逻辑处理速度比绘制速度快得多,因此您需要确保以最有效的方式绘制所有内容。以下是一些有关如何实现这一目标的建议。
一些建议可以使其更好。

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


尝试这个,可能是手机的问题。http://www.redmondpie.com/how-to-fix-lag-in-android-games-with-this-handy-tweak/ - Arjun Chaudhary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接