19

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

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

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

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

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

延迟主要发生在角色通过障碍物时(有时也会在障碍物之后一点点)。发生的情况是,每次绘制游戏状态时都会出现不均匀的延迟,从而导致动作卡顿。

  • GC 没有按照日志运行。
  • 口吃不是因为速度太快造成的(我知道是因为在游戏开始时,当障碍物不在视线范围内时,角色移动很顺畅)
  • 我不认为这个问题也与 FPS 相关,因为即使 MAX_FPS 字段设置为 100,仍然存在口吃。

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

问题是,我对 android 开发和 android游戏开发还是新手,所以我不知道是什么导致了这种延迟。

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

所以,请帮我找到解决这些恼人滞后问题的答案!

一些代码

主线程.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;
}

障碍物.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;
}
4

5 回答 5

20

更新:尽管如此详细,但它几乎没有触及表面。现在有更详细的解释。游戏循环建议在附录 A 中。如果您真的想了解发生了什么,请从它开始。

原帖如下...


我将首先简要介绍 Android 中图形管道的工作原理。你可以找到更彻底的处理方法(例如一些非常详细的 Google I/O 演讲),所以我只是达到了高潮。结果比我预期的要长,但我一直想写一些。

SurfaceFlinger

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

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

然后将具有可见内容的窗口(或“层”)的集合组合在一起。这可以通过 SurfaceFlinger(使用 OpenGL ES 将图层渲染到新缓冲区中)或通过 Hardware Composer HAL 来完成。硬件合成器(在最新的设备上可用)由硬件 OEM 提供,并且可以提供许多“覆盖”平面。如果 SurfaceFlinger 有三个窗口要显示,而 HWC 有三个可用的覆盖平面,它会将每个窗口放在一个覆盖中,并在显示框架时进行合成。从来没有一个缓冲区可以保存所有数据。这通常比在 GLES 中做同样的事情更有效率。(顺便说一句,这就是为什么你可以'

所以这就是消费者方面的样子。你可以自己欣赏它adb shell dumpsys SurfaceFlinger。让我们回到生产者(即您的应用程序)。

生产者

您正在使用一个SurfaceView,它有两个部分:一个与系统 UI 一起存在的透明视图,以及一个单独的 Surface 层。的SurfaceView表面直接进入 SurfaceFlinger,这就是为什么它的开销比其他方法(如TextureView)要少得多。

的表面的 BufferQueueSurfaceView是三重缓冲的。这意味着您可以扫描出一个缓冲区用于显示,一个位于 SurfaceFlinger 等待下一个 VSYNC 的缓冲区,以及一个供您的应用程序使用的缓冲区。拥有更多缓冲区可以提高吞吐量并消除颠簸,但会增加触摸屏幕和看到更新之间的延迟。在此之上添加整个帧的额外缓冲通常不会对你有多大好处。

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

如果你绘制的速度比显示器可以渲染的帧慢,队列最终会干涸,SurfaceFlinger 会在两次连续的显示刷新时显示相同的帧。如果您尝试通过sleep()通话来调整游戏节奏并且睡眠时间过长,这种情况会定期发生。由于理论上的原因(很难实现没有反馈机制的 PLL)和实际的原因(刷新率会随着时间而变化,例如我已经看到它在 58fps 到 62fps 之间变化,所以不可能精确匹配显示刷新率给定的设备)。

在游戏循环中使用sleep()调用来调整动画节奏是个坏主意。

不眠不休

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

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

更新游戏状态的代码永远不应该被设计为推进“一帧”。鉴于设备的多样性,以及单个设备可以使用的各种刷新率,您无法知道“帧”是什么。你的游戏会玩得有点慢或有点快——或者如果你很幸运,有人试图在通过 HDMI 锁定到 48Hz 的电视上玩它,你会非常迟钝。需要确定上一帧和当前帧的时间差,适当推进游戏状态。

这可能需要一些精神上的重新洗牌,但这是值得的。

您可以在Breakout中看到这一点,它根据经过的时间推进球的位置。它将时间上的大跳跃切割成更小的部分,以保持碰撞检测的简单性。Breakout 的问题在于它使用的是填满队列的方法,时间戳会随着 SurfaceFlinger 工作所需时间的变化而变化。此外,当缓冲区队列最初为空时,您可以非常快速地提交帧。(这意味着您计算两个时间增量几乎为零的帧,但它们仍然以 60fps 的速度发送到显示器。实际上您看不到这一点,因为时间戳差异非常小,看起来就像是同一帧绘制了两次,并且仅在从非动画过渡到动画时才会发生,因此您看不到任何口吃。)

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

当然,你还是要做好丢帧的准备。

没有留下任何框架

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

在用systrace挖掘之后,我发现框架 UI 偶尔会做一些布局工作(可能与位于SurfaceView表面顶部的 UI 层中的按钮和文本有关)。通常这需要 6 毫秒,但如果我没有在屏幕上主动移动手指,我的 Nexus 5 会减慢各种时钟以降低功耗并延长电池寿命。重新布局需要 28 毫秒。请记住,60fps 帧是 16.7ms。

GL 渲染几乎是瞬间完成的,但 Choreographer 更新被传递到 UI 线程,该线程在布局上磨掉了,所以我的渲染器线程直到很久以后才收到信号。(您可以让 Choreographer 将信号直接传递到渲染器线程,但如果这样做,Choreographer 中存在一个错误会导致内存泄漏。)修复是在 VSYNC 时间之后的当前时间超过 15 毫秒时丢弃帧。该应用程序仍然进行状态更新——碰撞检测非常初级,如果你让时间间隔变得太大,就会发生奇怪的事情——但它不会向 SurfaceFlinger 提交缓冲区。

在运行应用程序时,您可以知道何时丢帧,因为 Grafika 会闪烁红色边框并更新屏幕上的计数器。看动画是看不出来的。因为状态更新是基于时间间隔,而不是帧数,所以无论帧是否被丢弃,一切都会以同样快的速度移动,并且在 60fps 时您不会注意到单个丢弃帧。(在一定程度上取决于你的眼睛、游戏和显示硬件的特性。)

主要经验教训:

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

绘画

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

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

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

于 2014-03-13T18:21:08.180 回答
6

我从未在 Android 中制作过游戏,但我确实使用 Canvas 和 bufferStrategy 在 Java/AWT 中制作了 2D 游戏......

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

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

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

相反,您应该以您认为代码中的“逻辑”工作所需的任何频率进行更新——通常 10 或 25Hz 就可以了(我称之为“更新滴答声”,这与实际的 FPS 完全不同),而渲染是通过保持高分辨率跟踪时间来测量您的实际渲染“多长时间”来完成的(我使用了 nanoTime,这已经足够了,而 currentTimeInMillis 相当没用......),

这样,您可以在刻度之间插值并渲染尽可能多的帧,直到下一个刻度为止滴答声(因为你总是知道你在哪里 - 位置,你要去哪里 - 速度)

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

编辑

一些复制粘贴/概念代码——但请注意,这是 AWT 和 J2SE,不是 Android。但是,作为一个概念并且通过一些 Android 化,我确信这种方法应该可以顺利渲染,除非在您的逻辑/更新中完成的计算太重(例如,用于碰撞检测的 N^2 算法和 N 随粒子系统等而变大) )。

主动渲染循环

第一步是主动控制渲染循环并使用 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;
    }
}
于 2014-02-17T20:38:53.513 回答
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++;
            }
        }
    }
}
于 2014-02-17T20:44:14.123 回答
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()比渲染更频繁。

编辑:另外,因为你说当障碍物出现时事情会变慢。尝试将它们绘制到屏幕外的画布/位图中。我听说某些 drawSHAPE 方法是 CPU 优化的,您将获得更好的性能将它们绘制到离线画布/位图,因为它们不是硬件/gpu 加速的。

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

于 2014-03-13T01:17:25.210 回答
0

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

一些建议让它变得更好

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

于 2014-03-13T00:17:03.830 回答