Des retards / bégaements ennuyeux dans un jeu d'Android

Je viens de commencer avec le développement de jeux dans Android, et je travaille sur un jeu super simple.

Le jeu est essentiellement comme un oiseau flippé.

  • Sécurisation de php api à utiliser dans l'application Android
  • AdMob utilisant Google Play Services - Annonces de test - Pas de remplissage du serveur publicitaire
  • Application Android: Comment lire obtenir des paramètres à partir d'un schéma d'url personnalisé?
  • Installation de R sur Android
  • Après la mise à niveau Android sur Nexus vers 4.2.2, Eclipse affiche la cible inconnue pour le périphérique?
  • Modifier la couleur d'arrière-plan de la mise en page dans Android
  • J'ai réussi à faire fonctionner tout, mais j'ai beaucoup de bégaiements et de retard.

    Le téléphone que j'utilise pour tester est LG G2, donc il devrait et exécute des jeux beaucoup plus lourds et complexes que cela.

    Fondamentalement, il existe 4 «obstacles» qui sont une largeur d'écran pleine l'une de l'autre.
    Lorsque le jeu commence, les obstacles commencent à se déplacer (vers le personnage) à une vitesse constante. La valeur x du personnage du joueur est cohérente tout au long du jeu, alors que sa valeur y change.

    Le décalage se produit principalement lorsque le personnage traverse un obstacle (et parfois un peu après cet obstacle). Ce qui se passe, c'est qu'il y a des retards inégaux dans chaque dessin de l'état du jeu provoquant des bégaiements dans les mouvements.

    • Le GC ne fonctionne pas selon le journal.
    • Les bégaiements NE SONT PAS causés par la vitesse trop élevée (je sais que parce que, au début du jeu, lorsque les obstacles sont hors de vue, le personnage se déplace bien)
    • Je ne pense pas que le problème soit également lié au FPS, car même lorsque le champ MAX_FPS est défini sur 100 il y a encore des bégaiements.

    Je pense qu'il existe une ligne ou plusieurs lignes de code qui provoquent un peu de retard (et donc les cadres ignorés). Je pense également que ces lignes devraient être autour des méthodes update() et draw() de PlayerCharacter , Obstacle et MainGameBoard .

    Le problème est que je suis encore nouveau dans le développement d'Android et le développement de jeux Android, donc je n'ai aucune idée de ce qui pourrait causer de tels délais.

    J'ai essayé de chercher en ligne des réponses … Malheureusement, tout ce que j'ai trouvé pointé vers le GC était à blâmer. Cependant, je ne le crois pas le cas (corrigez-moi si je me trompe), ces réponses ne s'appliquent pas à moi. J'ai également lu la page des Performance Tips du développeur Android, mais je n'ai rien trouvé pour l'aider.

    Alors, aidez-moi à trouver la réponse pour résoudre ces défauts ennuyeux!

    Un certain code

    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; } 

  • Android: erreur de conversion d'octets en dex
  • Existe-t-il un moyen de faire répartir un groupe de radio horizontalement?
  • Désactiver le défilement dans tous les appareils mobiles
  • Activer l'accélération matérielle dans l'application Android, ciblant Honeycomb AND versions antérieures
  • Comment puis-je modifier un MenuItem sur le menu Options sur Android?
  • Aapt.exe se bloque lorsque j'essaie d'exporter mon application Android depuis Eclipse
  • 5 Solutions collect form web for “Des retards / bégaements ennuyeux dans un jeu d'Android”

    Mise à jour: aussi détaillé que cela, il a à peine rayé la surface. Une explication plus détaillée est maintenant disponible . Le conseil de la boucle de jeu se trouve à l'annexe A. Si vous voulez vraiment comprendre ce qui se passe, commencez par cela.

    La publication originale suit …


    Je vais commencer par un résumé de la capsule de la façon dont fonctionne le pipeline graphique dans Android. Vous pouvez trouver des traitements plus approfondis (par exemple, des discussions Google E / S bien détaillées), alors je viens juste de frapper les points forts. Cela s'est avéré plutôt plus long que prévu, mais j'ai voulu écrire un peu de cela pendant un certain temps.

    SurfaceFlinger

    Votre application ne s'appuie pas sur The Framebuffer. Certains appareils n'ont même pas The Framebuffer. Votre application détient le côté "producteur" d'un objet BufferQueue . Quand il a terminé le rendu d'une image, il appelle unlockCanvasAndPost() ou eglSwapBuffers() , qui met en attente le tampon rempli pour l'affichage. (Techniquement, le rendu ne commence peut-être même pas jusqu'à ce que vous lui disiez d'échanger et de continuer pendant que le tampon se déplace dans le pipeline, mais c'est une histoire pour un autre moment.)

    Le tampon est envoyé au côté «consommateur» de la file d'attente, qui, dans ce cas, est SurfaceFlinger, le compositor de la surface du système. Les tampons sont passés par la poignée; Le contenu n'est pas copié. Chaque fois que l'affichage se déclenche (appelons-le "VSYNC"), SurfaceFlinger regarde toutes les files d'attente différentes pour voir quels tampons sont disponibles. S'il trouve un nouveau contenu, il verrouille le prochain tampon de cette file. Si ce n'est pas le cas, il utilise tout ce qu'il avait auparavant.

    La collection de fenêtres (ou "couches") qui ont un contenu visible est ensuite composée ensemble. Cela peut être effectué par SurfaceFlinger (en utilisant OpenGL ES pour rendre les calques dans un nouveau tampon) ou via le Compositeur matériel HAL. Le compositeur de matériel (disponible sur les appareils les plus récents) est fourni par l'OEM matériel et peut fournir un certain nombre d'avions "superposés". Si SurfaceFlinger dispose de trois fenêtres à afficher et le HWC possède trois plans de superposition disponibles, il place chaque fenêtre dans une superposition, et la composition comme image est affichée . Il n'y a jamais de tampon contenant toutes les données. C'est généralement plus efficace que de faire la même chose dans GLES. (Incidemment, c'est pourquoi vous ne pouvez pas saisir une capture d'écran sur les appareils les plus récents en ouvrant simplement l'entrée et la lecture des pixels de framebuffer).

    C'est donc à quoi ressemble le côté du consommateur. Vous pouvez l'admirer avec adb shell dumpsys SurfaceFlinger . Revenons au producteur (c'est-à-dire votre application).

    le producteur

    Vous utilisez un SurfaceView , qui comporte deux parties: une vue transparente qui vit avec l'interface utilisateur du système et une couche de surface séparée. La SurfaceView de SurfaceView va directement à SurfaceFlinger, c'est pourquoi elle a beaucoup moins de frais généraux que d'autres approches (comme TextureView ).

    Le BufferQueue pour la SurfaceView de SurfaceView est triple-tamponné. Cela signifie que vous pouvez utiliser un tampon pour l'affichage, un tampon assis sur SurfaceFlinger en attente du prochain VSYNC et un tampon pour que votre application s'appuie sur. Avoir plus de tampons améliore le débit et lisse les bosses, mais augmente la latence entre lorsque vous appuyez sur l'écran et lorsque vous voyez une mise à jour. L'ajout d'une mise en mémoire tampon supplémentaire des trames entières n'est plus généralement très utile.

    Si vous dessinez plus rapidement que l'affichage peut générer des images, vous unlockCanvasAndPost() par remplir la file d'attente, et votre appel de mémoire tampon ( unlockCanvasAndPost() ) s'arrêtera. C'est un moyen simple de rendre la mise à jour de votre jeu comme celle du taux d'affichage – dessinez aussi vite que possible et laissez le système ralentir. Chaque image, vous avancez l'état en fonction de la durée écoulée. (J'ai utilisé cette approche dans Android Breakout .) Ce n'est pas tout à fait raison, mais à 60fps, vous ne remarquerez pas vraiment les imperfections. Vous obtiendrez le même effet avec sleep() appels sleep() si vous ne dormez pas assez longtemps – vous vous réveillerez uniquement pour attendre la file d'attente. Dans ce cas, il n'y a aucun avantage à dormir, car dormir en file d'attente est également efficace.

    Si vous dessinez plus lentement que l'affichage peut générer des images, la file sera finalement séchée, et SurfaceFlinger affichera la même image sur deux rafraîchissements d'affichage consécutifs. Cela se produira périodiquement si vous essayez de rythmer votre jeu avec sleep() appels de sleep() et vous dormez trop longtemps. Il est impossible de faire correspondre précisément le taux de rafraîchissement de l'affichage, pour des raisons théoriques (il est difficile de mettre en œuvre un PLL sans mécanisme de rétroaction) et des raisons pratiques (le taux de rafraîchissement peut changer avec le temps, par exemple, j'ai vu qu'il varie de 58fps à 62fps sur Un dispositif donné).

    L'utilisation des appels sleep() dans une boucle de jeu pour rythmer votre animation est une mauvaise idée.

    Aller sans dormir

    Vous avez quelques choix. Vous pouvez utiliser l'approche "dessiner aussi rapidement que possible jusqu'à l'interpolation de l'échange de mémoire tampon", c'est ce que beaucoup d'applications basées sur GLSurfaceView#onDraw() font (elles le savent ou non). Ou vous pouvez utiliser Chorégraphe .

    Choreographer vous permet de définir un rappel qui déclenche le prochain VSYNC. Fait important, l'argument du rappel est le temps réel VSYNC. Donc, même si votre application ne se réveille pas tout de suite, vous avez toujours une image précise de la date de mise à jour de l'affichage. Cela s'avère très utile lors de la mise à jour de votre état de jeu.

    Le code qui met à jour l'état du jeu ne doit jamais être conçu pour avancer "un cadre". Compte tenu de la variété des périphériques et de la variété des taux de rafraîchissement qu'un seul périphérique peut utiliser, vous ne pouvez pas savoir ce qu'est un «cadre». Votre jeu sera légèrement lent ou légèrement rapide – ou si vous obtenez de la chance et quelqu'un essaie de le jouer sur un téléviseur verrouillé à 48 Hz sur HDMI, vous serez sérieusement lent. Vous devez déterminer la différence de temps entre la trame précédente et la trame actuelle et avancer l'état du jeu de manière appropriée.

    Cela peut nécessiter un peu de remaniement mental, mais cela en vaut la peine.

    Vous pouvez le voir en action en Breakout , qui avance la position de la balle en fonction du temps écoulé. Il coupe de gros sauts dans le temps en pièces plus petites pour garder la détection de collision simple. Le problème avec Breakout est qu'il utilise l'approche stuff-the-queue-full, les horodatages sont soumis à des variations dans le temps requis pour que SurfaceFlinger effectue le travail. En outre, lorsque la file d'attente du tampon est initialement vide, vous pouvez soumettre des images très rapidement. (Cela signifie que vous comptez deux images avec un delta presque nul, mais elles sont toujours envoyées à l'affichage à 60fps. En pratique, vous ne voyez pas cela, car la différence de l'horodatage est si petite qu'elle ressemble à la même image Dessiné deux fois, et cela ne se produit que lorsque vous passez d'une animation non animée à une animation, de sorte que vous ne voyez rien bégayer.)

    Avec Choreographer, vous obtenez le temps réel VSYNC, de sorte que vous obtenez une bonne horloge régulière pour baser vos intervalles de temps hors de. Parce que vous utilisez le temps de rafraîchissement de l'affichage comme source d'horloge, vous ne serez jamais synchronisé avec l'affichage.

    Bien sûr, vous devez toujours être prêt à déposer des cadres.

    Aucun cadre laissé derrière

    Un peu plus tard, j'ai ajouté une démo d'enregistrement d'écran à Grafika ("Record GL app") qui fait une animation très simple – juste un rectangle de rebond à l'ombre plate et un triangle tournant. Il avance l'état et se dessine lorsque le chorégraphe signale. Je l'ai codé, l'ai couru … et j'ai commencé à noter les rappels de chorégraphe en cours de sauvegarde.

    Après avoir creusé avec systrace , j'ai découvert que l'UI du cadre faisait occasionnellement un travail de mise en page (probablement à SurfaceView des boutons et du texte dans la couche d'interface utilisateur, qui se trouve sur la surface de SurfaceView ). Normalement, cela a pris 6 ms, mais si je ne bougeait pas activement mon doigt sur l'écran, mon Nexus 5 ralentit les différentes horloges pour réduire la consommation d'énergie et améliorer la durée de vie de la batterie. Le réaménagement a pris 28 ms à la place. Gardez à l'esprit qu'un cadre de 60fps est 16.7ms.

    Le rendu GL a été presque instantané, mais la mise à jour de Choreographer a été livrée au thread UI, qui était en train de broyer sur la mise en page, de sorte que mon thread de rendu n'a pas reçu le signal jusqu'à beaucoup plus tard. (Vous pourriez avoir Chorégraphe livrer le signal directement au fil de rendu, mais il y a un bug dans Choreographer qui causera une fuite de mémoire si vous le faites.) Le correctif était de supprimer des trames lorsque l'heure actuelle est supérieure à 15ms après le temps VSYNC. L'application fait toujours la mise à jour de l'état – la détection de collision est si rudimentaire que des choses étranges se produisent si vous laissez l'écart de temps trop grand – mais ne soumet pas de tampon à SurfaceFlinger.

    Lors de l'exécution de l'application, vous pouvez indiquer quand les images sont abandonnées, car Grafika clignote la bordure rouge et met à jour un compteur sur l'écran. Vous ne pouvez pas le dire en regardant l'animation. Étant donné que les mises à jour de l'état sont basées sur des intervalles de temps , et non sur les comptes de trame, tout se déplace aussi vite que si le cadre a été abandonné ou non, et à 60fps, vous ne remarquerez pas un seul cadre abandonné. (Cela dépend dans une certaine mesure de vos yeux, du jeu et des caractéristiques du matériel d'affichage.)

    Leçons clés:

    • Les chutes de trame peuvent être causées par des facteurs externes – dépendance d'un autre thread, vitesse d'horloge de la CPU, synchronisation Gmail arrière, etc.
    • Vous ne pouvez pas éviter toutes les chutes de trame.
    • Si vous définissez votre cycle de tirage au sort, personne ne le remarquera.

    Dessin

    Le rendu sur un Canvas peut être très efficace s'il est accéléré par le matériel. Si ce n'est pas le cas, et vous faites le logiciel de dessin, cela peut prendre un certain temps – surtout si vous touchez beaucoup de pixels.

    Deux éléments importants de lecture: découvrez le rendu accéléré par le matériel et utilisez le détartreur pour réduire le nombre de pixels que votre application doit toucher. Le «exerciseur de classeur de matériel» à Grafika vous donnera un sens pour ce qui se passe lorsque vous réduisez la taille de votre surface de dessin – vous pouvez être assez petit avant que les effets ne soient perceptibles. (Je trouve curieusement amusant de regarder GL faire un triangle tournant sur une surface 100×64).

    Vous pouvez également enlever le mystère du rendu en utilisant OpenGL ES directement. Il y a un peu de bêtises qui apprennent comment les choses fonctionnent, mais Breakout (et, pour un exemple plus élaboré, Replica Island ) montre tout ce dont vous avez besoin pour un jeu simple.

    Sans avoir jamais fait un jeu dans Android, j'ai fait des jeux 2D en Java / AWT en utilisant Canvas et bufferStrategy …

    Si vous rencontrez un scintillement, vous pouvez toujours utiliser un double tampon manuel (éliminer le scintillement) en le rendant sur une image hors écran, puis en basculant / desserrer avec les nouveaux contenus pré-rendus directement.

    Mais, j'ai l'impression que vous êtes plus préoccupé par la «douceur» de votre animation, auquel cas je vous recommande d'étendre votre code avec une interpolation entre les différentes tiques d'animation;

    À l'heure actuelle, votre état logique de mise à jour de la boucle de rendu (déplacer les choses de manière logique) au même rythme que vous rendez, et mesurer avec un certain temps de référence et essayer de suivre le temps passé.

    Au lieu de cela, vous devriez mettre à jour dans n'importe quelle fréquence que vous jugez souhaitable que les «logiques» de votre code fonctionnent – généralement 10 ou 25 Hz est très bien (je l'appelle «mise à jour des tiques», ce qui est complètement différent du FPS actuel) Alors que le rendu se fait en gardant un suivi de temps de haute résolution pour mesurer «combien de temps» prend votre rendu réel (j'ai utilisé nanoTime et cela a été assez suffisant, alors que currentTimeInMillis est plutôt inutile …)

    De cette façon, vous pouvez interpoler entre les tiques et afficher autant de cadres que possible jusqu'à la prochaine coche en calculant les positions fines en fonction de la durée écoulée depuis la dernière coche, par rapport à combien de temps «devrait» se situer entre deux Tiques (puisque vous savez toujours où vous êtes – position et où vous vous dirigez – vitesse)

    De cette façon, vous obtiendrez la même "vitesse d'animation" indépendamment de la CPU / plate-forme, mais plus ou moins de douceur, car les CPU plus rapides effectueront plus de rendus entre différentes tiques.

    MODIFIER

    Certains copier-coller / code conceptuel – mais notez que c'était AWT et J2SE, pas d'Android. Cependant, en tant que concept et avec une certaine Androidisation, je suis sûr que cette approche devrait fonctionner en douceur à moins que le calcul effectué dans votre logique / mise à jour soit trop lourd (p. Ex. Algorithmes N ^ 2 pour la détection de collision et N grandit avec des systèmes de particules et similaires ).

    Boucle de rendu active

    Au lieu de compter sur le repeindre pour faire la peinture pour vous (ce qui pourrait prendre un temps différent, selon ce que fonctionne l'OS), la première étape consiste à prendre un contrôle actif sur la boucle de rendu et à utiliser un BufferStrategy où vous rendez, puis activement " "Le contenu lorsque vous avez terminé, avant de revenir à nouveau.

    Stratégie tampon

    Peut-être exiger que certains articles Android spéciaux s'allongent, mais c'est assez simple. J'utilise 2 pages pour le bufferStrategy pour créer un mécanisme "page-flipping".

     try { EventQueue.invokeAndWait(new Runnable() { public void run() { canvas.createBufferStrategy(2); } }); } catch(Exception x) { //BufferStrategy creation interrupted! } 

    Boucle d'animation principale

    Ensuite, dans votre boucle principale, obtenez la stratégie et prenez le contrôle actif (ne pas utiliser repaint)!

     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 objet géré soit la boucle principale ci-dessus apparaîtrait quelque chose dans le sens;

     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); } } 

    Toutes les positions 2D sont gérées à l'aide d'une classe d'utilité GfxPoint avec une double précision (puisque les mouvements interpolés peuvent être très fins et l'arrondi n'est généralement pas recherché avant de rendre les graphiques réels). Pour simplifier les choses de mathématiques nécessaires et rendre le code plus lisible, j'ai également ajouté diverses méthodes.

     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 += px; y += py; } public void add(double x_inc, double y_inc) { x += x_inc; y += y_inc; } public void sub(GfxPoint p) { x -= px; y -= py; } public void sub(double x_dec, double y_dec) { x -= x_dec; y -= y_dec; } public void set(GfxPoint p) { x = px; y = py; } public void set(double x_new, double y_new) { x = x_new; y = y_new; } public void mult(GfxPoint p) { x *= px; y *= py; } 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(px, py); } 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; } } 

    Essayez celui-ci pour la taille. Vous remarquerez que vous ne synchronisez et verrouillez la toile que pour la période la plus courte. Sinon, le système d'exploitation sera soit A) Supprimez le tampon car vous étiez trop lent ou B) ne vous mettez pas à jour jusqu'à ce que votre attente de repos soit terminée.

     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++; } } } } 

    Tout d'abord, Canvas peut se produire mal, alors ne vous attendez pas trop. Vous voudrez peut-être essayer l'exemple lunaire du SDK et voir les performances que vous obtenez sur votre matériel.

    Essayez d'abaisser le maximum de fps à quelque chose comme 30, le but est d'être lisse pas vite.

      private final static int MAX_FPS = 30; // desired fps 

    Éloignez-vous également des appels de sommeil, le rendu sur la toile pourrait probablement dormir suffisamment. Essayez quelque chose de plus comme:

      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 vous voulez que vous puissiez faire votre this.mMainGameBoard.update() plus souvent que votre rendu.

    Edit: De même, puisque vous dites que les choses se ralentissent lorsque les obstacles apparaissent. Essayez de les dessiner sur un canevas hors-toile / Bitmap. J'ai entendu dire que certaines des méthodes drawSHAPE sont optimisées pour la CPU et que vous obtiendrez de meilleures performances en les dessinant sur un canevas / bitmap hors ligne car ceux-ci ne sont pas matériels / gpu accélérés.

    Edit2: qu'est-ce que Canvas.isHardwareAccelerated () retourne pour vous?

    L'une des causes les plus courantes de ralentissement et de bégaiement dans un jeu est le pipeline graphique. La logique de jeu est beaucoup plus rapide à traiter que celle de dessiner (en général) afin de vous assurer que vous dessinez tout de la manière la plus efficace possible. Vous trouverez ci-dessous quelques conseils sur la façon d'y parvenir.

    Une suggestion pour mieux le faire

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

    coAndroid est un fan Android de Google, tout sur les téléphones Android, Android Wear, Android Dev et Android Games Apps.