Asa*_*saf 18 java android surfaceview game-loop flappy-bird-clone
我刚开始在android中进行游戏开发,而我正在开发一款超级简单的游戏.
这场比赛基本上就像飞扬的小鸟.
我设法让一切工作,但我得到了很多口吃和滞后.
我用来测试的手机是LG G2,所以它应该并且确实运行的游戏比这更重,更复杂.
基本上有4个"障碍"是全屏宽度彼此分开.
当游戏开始时,障碍物开始以恒定速度移动(朝向角色).玩家角色的x值在整个游戏中是一致的,而其y值会发生变化.
滞后主要发生在角色穿过障碍物时(有时也会在障碍物之后).会发生的是,游戏状态的每个图形都存在不均匀的延迟,导致运动中出现断断续续的情况.
我的想法是有一行或多行代码导致某种延迟发生(因此跳过帧).我也认为这些线路应该是围绕update()和draw()方法PlayerCharacter,Obstacle以及MainGameBoard.
问题是,我还是专门针对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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
fad*_*den 20
更新:就像这样详细,它几乎没有触及表面.现在可以获得更详细的解释.游戏循环建议在附录A中.如果您真的想了解正在发生的事情,请从此开始.
原帖如下......
我将首先介绍Android中图形管道的工作原理.你可以找到更全面的治疗方法(例如一些非常详细的谷歌I/O会谈),所以我只是达到了高点.结果比我预期的要长,但我一直想写一些这样的东西.
您的应用程序不会在Framebuffer上绘制.有些设备甚至没有 Framebuffer.您的应用程序保存对象的"生产者"端BufferQueue.当它完成渲染帧时,它会调用unlockCanvasAndPost()或者eglSwapBuffers()将完成的缓冲区排队等待显示.(从技术上讲,渲染可能甚至没有开始,直到你告诉它交换并且可以在缓冲区移动通过管道时继续,但这是另一个时间的故事.)
缓冲区被发送到队列的"消费者"端,在这种情况下是系统表面合成器SurfaceFlinger.缓冲区通过句柄传递; 内容不会被复制.每次显示刷新(让我们称之为"VSYNC")开始时,SurfaceFlinger会查看所有各种队列以查看可用的缓冲区.如果找到新内容,则会锁定该队列中的下一个缓冲区.如果没有,它会使用之前获得的任何内容.
然后将具有可见内容的窗口(或"层")的集合合成在一起.这可以通过SurfaceFlinger(使用OpenGL ES将图层渲染到新缓冲区)或通过Hardware Composer HAL完成.硬件组合器(在最新设备上可用)由硬件OEM提供,并且可以提供许多"覆盖"平面.如果SurfaceFlinger有三个要显示的窗口,并且HWC有三个可用的覆盖平面,它会将每个窗口放入一个叠加层,并在显示帧时进行合成.从来没有一个缓冲区可以容纳所有数据.这通常比在GLES中做同样的事情更有效.(顺便说一句,这就是为什么你不能通过打开framebuffer dev条目和读取像素来获取最近设备上的屏幕截图.)
这就是消费者方面的样子.你可以自己欣赏它adb shell dumpsys SurfaceFlinger.让我们回到制作人(即你的应用).
您使用的是a SurfaceView,它有两个部分:一个与系统UI一起使用的透明视图,以及一个独立的Surface层.它SurfaceView的表面直接进入SurfaceFlinger,这就是为什么它比其他方法(如TextureView)具有更少的开销.
SurfaceView表面的BufferQueue 是三重缓冲的.这意味着你可以扫描出一个缓冲区用于显示,一个缓冲区位于SurfaceFlinger,等待下一个VSYNC,一个缓冲区供应用程序绘制.拥有更多缓冲区可提高吞吐量并消除颠簸,但会增加触摸屏幕与查看更新之间的延迟.在此基础上添加额外的整个帧缓冲通常不会对你有好处.
如果绘制的速度比显示器渲染帧快,则最终会填满队列,缓冲区交换调用(unlockCanvasAndPost())将暂停.这是让游戏的更新速率与显示速率相同的简单方法 - 尽可能快地绘制,让系统降低速度.每一帧,你根据经过的时间提前状态.(我在Android Breakout中使用了这种方法.)这不太对,但是在60fps时你不会真正注意到这些不完美之处.sleep()如果你没有长时间睡眠,你会得到与呼叫相同的效果- 你只会在队列中等待醒来.在这种情况下,睡眠没有任何好处,因为在队列中睡觉同样有效.
如果绘制的速度慢于显示器可以渲染帧,则队列最终会变干,SurfaceFlinger将在两次连续的显示刷新时显示相同的帧.如果您试图通过sleep()电话调整游戏速度并且睡眠时间过长,这将会定期发生.这是不可能精确匹配的显示刷新速率,理论上的原因(这是很难实现的PLL没有反馈机制)和现实的原因(刷新率可以随时间而改变,例如,我已经看到了从58fps的不同而不同,以62fps给定的设备).
sleep()在游戏循环中使用调用来调整动画速度是一个坏主意.
你有几个选择.您可以使用"尽可能快地绘制,直到缓冲区交换调用备份"方法,这是许多应用程序所依据的GLSurfaceView#onDraw()(无论他们是否知道).或者你可以使用Choreographer.
编舞器允许您设置在下一个VSYNC上触发的回调.重要的是,回调的参数是实际的VSYNC时间.因此,即使您的应用程序没有立即唤醒,您仍然可以准确了解显示屏刷新的时间.在更新游戏状态时,这非常有用.
更新游戏状态的代码永远不应该被设计为推进"一帧".鉴于设备种类繁多,以及单个设备可以使用的各种刷新率,您无法知道"帧"是什么.你的游戏会稍微慢一点或稍微快一点 - 或者如果你运气好,有人试图通过HDMI锁定到48Hz的电视上播放它,你将会非常迟钝.您需要确定前一帧和当前帧之间的时差,并适当地提前游戏状态.
这可能需要一些精神上的重新洗牌,但这是值得的.
你可以在Breakout中看到这个,它根据经过的时间推进球的位置.它可以及时将大跳跃切成小块,以保持碰撞检测的简单性.Breakout的问题在于它使用了stuff-the-queue-full方法,时间戳可能会受到SurfaceFlinger工作所需时间的影响.此外,当缓冲区队列最初为空时,您可以非常快速地提交帧.(这意味着你计算两个时间差几乎为零的帧,但它们仍然以60fps的速度发送到显示器.实际上你没有看到这个,因为时间戳差异很小,看起来就像是同一帧两次绘制,只有在从非动画转换到动画时才会发生,所以你看不到任何口吃.)
使用Choreographer,您可以获得实际的VSYNC时间,因此您可以获得一个很好的常规时钟,以使您的时间间隔为基础.因为您使用显示刷新时间作为时钟源,所以您永远不会与显示器不同步.
当然,你仍然需要准备丢帧.
前段时间我向Grafika("记录GL应用程序")添加了一个屏幕录制演示,它做了非常简单的动画 - 只是一个平面阴影的弹跳矩形和一个旋转的三角形.当编舞者发出信号时,它会提升状态并绘制.我编写了它,运行它...并开始注意到Choreographer回调备份.
在使用systrace进行挖掘之后,我发现框架UI偶尔会进行一些布局工作(可能与UI层中的按钮和文本有关,它位于SurfaceView表面的顶部).通常这需要6ms,但如果我没有主动在屏幕上移动手指,我的Nexus 5会减慢各种时钟的速度以降低功耗并延长电池寿命.重新布局需要28ms.请记住,60fps帧是16.7ms.
GL渲染几乎是即时的,但是Choreographer更新被传递到UI线程,它正在磨损布局,所以我的渲染器线程直到很久以后才得到信号.(你可以让Choreographer直接将信号传递给渲染器线程,但是如果你这样做,Choreographer中会出现一个会导致内存泄漏的错误.)修复是在当前时间超过VSYNC时间后的15ms时丢帧.该应用程序仍然进行状态更新 - 碰撞检测非常简陋,如果你让时间差距变得太大,就会发生奇怪的事情 - 但是它没有向SurfaceFlinger提交缓冲区.
在运行应用程序时,您可以判断帧何时被丢弃,因为Grafika将边框闪烁红色并更新屏幕上的计数器.通过观看动画你无法分辨.因为状态更新是基于时间间隔而不是帧计数,所以一切都移动的速度与帧丢失的速度一样快,并且在60fps时您不会注意到单个丢帧.(在某种程度上取决于你的眼睛,游戏和显示硬件的特性.)
主要课程:
如果硬件加速,渲染到画布可能非常有效.如果不是,并且您正在使用软件进行绘图,则可能需要一段时间 - 特别是如果您触摸了大量像素.
阅读的两个重要部分:了解硬件加速渲染,并使用硬件缩放器减少应用需要触摸的像素数.Grafika中的"硬件缩放器练习器"将让您了解在缩小绘图表面大小时会发生什么 - 在效果明显之前,您可以获得相当小的效果.(我发现在100x64表面上观看GL渲染旋转三角形时,我觉得很奇怪.)
您还可以直接使用OpenGL ES来渲染渲染中的一些谜团.有一点可以了解事情是如何运作的,但是Breakout(以及更详细的例子,Replica Island)显示了简单游戏所需的一切.
在没有用Android制作游戏的情况下,我确实使用Canvas和bufferStrategy在Java/AWT中制作了2D游戏......
如果您遇到闪烁,您可以通过渲染到屏幕外的图像,然后直接使用新的预渲染内容进行翻页翻转/绘图图像来寻找手动双缓冲区(摆脱闪烁).
但是,我觉得你更关心动画中的"平滑度",在这种情况下,我建议你在不同的动画刻度之间用插值扩展你的代码;
目前,渲染循环以与渲染相同的速度更新逻辑状态(逻辑移动),并使用一些参考时间进行测量并尝试跟踪传递的时间.
相反,您应该以您认为代码中"逻辑"所需的频率更新 - 通常10或25Hz就好了(我称之为"更新刻度",这与实际FPS完全不同),而渲染是通过保持高分辨率的时间跟踪来测量你的实际渲染所需的"多长时间"(我已经使用过nanoTime并且已经足够了,而currentTimeInMillis则相当无用......),
通过这种方式,您可以在刻度线之间进行插值并渲染尽可能多的帧,直到下一次刻度为止,根据自上次刻度后经过的时间计算细粒度位置,与"应该"在两次之间的时间相比.滴答声(因为你总是知道你在哪里 - 位置,以及你前进的位置 - 速度)
这样,无论CPU /平台如何,您都将获得相同的"动画速度",但或多或少具有平滑性,因为更快的CPU将在不同的刻度之间执行更多渲染.
一些复制粘贴/概念代码 - 但要注意这是AWT和J2SE,没有Android.然而,作为一个概念和一些Android化,我确信这种方法应该顺利呈现,除非你的逻辑/更新中的微积分太重(例如,用于碰撞检测的N ^ 2算法和N随着粒子系统等而变大) ).
主动渲染循环
而不是依赖重绘为你做绘画(这可能需要不同的时间,取决于操作系统正在做什么),第一步是主动控制渲染循环并使用BufferStrategy在你渲染,然后积极"显示"你完成后的内容,然后再回到它.
缓冲策略
可能需要一些特殊的Android内容才能开始,但这是相当直接的.我使用2页的bufferStrategy来创建一个"页面翻转"机制.
try
{
EventQueue.invokeAndWait(new Runnable() {
public void run()
{
canvas.createBufferStrategy(2);
}
});
}
catch(Exception x)
{
//BufferStrategy creation interrupted!
}
Run Code Online (Sandbox Code Playgroud)
主要动画循环
然后,在您的主循环中,获取策略并采取主动控制(不要使用重绘)!
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);
}
}
Run Code Online (Sandbox Code Playgroud)
一个被管理的对象就是上面的主循环,然后看起来就像是一条线;
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);
}
}
Run Code Online (Sandbox Code Playgroud)
使用具有双精度的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;
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
9388 次 |
| 最近记录: |