android boucle de jeu vs la mise à jour dans le thread de rendu
Je suis en train de faire un jeu android et je suis en train de ne pas obtenir de la performance, je voudrais. J'ai une boucle de jeu dans son propre thread qui met à jour la position d'un objet. Le thread de rendu la traversée de ces objets et de les dessiner. Le comportement actuel est ce qui semble être agitée/mouvement irrégulier. Ce que je ne peux pas expliquer, c'est qu'avant j'ai mis la mise à jour de la logique dans son propre thread, je l'ai eu dans le onDrawFrame méthode, juste avant le gl appels. Dans ce cas, l'animation était parfaitement lisse, elle ne devient saccadée/irrégulier spécifiquement lorsque j'essaie de limiter ma mise à jour en boucle via le Fil de discussion.sommeil. Même quand j'permettre la mise à jour du thread pour aller berserk (pas de sommeil), l'animation est fluide, uniquement lorsque le Thread.le sommeil participe-t-il sur la qualité de l'animation.
J'ai créé un squelette de projet pour voir si je pouvais recréer la question, voici la mise à jour de boucle et de la onDrawFrame méthode dans le moteur de rendu:
mise à Jour de Boucle
@Override
public void run()
{
while(gameOn)
{
long currentRun = SystemClock.uptimeMillis();
if(lastRun == 0)
{
lastRun = currentRun - 16;
}
long delta = currentRun - lastRun;
lastRun = currentRun;
posY += moveY*delta/20.0;
GlobalObjects.ypos = posY;
long rightNow = SystemClock.uptimeMillis();
if(rightNow - currentRun < 16)
{
try {
Thread.sleep(16 - (rightNow - currentRun));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Et voici mon onDrawFrame méthode:
@Override
public void onDrawFrame(GL10 gl) {
gl.glClearColor(1f, 1f, 0, 0);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
gl.glTranslatef(transX, GlobalObjects.ypos, transZ);
//gl.glRotatef(45, 0, 0, 1);
//gl.glColor4f(0, 1, 0, 0);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, uvBuffer);
gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length,
GL10.GL_UNSIGNED_SHORT, indiceBuffer);
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}
J'ai regardé à travers réplique de l'île de la source et il fait de sa logique de mise à jour dans un thread séparé, ainsi que de la limitation des avec Fil.le sommeil, mais son jeu est très lisse. Quelqu'un a une idée ou quelqu'un a vécu ce que je parle?
---EDIT: 1/25/13---
J'ai eu le temps de penser et d'avoir lissé ce moteur de jeu considérablement. Comment j'ai réussi ce pourrait être blasphématoire ou insultant pour les programmeurs, n'hésitez pas à corriger l'une quelconque de ces idées.
L'idée de base est de garder un motif de mise à jour, de dessiner... mise à jour, de dessiner... tout en gardant le temps delta relativement de la même façon (souvent hors de votre contrôle).
Mon premier cours de l'action était de synchroniser mon convertisseur de telle manière qu'il fit seulement après avoir été informé qu'il était autorisé à le faire. Cela ressemble à quelque chose comme ceci:
public void onDrawFrame(GL10 gl10) {
synchronized(drawLock)
{
while(!GlobalGameObjects.getInstance().isUpdateHappened())
{
try
{
Log.d("test1", "draw locking");
drawLock.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
Quand j'ai fini ma logique de mise à jour, j'appelle drawLock.notify(), libérant le thread de rendu à dessiner ce que je viens de mettre à jour. Le but de cette opération est d'aider à établir le schéma de mise à jour, de dessiner... mise à jour, de dessiner... etc.
Une fois que j'ai mis en place, il est beaucoup plus lisse, bien que j'avais toujours des sauts occasionnels dans le mouvement. Après quelques tests, j'ai vu que j'avais plusieurs mises à jour ont lieu entre les appels de ondrawFrame. C'était à l'origine une image pour montrer le résultat de deux (ou plus) des mises à jour, un plus grand saut que la normale.
Ce que j'ai fait pour résoudre ce a été de limiter le temps delta à une certaine valeur, dire 18ms, entre deux onDrawFrame appels et de stocker le plus de temps dans un reste. Ce reste sera distribué aux temps de deltas au cours de la prochaine quelques mises à jour si elles pouvaient gérer. Cette idée empêche tous soudaine de longs sauts, essentiellement le lissage d'un temps de spike sur plusieurs images. En faisant cela m'a donné d'excellents résultats.
L'inconvénient de cette approche est que pour un peu de temps, la position des objets ne seront pas précises avec le temps, et sera en fait accélérer pour rattraper cette différence. Mais c'est plus lisse et le changement de vitesse n'est pas très visible.
Enfin, j'ai décidé de réécrire mon moteur avec les deux idées ci-dessus à l'esprit, plutôt que de rafistoler le moteur que j'avais faites à l'origine. J'ai fait quelques optimisations pour la synchronisation de thread que peut-être quelqu'un pourrait commenter.
Mes threads en cours d'interagir comme ceci:
-mise à Jour du thread mises à jour du tampon courant (double système de tampon afin de mettre à jour et de tirer en même temps) et ensuite donner ce tampon pour le moteur de rendu si l'image précédente a été tirée.
-Si l'image précédente n'a pas encore dessiner, ou est le dessin, la mise à jour du thread attendre jusqu'à ce que le thread de rendu notifie qu'il a dessiné.
-thread de Rendu attend jusqu'à ce que notifié par mise à jour du thread une mise à jour a eu lieu.
-Lorsque le thread de rendu dessine, il définit une "dernière variable" pour indiquer laquelle des deux buffers, ce dernier a attiré et informe également sur la mise à jour thread si elle a été en attente sur le précédent de la mémoire tampon pour être tiré.
Peut-être un peu compliquée, mais ce qui est fait est permettant pour que les avantages du multithreading, en ce qu'elle peut effectuer la mise à jour de l'image n, tandis que l'image n-1 est le dessin, tout en empêchant la mise à jour de plusieurs itérations par image si le moteur de rendu est de prendre beaucoup de temps. Pour expliquer plus en détail, ce multiple-scénario de mise à jour est assurée par la mise à jour de filetage de verrouillage s'il détecte que le lastDrawn tampon est égale à celle qui a été juste mis à jour. Si elles sont égales, ce qui indique à la mise à jour du thread que le cadre avant n'a pas encore été établi.
Jusqu'à présent, j'obtiens de bons résultats. Laissez-moi savoir si quelqu'un a des commentaires, serait heureux d'entendre vos pensées sur quelque chose que je suis en train de faire, de bonne ou de mauvaise.
Grâce
Vous devez vous connecter pour publier un commentaire.
(La réponse de Blackhex soulève quelques points intéressants, mais je ne peux pas garder tout ça dans un commentaire.)
Avoir deux fils d'exploitation de manière asynchrone est lié à conduire à des questions de ce genre. Regardons cela de cette façon: l'événement qui entraîne l'animation est du matériel "vsync" le signal, c'est à dire le point où le Android de surface compositeur propose un nouvel écran de données pour le matériel d'affichage. Vous voulez avoir un nouveau bloc de données à chaque fois que vsync arrive. Si vous n'avez pas de nouvelles données, le jeu s'annonce agitée. Si vous avez généré 3 trames de données dans cette période, deux vont être ignorées, et vous êtes juste de gaspiller la vie de la batterie.
(L'exécution d'un PROCESSEUR complet peut également causer de l'appareil à la chaleur, ce qui peut conduire à la limitation thermique, ce qui ralentit tout le système vers le bas... et peut rendre votre animation saccadée.)
La meilleure façon de rester en synchronisation avec l'affichage est d'effectuer toutes vos mises à jour d'état dans
onDrawFrame()
. Si elle prend parfois plus de temps qu'une image afin d'effectuer vos mises à jour d'état et le rendu de l'image, puis vous allez l'air mauvais, et avez besoin de modifier votre approche. Le simple fait de transférer toutes les mises à jour d'état à un second noyau ne va pas aider autant que vous aimeriez -- si le core #1 est le moteur de rendu de thread, et core #2 est le jeu de l'état de mise à jour du fil, puis core #1 va rester les bras croisés tandis que les core #2 mises à jour de l'état, après quoi core #1 résumé pour faire le rendu de tout cœur #2 est inactive, et il va prendre autant de temps. Pour augmenter la quantité de calcul que vous pouvez faire par image, vous aurez besoin d'avoir deux (ou plus) de cœurs de travail simultanément, ce qui pose d'intéressants problèmes de synchronisation selon la façon dont on définit la division du travail (voir http://developer.android.com/training/articles/smp.html si vous voulez aller sur la route).De tenter de l'utiliser
Thread.sleep()
pour gérer le taux de trame se termine généralement mal. Vous ne pouvez pas savoir combien de temps la période de l'entre-vsync est, ou combien de temps jusqu'à ce que le suivant arrive. C'est différent pour chaque périphérique, et sur certains appareils, il peut être variable. Vous avez essentiellement deux horloges -- vsync et de sommeil-battre les uns contre les autres, et le résultat est agitée de l'animation. En plus de cela,Thread.sleep()
n'est pas spécifique des garanties quant à l'exactitude ou le minimum de la durée du sommeil.Je n'ai pas vraiment passé par la Réplique de l'Île de sources, mais dans
GameRenderer.onDrawFrame()
vous pouvez voir l'interaction entre leur état de jeu de thread (ce qui crée une liste d'objets à dessiner) et le rendu GL thread (ce qui attire tout de la liste). Dans leur modèle, l'état de la partie seulement des mises à jour en tant que de besoin, et si rien n'a changé, il viens de re-dessine le précédent tirage de la liste. Ce modèle fonctionne bien pour un event-driven jeu, c'est à dire le contenu sur l'écran de mise à jour quand il se passe quelque chose (vous appuyez sur une touche, un compte à rebours incendies, etc). Lorsqu'un événement se produit, ils peuvent faire un état minimal de mise à jour et ajuster le tirage au sort de la liste selon le cas.D'une autre façon, le thread de rendu et de l'état de jeu travailler en parallèle, car ils ne sont pas rigidement liés entre eux. Le jeu tourne juste autour de la mise à jour des choses comme nécessaires, et le thread de rendu il verrouille tous les vsync et dessine tout ce qu'il trouve. Tant qu'aucun des deux camps ne garde rien enfermé pendant trop longtemps, ils n'ont visiblement pas interférer. Le seul intérêt de l'état partagé est le tirage au sort de la liste, surveillé avec un mutex, de sorte que leur multi-core questions sont réduites au minimum.
Pour Android Breakout ( http://code.google.com/p/android-breakout/ ), le jeu a une balle de rebondir, dans un mouvement continu. Il nous voulons mettre à jour notre état aussi souvent que l'écran nous permet de nous, donc nous conduire à la modification de l'état de désactiver la vsync, à l'aide d'un temps delta à partir de l'image précédente pour déterminer dans quelle mesure les choses ont avancé. Le frame par frame calcul est petit, et le rendu est assez trivial pour un moderne GL appareil, de sorte qu'il s'intègre facilement dans le 1/60ème de seconde. Si l'affichage de la mise à jour beaucoup plus rapidement (240Hz) on pourrait parfois laisser tomber les cadres (encore une fois, peu de chances d'être remarqué), et nous serions de gravure de 4x autant de CPU sur le cadre des mises à jour (ce qui est regrettable).
Si pour une raison quelconque l'un de ces jeux manqué un vsync, le joueur peut ou peut ne pas remarquer. L'état avance par le temps écoulé, pas de pré-définir la notion de fixe la durée de "cadre", de sorte que par exemple, la balle sera soit un déplacement de 1 unité sur chacun des deux images consécutives, ou 2 unités sur une image. En fonction de la fréquence d'image et la réactivité de l'écran, ceci peut ne pas être visible. (Ceci est un problème de conception, et que peut mess avec votre tête si vous envisagé votre état de jeu en termes de "tiques".)
Ces deux sont des approches valables. La clé est d'attirer l'état actuel chaque fois que
onDrawFrame
est appelée, et à la mise à jour de l'état aussi rarement que possible.Note pour quelqu'un d'autre qui arrive à lire ceci: ne pas utiliser
System.currentTimeMillis()
. L'exemple de la question utiliséeSystemClock.uptimeMillis()
, qui est basée sur la fonction monotone de l'horloge plutôt que sur l'horloge murale. Que, ouSystem.nanoTime()
, sont de meilleurs choix. (Je suis sur un mineur croisade contrecurrentTimeMillis
, qui, sur un appareil mobile pourrait soudainement saut vers l'avant ou vers l'arrière.)Mise à jour:, j'ai écrit un même plus de réponse à une question similaire.
Mise à jour 2:, j'ai écrit un même plus, plus de réponse sur le problème général (voir l'Annexe A).
Une partie du problème peut être causé par le fait que le Thread.sleep() n'est pas exacte. Essayez d'enquêter sur ce qu'est le temps réel de la veille.
La chose la plus importante qui devrait rendre vos animations lisses, c'est que vous devez calculer certaines facteur d'interpolation, de l'appeler alpha, qui interpole linéairement vos animations dans consécutives thread de rendu des appels entre deux années consécutives à l'animation de la mise à jour des appels de fil. En d'autres termes, si votre intervalle de mise à jour est élevé par rapport à vos framerate, pas d'interpolation de votre animation de l'opération de mise à jour, c'est comme vous seriez rendu à l'intervalle de mise à jour framerate.
EDIT: Comme un exemple, c'est comment Regardern t-il: