java - Molestos retrasos/tartamudeo en un juego de Android
surfaceview game-loop (5)
En primer lugar, Canvas puede tener un bajo rendimiento, así que no esperes demasiado. Es posible que desee probar el ejemplo de lunarlander desde el SDK y ver qué rendimiento obtiene en su hardware.
Intente bajar los fps máximos a unos 30, el objetivo es ser suave, no rápido.
private final static int MAX_FPS = 30; // desired fps
También deshágase de las llamadas de reposo, la reproducción en el lienzo probablemente duerma lo suficiente. Intenta algo más como:
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);
}
}
Si lo deseas, puedes hacer tu this.mMainGameBoard.update()
más a menudo que tu render.
Edit: También, ya que dices que las cosas se vuelven lentas cuando aparecen los obstáculos. Intenta dibujarlos en un Canvas / Bitmap fuera de pantalla. He escuchado que algunos de los métodos drawSHAPE están optimizados para la CPU y obtendrás un mejor rendimiento al dibujarlos en un lienzo / mapa de bits sin conexión porque no son hardware / gpu acelerados.
Edit2: ¿Qué devuelve Canvas.isHardwareAccelerated ()?
Acabo de comenzar con el desarrollo de juegos en Android, y estoy trabajando en un juego súper simple.
El juego es básicamente como el pájaro flappy.
Me las arreglé para que todo funcionara, pero tengo muchos tartamudos y retrasos.
El teléfono que estoy usando para las pruebas es LG G2, por lo que debería y ejecuta juegos mucho más pesados y complejos que este.
Básicamente, hay 4 ''obstáculos'' que están separados por un ancho de pantalla completo.
Cuando comienza el juego, los obstáculos comienzan a moverse (hacia el personaje) a una velocidad constante. El valor x del personaje del jugador es consistente a lo largo de todo el juego, mientras que su valor y cambia.
El retraso ocurre principalmente cuando el personaje pasa a través de un obstáculo (y algunas veces también un poco después de ese obstáculo). Lo que sucede es que hay retrasos desiguales en cada sorteo del estado del juego que causa tartamudeo en los movimientos.
- GC no se ejecuta de acuerdo con el registro.
- Los tartamudos NO son causados porque la velocidad es demasiado alta (lo sé porque al principio del juego, cuando los obstáculos están fuera de la vista, el personaje se mueve suavemente)
- No creo que el problema esté relacionado con FPS también, porque incluso cuando el campo MAX_FPS se establece en 100, todavía hay interruptores.
Mi pensamiento es que hay una línea o varias líneas de código que causan algún tipo de demora (y por lo tanto, los marcos se saltan). También creo que estas líneas deben estar alrededor de los métodos update()
y draw()
de PlayerCharacter
, Obstacle
, y MainGameBoard
.
El problema es que todavía soy nuevo en el desarrollo de Android y en el desarrollo de juegos de Android específicamente, por lo que no tengo idea de qué podría causar ese retraso.
Intenté buscar respuestas en línea ... Desafortunadamente, todo lo que encontré apuntaba a que GC tenía la culpa. Sin embargo, por lo que no creo que sea así (corríjame si me equivoco) esas respuestas no se aplican a mí. También leí la página de Performance Tips
del desarrollador de Android, pero no pude encontrar nada que ayudara.
Entonces, por favor, ¡ayúdame a encontrar la respuesta para resolver estos molestos retrasos!
Algun codigo
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;
}
Obstáculo.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;
}
Prueba este en tamaño. Notará que solo sincroniza y bloquea el lienzo durante el período de tiempo más corto. De lo contrario, el sistema operativo A) Soltará el búfer porque era demasiado lento o B) no se actualizará hasta que finalice la espera.
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++;
}
}
}
}
Sin haber hecho nunca un juego en Android, sí he hecho juegos en 2D en Java / AWT usando Canvas y bufferStrategy ...
Si experimenta parpadeo, siempre puede optar por un doble búfer manual (deshacerse del parpadeo) al renderizar a una imagen fuera de la pantalla, y luego solo pasar la página / dibujar imagen con los nuevos contenidos pre-renderizados directamente.
Pero, tengo la sensación de que está más preocupado por la "suavidad" en su animación, en cuyo caso le recomiendo que extienda su código con interpolación entre las diferentes marcas de animación;
Actualmente, su bucle de representación actualiza el estado lógico (mueva las cosas de manera lógica) al mismo ritmo que la renderización, mida con un tiempo de referencia e intente realizar un seguimiento del tiempo transcurrido.
En su lugar, debe actualizar en la frecuencia que considere conveniente para que funcionen las "lógicas" de su código; por lo general, 10 o 25 Hz está bien (lo llamo "tics de actualización", que es completamente diferente del FPS real). mientras que la representación se realiza manteniendo un seguimiento del tiempo de alta resolución para medir "cuánto tiempo" toma su representación real (he usado nanoTime y eso ha sido bastante suficiente, mientras que currentTimeInMillis es bastante inútil ...),
De esa manera, puede interpolar entre tics y renderizar tantos cuadros como sea posible hasta el siguiente tic, calculando las posiciones de grano fino en función del tiempo transcurrido desde el último tic, en comparación con el tiempo que "debería" estar entre dos. garrapatas (ya que siempre sabes dónde estás: posición y hacia dónde te diriges, velocidad)
De esta manera, obtendrá la misma "velocidad de animación" independientemente de la CPU / plataforma, pero más o menos suavidad, ya que las CPU más rápidas realizarán más representaciones entre diferentes tics.
EDITAR
Algunos códigos de copiar / pegar / conceptuales, pero tenga en cuenta que esto fue AWT y J2SE, no Android. Sin embargo, como concepto y con cierta Androidificación, estoy seguro de que este enfoque debería mostrarse sin problemas a menos que el cálculo realizado en su lógica / actualización sea demasiado pesado (p. Ej., Los algoritmos N ^ 2 para la detección de colisiones y N crezca en grande con los sistemas de partículas y similares) ).
Bucle de render activo
En lugar de confiar en el repintado para hacer la pintura por usted (lo que puede tomar un tiempo diferente, dependiendo de lo que haga el sistema operativo), el primer paso es tomar el control activo del bucle de representación y usar una estrategia de Buffer donde renderice y luego muestre activamente " "el contenido cuando hayas terminado, antes de volver a ello.
Estrategia de amortiguación
Podría requerir algunas cosas especiales de Android para ponerse en marcha, pero es bastante sencillo. Uso 2 páginas para bufferStrategy para crear un mecanismo de "cambio de página".
try
{
EventQueue.invokeAndWait(new Runnable() {
public void run()
{
canvas.createBufferStrategy(2);
}
});
}
catch(Exception x)
{
//BufferStrategy creation interrupted!
}
Bucle de animación principal
Luego, en su bucle principal, ¡obtenga la estrategia y tome el control activo (no use el repintado)!
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);
}
}
Un objeto administrado por el bucle principal anterior se vería entonces en la línea de;
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);
}
}
Todas las posiciones 2D se administran utilizando una clase de utilidad GfxPoint con doble precisión (ya que los movimientos interpolados pueden ser muy finos y, por lo general, no se desea redondear hasta que se representan los gráficos reales). Para simplificar las cosas de matemáticas necesarias y hacer que el código sea más legible, también he agregado varios métodos.
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;
}
}
Actualización: Tan detallado como fue, apenas arañó la superficie. Una explicación más detallada ya está disponible . El consejo del juego está en el Apéndice A. Si realmente quiere entender lo que está pasando, comience con eso.
La publicación original sigue ...
Voy a comenzar con un resumen en cápsula de cómo funciona el flujo de gráficos en Android. Puede encontrar tratamientos más exhaustivos (por ejemplo, algunas charlas de I / O de Google muy bien detalladas), así que solo estoy alcanzando los puntos más altos. Esto resultó bastante más largo de lo que esperaba, pero he querido escribir algo de esto por un tiempo.
SurfaceFlinger
Su aplicación no se basa en el Framebuffer. Algunos dispositivos ni siquiera tienen el Framebuffer. Su aplicación tiene el lado "productor" de un objeto BufferQueue
. Cuando ha completado la representación de un marco, llama a unlockCanvasAndPost()
o eglSwapBuffers()
, que pone en cola el búfer completado para su visualización. (Técnicamente, el renderizado puede que ni siquiera comience hasta que le indiques que se intercambie y puede continuar mientras el búfer se mueve a través de la tubería, pero eso es una historia para otro momento).
El búfer se envía al lado "consumidor" de la cola, que en este caso es SurfaceFlinger, el compositor de superficie del sistema. Los amortiguadores se pasan por la manija; Los contenidos no se copian. Cada vez que se inicia la actualización de la pantalla (llamémosla "VSYNC"), SurfaceFlinger examina las diferentes colas para ver qué buffers están disponibles. Si encuentra contenido nuevo, cierra el siguiente búfer de esa cola. Si no lo hace, usa lo que tenía anteriormente.
La colección de ventanas (o "capas") que tienen contenido visible luego se componen juntas. Esto puede hacerse mediante SurfaceFlinger (utilizando OpenGL ES para representar las capas en un nuevo búfer) o mediante el HAL del Compositor de Hardware. El compositor de hardware (disponible en los dispositivos más recientes) lo proporciona el OEM del hardware y puede proporcionar una serie de planos de "superposición". Si SurfaceFlinger tiene tres ventanas para mostrar, y el HWC tiene tres planos de superposición disponibles, coloca cada ventana en una sola superposición y realiza la composición a medida que se muestra el marco . Nunca hay un búfer que contiene todos los datos. Esto es generalmente más eficiente que hacer lo mismo en GLES. (Por cierto, esta es la razón por la que no puede tomar una captura de pantalla en los dispositivos más recientes simplemente abriendo la entrada del dispositivo de almacenamiento de imágenes y leyendo los píxeles).
Así es como se ve el lado del consumidor. Puedes admirarlo por ti mismo con adb shell dumpsys SurfaceFlinger
. Volvamos al productor (es decir, su aplicación).
el productor
Está utilizando un SurfaceView
, que tiene dos partes: una vista transparente que vive con la interfaz de usuario del sistema y una capa de superficie separada. La superficie de SurfaceView va directamente a SurfaceFlinger, por lo que tiene mucho menos sobrecarga que otros enfoques (como TextureView
).
El BufferQueue para la superficie de SurfaceView
tiene triple buffer. Eso significa que puede escanear un búfer para la pantalla, un búfer que se encuentra en SurfaceFlinger esperando el próximo VSYNC, y un búfer para que la aplicación se base. Tener más búferes mejora el rendimiento y suaviza los baches, pero aumenta la latencia entre cuando toca la pantalla y cuando ve una actualización. La adición de búferes adicionales de fotogramas completos sobre esto generalmente no te hará mucho bien.
Si dibuja más rápido de lo que la pantalla puede representar marcos, eventualmente llenará la cola y su llamada de intercambio de búfer ( unlockCanvasAndPost()
) se detendrá. Esta es una manera fácil de hacer que la velocidad de actualización de tu juego sea igual a la velocidad de visualización: dibuja lo más rápido que puedas y deja que el sistema te detenga. En cada fotograma, avanza el estado según el tiempo transcurrido. (Utilicé este enfoque en Android Breakout .) No está del todo bien, pero a 60 fps realmente no notará las imperfecciones. Obtendrás el mismo efecto con las llamadas de sleep()
si no duermes lo suficiente; solo te despertarás para esperar en la cola. En este caso, no es ventajoso dormir, porque dormir en la cola es igual de eficiente.
Si dibuja más lento de lo que la pantalla puede renderizar marcos, la cola finalmente se secará, y SurfaceFlinger mostrará el mismo marco en dos actualizaciones de pantalla consecutivas. Esto sucederá periódicamente si estás tratando de acelerar tu juego con llamadas sleep()
y estás durmiendo demasiado tiempo. Es imposible hacer coincidir con precisión la frecuencia de actualización de la pantalla, por razones teóricas (es difícil implementar un PLL sin un mecanismo de retroalimentación) y razones prácticas (la frecuencia de actualización puede cambiar con el tiempo, por ejemplo, he visto que varía de 58 fps a 62 fps en un dispositivo dado).
El uso de llamadas sleep()
en un bucle de juego para controlar tu animación es una mala idea.
ir sin dormir
Usted tiene un par de opciones. Puede usar el método "dibujar lo más rápido posible hasta que la copia de seguridad de las copias de respaldo de intercambio de búfer", que es lo que hacen muchas aplicaciones basadas en GLSurfaceView#onDraw()
(ya sea que lo sepan o no). O puedes usar el Choreographer .
Choreographer le permite establecer una devolución de llamada que se dispara en el próximo VSYNC. Es importante destacar que el argumento de la devolución de llamada es la hora real de VSYNC. Así que incluso si su aplicación no se despierta de inmediato, todavía tiene una idea precisa de cuándo comenzó la actualización de la pantalla. Esto resulta muy útil al actualizar el estado de tu juego.
El código que actualiza el estado del juego nunca debe diseñarse para avanzar "un fotograma". Dada la variedad de dispositivos y la variedad de frecuencias de actualización que puede usar un solo dispositivo, no se puede saber qué es un "marco". Tu juego se jugará un poco lento o un poco rápido, o si tienes suerte y alguien intenta jugarlo en un televisor bloqueado a 48Hz a través de HDMI, estarás muy inactivo. Debes determinar la diferencia de tiempo entre el cuadro anterior y el cuadro actual, y avanzar el estado del juego de manera apropiada.
Esto puede requerir un poco de reorganización mental, pero vale la pena.
Puedes ver esto en acción en Breakout , que avanza la posición de la bola en función del tiempo transcurrido. Corta grandes saltos a tiempo en pedazos más pequeños para que la detección de colisiones sea simple. El problema con Breakout es que está utilizando el enfoque de stuff-the-queue-full, las marcas de tiempo están sujetas a variaciones en el tiempo requerido para que SurfaceFlinger funcione. Además, cuando la cola del búfer está inicialmente vacía, puede enviar marcos muy rápidamente. (Esto significa que calcula dos fotogramas con delta de tiempo casi cero, pero aún se envían a la pantalla a 60 fps. En la práctica, no se ve esto porque la diferencia de marca de tiempo es tan pequeña que parece el mismo fotograma se dibuja dos veces, y solo sucede cuando se pasa de no animar a animar, por lo que no se ve ningún tartamudeo.)
Con Choreographer, obtienes la hora real de VSYNC, así que obtienes un buen reloj regular para basar tus intervalos de tiempo. Debido a que está utilizando el tiempo de actualización de la pantalla como su fuente de reloj, nunca se desincroniza con la pantalla.
Por supuesto, todavía tienes que estar preparado para soltar marcos.
sin marco dejado atrás
Hace un tiempo, agregué una demostración de grabación de pantalla a Grafika ("aplicación Record GL") que hace una animación muy simple: solo un rectángulo rebotando de forma plana y un triángulo giratorio. Avanza estado y dibuja cuando señala el coreógrafo. Lo codifiqué, lo ejecuté ... y comencé a notar que las devoluciones de llamada del Coreógrafo se estaban recuperando.
Después de systrace en él con systrace , descubrí que la IU del marco de trabajo ocasionalmente realizaba algún trabajo de diseño (probablemente relacionado con los botones y el texto en la capa de IU, que se encuentra en la parte superior de la superficie SurfaceView
). Normalmente esto requería 6 ms, pero si no movía activamente mi dedo por la pantalla, mi Nexus 5 redujo la velocidad de los distintos relojes para reducir el consumo de energía y mejorar la vida útil de la batería. El rediseño tomó 28ms en su lugar. Tenga en cuenta que un marco de 60 fps es de 16.7 ms.
La representación de GL fue casi instantánea, pero la actualización de Choreographer se estaba entregando al subproceso de la interfaz de usuario, que estaba moliendo en el diseño, por lo que mi subproceso de procesador no recibió la señal hasta mucho más tarde. (Podría hacer que Choreographer envíe la señal directamente al subproceso del renderizador, pero hay un error en Choreographer que provocará una pérdida de memoria si lo hace). La solución fue eliminar fotogramas cuando la hora actual es más de 15 ms después de la hora VSYNC. La aplicación sigue actualizando el estado (la detección de colisiones es tan rudimentaria que ocurren cosas extrañas si dejas que el intervalo de tiempo sea demasiado grande) pero no envía un búfer a SurfaceFlinger.
Mientras ejecuta la aplicación, puede saber cuándo se están eliminando los marcos, ya que Grafika parpadea el borde rojo y actualiza un contador en la pantalla. No se puede ver viendo la animación. Debido a que las actualizaciones de estado se basan en intervalos de tiempo , no en conteos de cuadros, todo se mueve tan rápido como lo haría si el cuadro se eliminó o no, y a 60 fps no notará un solo cuadro eliminado. (Depende en cierta medida de tus ojos, el juego y las características del hardware de la pantalla).
Lecciones clave:
- La caída de cuadros puede deberse a factores externos: dependencia de otro hilo, velocidades de reloj de la CPU, sincronización de Gmail en segundo plano, etc.
- No puedes evitar todas las gotas de marco.
- Si configuras tu bucle de sorteo bien, nadie lo notará.
Dibujo
El renderizado a un Canvas puede ser muy eficiente si se acelera por hardware. Si no lo es, y estás haciendo el dibujo en el software, puede llevarte un tiempo, especialmente si tocas muchos píxeles.
Dos partes importantes de la lectura: aprenda sobre la representación acelerada por hardware y use el escalador de hardware para reducir la cantidad de píxeles que su aplicación necesita tocar. El "ejercitador de escalador de hardware" en Grafika le dará una idea de lo que sucede cuando reduce el tamaño de la superficie de dibujo: puede reducirse bastante antes de que se noten los efectos. (Me resulta extrañamente divertido ver a GL renderizar un triángulo giratorio en una superficie de 100x64).
También puede eliminar algunos de los misterios de la representación utilizando OpenGL ES directamente. Es un poco difícil aprender cómo funcionan las cosas, pero Breakout (y, para un ejemplo más detallado, Replica Island ) muestra todo lo que necesitas para un juego simple.
Una de las causas más comunes de desaceleración y tartamudeo en un juego es el flujo de gráficos. La lógica del juego es mucho más rápida de procesar que de dibujar (en general), por lo que debes asegurarte de dibujar todo de la manera más eficiente posible. A continuación puede encontrar algunos consejos sobre cómo lograr esto.
Algunas sugerencias para hacerlo mejor.