gpt4 book ai didi

java - android游戏中令人讨厌的滞后/口吃

转载 作者:行者123 更新时间:2023-12-02 09:40:08 30 4
gpt4 key购买 nike

我刚开始在android中进行游戏开发,并且正在开发一款 super 简单的游戏。

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

我设法使所有工作正常进行,但是却遇到了很多困难和滞后。

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

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

滞后主要发生在角色穿过障碍物时(有时也越过障碍物)。发生的情况是,在游戏状态的每个绘制中都有不均匀的延迟,从而导致 Action 停顿。

  • GC无法根据日志运行。
  • 结结不是由速度过高引起的(我知道这是因为在游戏开始时,当障碍物不在视野中时,角色会平稳移动)
  • 我也不认为问题也与FPS有关,因为即使将MAX_FPS字段设置为100,仍然会出现卡顿现象。

  • 我的想法是,一行或多行代码会导致某种程度的延迟发生(从而导致跳过帧)。我还认为这些行应该围绕 update()draw()PlayerCharacterObstacleMainGameBoard方法。

    问题是,我仍然对android开发和android 游戏开发特别陌生,所以我不知道是什么原因导致了这种延迟。

    我尝试在网上寻找答案...不幸的是,我发现所有这些都指向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;
    }

    最佳答案

    更新:如此详细,它几乎没有爬取表面。更加详细的解释是now available。游戏循环建议位于附录A中。如果您确实想了解发生了什么,请从此开始。

    原始帖子如下...

    我将从胶囊摘要开始,介绍Android中的图形管道如何工作。您可以找到更彻底的处理方法(例如,一些非常详细的Google I/O对话),所以我的意思是正确的。结果比我预期的要长,但是我一直想写一些这样的东西。

    SurfaceFlinger

    您的应用程序不使用Framebuffer。有些设备甚至没有帧缓冲。您的应用程序包含BufferQueue对象的“生产者”端。完成渲染帧后,它将调用unlockCanvasAndPost()eglSwapBuffers(),它们将已完成的缓冲区排队等待显示。 (从技术上讲,渲染可能要到您告诉它交换之后才可能开始,并且可以在缓冲区通过管道移动时继续进行,但这是另一回事了。)

    缓冲区被发送到队列的“消费者”端,在本例中为系统表面合成器SurfaceFlinger。缓冲区通过句柄传递;内容不会被复制。每次显示刷新(我们称其为“VSYNC”)开始时,SurfaceFlinger都会查看所有各种队列,以查看可用的缓冲区。如果找到新内容,它将锁存该队列中的下一个缓冲区。如果没有,它将使用以前得到的任何东西。

    然后将具有可见内容的窗口(或“图层”)集合组合在一起。这可以通过SurfaceFlinger(使用OpenGL ES将图层渲染到新缓冲区中)或通过Hardware Composer HAL完成。硬件编辑器(在最新的设备上可用)由硬件OEM提供,并且可以提供许多“重叠”平面。如果SurfaceFlinger具有三个要显示的窗口,并且HWC具有三个可用的覆盖平面,则它将每个窗口置于一个覆盖中,并在显示框架时进行构图。永远不会有一个缓冲区来保存所有数据。通常,这比在GLES中执行相同的操作更有效。 (顺便说一句,这就是为什么您仅通过打开framebuffer开发条目并读取像素就无法在最新设备上截取屏幕截图的原因。)

    这就是消费者方面的样子。您可以使用adb shell dumpsys SurfaceFlinger自己欣赏。让我们回到制作人(即您的应用)。

    生产者

    您正在使用SurfaceView,它包含两个部分:与系统UI一起使用的透明View,以及一个单独的单独的Surface层。 SurfaceView的表面直接进入SurfaceFlinger,这就是为什么它比其他方法(如TextureView)具有更少的开销的原因。
    SurfaceView的表面的BufferQueue是三重缓冲的。这意味着您可以扫描出一个缓冲区以供显示,一个缓冲区位于SurfaceFlinger上等待下一个VSYNC,一个缓冲区供应用程序使用。拥有更多的缓冲区可以提高吞吐量并消除颠簸,但会增加触摸屏和看到更新之间的延迟。在此之上添加整个帧的额外缓冲通常不会带来多大好处。

    如果绘制速度快于显示器可以渲染帧的速度,则最终将填满队列,并且缓冲区交换调用(unlockCanvasAndPost())将暂停。这是一种使游戏的更新速率与显示速率相同的简单方法-尽可能快地绘制,并让系统放慢速度。每帧,您都会根据经过的时间前进状态。 (我在Android Breakout中使用了这种方法。)不太正确,但是在60fps时,您不会真正注意到这些缺陷。如果您没有足够长的 sleep 时间,就会在sleep()调用中获得相同的效果-您只会醒来只是等待队列。在这种情况下, hibernate 没有任何优势,因为在队列上 hibernate 同样有效。

    如果绘制速度慢于显示器可渲染帧的速度,则队列最终将耗尽,SurfaceFlinger将在两次连续的显示刷新中显示相同的帧。如果您尝试通过sleep()调用来加快游戏进度,并且 sleep 时间太长,则会定期发生这种情况。出于理论上的原因(很难实现没有反馈机制的PLL)和实际的原因(刷新率会随时间变化,例如我看到它从58fps到在给定设备上为62fps)。

    在游戏循环中使用sleep()调用来加快动画速度是一个坏主意。

    不 sleep

    您有两种选择。您可以使用“尽可能快地绘制,直到备份缓冲区交换调用备份”方法,这是许多基于GLSurfaceView#onDraw()的应用程序所做的事情(无论他们是否知道)。或者,您可以使用Choreographer

    Choreographer允许您设置在下一个VSYNC上触发的回调。重要的是,回调的参数是实际的VSYNC时间。因此,即使您的应用没有立即唤醒,您仍然可以准确地了解显示刷新的开始时间。事实证明,这在更新游戏状态时非常有用。

    更新游戏状态的代码绝不应设计为前进“一帧”。考虑到设备的种类以及单个设备可以使用的刷新率,您不知道什么是“帧”。您的游戏将慢速播放或快慢播放-或如果您很幸运,并且有人尝试在通过HDMI锁定到48Hz的电视上播放它,则您将严重呆滞。您需要确定前一帧与当前帧之间的时间差,并适本地推进游戏状态。

    这可能需要一些精神上的改组,但这是值得的。

    您可以在Breakout中看到这一点,它会根据经过的时间来提高球的位置。它可以将较大的跳跃时间缩短为较小的部分,以使碰撞检测变得简单。 Breakout的问题在于,它使用的是“塞满队列”方法,时间戳可能会因SurfaceFlinger工作所需的时间而异。另外,当缓冲区队列最初为空时,您可以非常快速地提交帧。 (这意味着您计算的两个帧的时间增量几乎为零,但它们仍以60fps的速率发送到显示器。实际上,您不会看到此帧,因为时间戳差异很小,以至于看起来像是同一帧绘制两次,并且只有在从非动画过渡到动画时才会发生,这样您就看不到任何结结。)

    使用Choreographer,您可以获得实际的VSYNC时间,因此可以获得一个不错的常规时钟来作为时间间隔的基础。因为您使用显示器刷新时间作为时钟源,所以您永远不会与显示器不同步。

    当然,您仍然必须准备丢帧。

    无框不留

    不久前,我在Grafika(“记录GL应用程序”)中添加了一个屏幕录制演示,该演示执行非常简单的动画-只是一个平底的反弹矩形和一个旋转的三角形。当编舞者发出信号时,它前进状态并绘制。我对其进行了编码,运行...并开始注意到Choreographer回调已备份。

    在使用systrace进行研究之后,我发现框架UI有时会做一些布局工作(可能与位于SurfaceView表面顶部的UI层中的按钮和文本有关)。通常情况下,这需要6毫秒的时间,但是如果我不主动在屏幕上移动手指,那么Nexus 5会降低各种时钟的频率,以减少功耗并延长电池生命周期。重新布局花了28毫秒。请记住,一个60fps的帧是16.7ms。

    GL渲染几乎是瞬时的,但是Choreographer更新已交付给UI线程,而该UI线程正在布局中工作,因此我的渲染器线程直到很晚才收到信号。 (您可以让Choreographer直接将信号传递到渲染器线程,但如果这样做,Choreographer中有一个bug会导致内存泄漏。)解决方法是,当当前时间在VSYNC时间之后超过15毫秒时,丢弃帧。该应用程序仍会进行状态更新-碰撞检测非常简单,如果您让时间间隔过大,则会发生奇怪的事情-但它不会向SurfaceFlinger提交缓冲区。

    在运行该应用程序时,您可以知道何时丢帧,因为Grafika会闪烁红色边框并更新屏幕上的计数器。观看动画无法分辨。因为状态更新基于时间间隔,而不是帧数,所以所有 Action 的移动速度都与是否丢失帧一样快,并且在60fps时,您不会注意到单个丢失的帧。 (在某种程度上取决于您的眼睛,游戏和显示硬件的特性。)

    关键类(class):

  • 帧丢失可能是由外部因素引起的-依赖于另一个线程,CPU时钟速度,后台gmail同步等。
  • 您无法避免所有丢帧现象。
  • 如果您将绘制循环正确设置,没有人会注意到。

  • 绘画

    如果它是硬件加速的,则渲染到Canvas会非常高效。如果不是这样,并且您正在用软件进行绘图,则可能需要一段时间-尤其是当您触摸大量像素时。

    阅读时需要注意的两个重要方面:了解 hardware-accelerated renderingusing the hardware scaler以减少应用程序需要触摸的像素数量。 Grafika中的“硬件除垢器练习器”将使您了解减小绘图表面尺寸时会发生什么—在效果显着之前,您可以将其变小。 (我发现看着GL在100x64的表面上渲染一个旋转的三角形很奇怪。)

    您还可以通过直接使用OpenGL ES来消除渲染中的一些奥秘。学习事物的运行方式有些困难,但是Breakout(以及更详细的示例 Replica Island)显示了简单游戏所需的一切。

    关于java - android游戏中令人讨厌的滞后/口吃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21838523/

    30 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com