- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我刚开始在android中进行游戏开发,并且正在开发一款 super 简单的游戏。
游戏基本上就像飘扬的小鸟。
我设法使所有工作正常进行,但是却遇到了很多困难和滞后。
我用于测试的手机是LG G2,因此它应该并且确实比这更重,更复杂地运行游戏。
基本上有4个“障碍物”,它们彼此隔开一个完整的屏幕宽度。
游戏开始时,障碍物开始以恒定速度移动(朝向角色)。玩家角色的x值在整个游戏中保持一致,而其y值则发生变化。
滞后主要发生在角色穿过障碍物时(有时也越过障碍物)。发生的情况是,在游戏状态的每个绘制中都有不均匀的延迟,从而导致 Action 停顿。
update()
,
draw()
和
PlayerCharacter
的
Obstacle
和
MainGameBoard
方法。
Performance Tips
页面,但找不到任何有帮助的内容。
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);
}
}
}
}
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;
}
}
}
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;
}
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):
关于java - android游戏中令人讨厌的滞后/口吃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21838523/
我看到过关于此的类似帖子,但无法让 Netbeans 在正常工作时停止在我的代码中显示错误消息 "Unable to resolve identifier nullptr"。我已正确启用 C++11,
就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the
我有一个 cucumber 步骤,最近开始失败时 已添加到我的布局中。如果我拿 出来,我的测试都通过了。当我把它放回去时,使用 WebRat 提供的 click_link 方法
我一直致力于创建独立于 .Net 客户端运行的 WCF 服务。感谢 Google 和 StackOverflow,我已经能够创建简单的 xml 和 json 服务,而无需 Soap 包装器和一堆我不需
有人可以向我解释一下 python 在 ubuntu 9.04 中发生了什么吗? 我正在尝试启动 virtualenv,而 --no-site-packages 标志似乎对 ubuntu 没有任何作用
我是一名优秀的程序员,十分优秀!