Posté le 19/04/2018 21:14
Planète Casio v4.3 © créé par Neuronix et Muelsaco 2004 - 2024 | Il y a 118 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements
Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd
Citer : Posté le 19/04/2018 21:24 | #
Ce que la majorité des moteurs de jeux proposent, c'est une abstraction qui permet à l'utilisateur de spécifier simplement la logique qui doit être exécutée régulièrement, à travers deux mécanismes :
une fonction update(delta) qui prend en paramètre le temps écoulé depuis le dernier appel à cette fonction, et doit modifier l'état du jeu en conséquence (physique, collisions, réagir à l'input de l'utilisateur -j'y reviendrai)
une fonction draw(delta) qui prend également en paramètre le delta-time, et se charge d'effectuer le dessin. Si on reste haut niveau, on n'a souvent même pas besoin de l'override parce qu'on travaille avec des classes déjà toute faites pour les sprites, animations, etc.
Pour citer deux frameworks avec lesquels j'ai déjà fait des jeux, ExcaliburJS en TypeScript et cocos2d en C++, ils ont tout deux un système de scènes qui incorporent elles-même des acteurs, la fonction update de la scène est appelée par le moteur, et elle appelle elle-même les fonctions update de ses acteurs, et de même pour la fonction draw.
Il est important de noter qu'on n'a souvent aucune garantie qu'un appel à update ou à draw ne va pas être sauté, à moins de faire en sorte d'avoir de la marge sur le temps d'exécution alloué pour le framerate visé.
Bon, après tout cela n'est pas valable pour tous les moteurs, par exemple pygame en Python est impératif et on utilise une espèce de fonction sleep pour attendre un certain intervalle depuis le dernier appel.
EDIT:
Oh, et puisque j'avais dit que je reviendrais sur l'input, petite note là-dessus. Le truc avec l'input c'est que tous les moteurs ne le gèrent pas de la même façon, donc ce que j'ai vu plusieurs fois c'est simplement une gestion événementielle, et comme c'est un peu relou parce qu'on veut s'en servir dans la fonction update, ce que je fais c'est que je maintiens un tableau dynamique avec les touches actuellement pressées, et je gère via les événements up et down du keyboard. Ça marche assez bien.
Citer : Posté le 19/04/2018 21:27 | #
Est-ce que tu peux en dire plus sur la façon dans les appels peuvent être « sautés » ? Que se passe-t-il si l'utilisateur implémente des fonctions update() de draw() qui mettent des plombes à terminer ?
Et questions plus rapide, si tu as une scène avec deux objets entortillés, comment peut-on faire le rendu ? Dès qu'on affiche l'un ça écrase l'autre. Je me serait attendu à ce que la scène soit capable de faire des choses plus fines que juste demander aux objets de se dessiner.
Citer : Posté le 19/04/2018 21:32 | #
Pour répondre rapidement à la deuxième question :
En 3D, https://fr.wikipedia.org/wiki/Z-buffer
En 2D, chaque sprite a un z-index, la scène a un arbre pour ordonner ses sprites et appeler leur draw en respectant leur z-index.
Citer : Posté le 19/04/2018 21:39 | #
L'histoire du Z-buffer signifie-t-elle que la fonction draw() des acteurs ne dessine pas tous les pixels de chaque acteur ? Je propose cette interprétation par opposition avec un cas où la logique de décision serait dans le code de la scène.
Citer : Posté le 19/04/2018 21:43 | #
Nan, l'algo suivi est simplement :
Si le pixel que je veux dessiner a une profondeur plus grande que celle marquée dans le z-buffer, je m'abstiens.
Si le pixel que je veux dessiner est devant, je ré-écris ce pixel.
En gros, peu importe l'ordre de dessin, on vérifie à chaque fois si on doit dessiner le pixel ou pas.
Après y'a quelques trucs à gérer comme la transparence qui est un peu relou.
Citer : Posté le 19/04/2018 21:47 | #
Bon, ça correspond à ce que j'imaginais donc, je devais pas être clair.
Une remarque (rapide) pour mon autre question, ce qui se passe si les fonctions appelées par le moteur sont trop lentes ?
Citer : Posté le 19/04/2018 21:51 | #
Pour ta première question, c'est plus tendu et ça dépend du moteur. Je pense pas qu'il y ait de solution magique pour gérer une fonction trop lente.
La solution simple que font certains moteurs est d'appeler les deux fonctions avec le timer c'est d'essayer d'appeler les fonctions régulièrement, abandonner s'il ya déjà une exécution : si c'est trop long bah le framerate en prend en coup mais c'est difficile à éviter de toute façon. C'est au programmeur d'être intelligent et de gérer son temps.
Il y a sûrement des moteurs qui font ça intelligemment, tu piques ma curiosité et ça mérite que je m'y penche quand j'aurai un peu de temps.
Par rapport à la 3D et au z-buffer, si quelque-chose t'échappe tu as peut-être une mauvais compréhension de la pipeline graphique (https://en.wikipedia.org/wiki/Graphics_pipeline#Shader)
Ajouté le 19/04/2018 à 21:55 :
Si tu fonctionnes avec deux timers, tu peux avoir un système pour vérifier si une exécution de ta fonction est en cours ou non.
Si l'update prend plus longtemps, ce système basique peut continuer à faire des draw, et inversement. Dans une certaine limite ce n'est pas gênant, mais si tu as des animations à 60 fps et une physique à 5 fps bof.
Ajouté le 19/04/2018 à 21:59 :
Disclaimer : je n'ai pas lu le document suivant.
Mais ça peut être intéressant, à toi de voir : http://www.brandonfoltz.com/downloads/tutorials/The_Game_Loop_and_Frame_Rate_Management.pdf
Citer : Posté le 19/04/2018 22:15 | #
Eh bien, que de lecture ! Ma connaissance du pipeline graphique est en effet superficielle ; je vais m'y pencher.
Je te dois quelques détails de plus. Les timers du processeur de la calculatrice sont très précis (µs) mais peu nombreux : dans les versions actuelles de gint, 3. Du coup j'avais implémenté un système de timer virtuels moins précis (ms) qui tournent tous en même temps sur un seul timer physique.
En vérité, il y a 4 timers physiques sur SH3 et visiblement 9 sur SH4. Sur G85, on ne peut donc en utiliser que 4, mais 9 sur G90, ce qui est bien assez large. Reste que gint en utilise au moins un pour le moteur de gris, et jusqu'ici, un pour le clavier. Donc sur G85, ça ne donne que 2 timers pour coder le jeu, j'avais peur que ce soit short.
Ce que je vais faire c'est donner une option pour faire tourner le clavier sur la RTC pour libérer un 3ème timer, et en cas de besoin plus ultime, je proposerai le système de timer virtuels comme une lib' en plus, pour ceux qui veulent. Cela libérera gint des timers virtuels, avec lesquels j'ai toujours eu du mal.
Du reste, comme mon but ultime derrière gint c'est de coder TLT (sur G90 probablement, j'ai trop bavé -pun intended- de mes tests sur G85), toutes ces histoires de moteur m'intéressent beaucoup. Enfin TLT est très 2D donc ça simplifie les choses.
Pour revenir au sujet, le problème est que si le timer du moteur est réglé sur 10 ms et que ma fonction prend 11 ms le framerate est divisé par deux par rapport à ce qui est faisable, dans une implémentation naïve. Cela dit un petit bit de « j'ai des frames en retard » dans le moteur peut régulariser ça et l'inciter à rappeler la fonction trop lente dès qu'elle se termine.
Pour un jeu sur calculatrice où de toute façon la majorité de la puissance sera occupée parce que le proco est pas très puissant, mon idée était d'appeler le moteur physique sur une base régulière (eg. 30 FPS) et de faire tourner le moteur graphique dans la boucle principale.
Dans une situation où le moteur graphique n'arrive pas à tenir 30 FPS, il tourne à fond et envoie des frames aussi vite qu'il le peut. Quand il prendra trop de retard, un frame sera naturellement sauté parce que les données physiques auront été mises à jour deux fois pendant un seul frame. (Cela nécessite évidemment un peu de synchronisation pour éviter des collisions de type WAR, mais ça on sait faire.)
Dans la situation où il arrive à suivre, à chaque frame rendu le processeur est endormi jusqu'à ce que le moteur physique soit appelé par le timer, ce qui donne des données fraîches à faire afficher par le moteur graphique.
S'il te vient une remarque ou un défaut évident sur ce système, je suis preneur.
Citer : Posté le 20/04/2018 10:28 | #
Merci de faire un topic pour laisser des traces des sujets de discussion du chat
- Pour ma part, j'utilise 0 ou 1 timers pour mes jeux. Jamais plus.
Avec 0 timer quand j'utilisais une fonction de régulation de FPS, c'était crado, je ne fais plus comme ça.
Avec 1 timer pour faire des jeux simples, ça permet d'avoir un synchronisme entre la physique et le rendu graphique. Je calcul puis j'affiche dans la fonction appellée par le timer, dans la boucle principale c'est un Getkey pour "bloquer le programme" (occune idée si c'est bien ou pas).
Ou alors, comme je fais comme dans Windmill, mon moteur 3D, 1 timer pour avoir une physique régulée et le rendu dans la boucle principale pour un max de FPS.
Donc pour l'utilisation que j'en fais 2 timers suffisent.
Pour moi il y a un point important, c'est que dans la majorité des cas, il faut y avoir un synchronisme exacte entre la physique et le rendu. C'est un débat qu'on a déjà eu, mais grosso modo, si le rendu va plus vite que la physique, il va dessiner plusieurs fois la même frame. Le moyen de contrer ça est d'interpoler entre deux frames. Mais vu comment c'est compliqué et pour l'apport que ça représente personne ne voudrait se lancer là dedans...
Pour moi la solution optimale serait simplement d'avoir une fonction particulière appellée toutes les X secondes. Le programmeur pose son code dedans. Comme la fonction draw() dans Processing qui est une fonction particulière appellée tous les X secondes modifiable via FrameRate = X.
Citer : Posté le 20/04/2018 10:40 | #
Le semestre dernier, j'ai codé en C# avec le Framework Monogame, qui utilisait les méthodes Update() et Draw(), basée sur ce qu'on appelait le GameTime, le temps qui s'écoulait en jeu.
Ce semestre, c'est la même chose mais avec SFML, un autre Framework normalement utilisé en C++ mais porté pour le C#. J'utilise les deux mêmes méthodes dans la plupart de mes classes mais j'utilise d'autres formes de timer. Déjà, un pour les FPS (le Delta Time dont parlait Louloux mais utilisé différemment) et d'autres mais qui ne rentrent certainement pas dans la catégorie dont vous parlez, ceux que j'utilise pour mettre des cooldown pour les utilisations de potions, le rechargement d'arme, etc.
Citer : Posté le 20/04/2018 11:24 | #
Je ne crois pas que l'égalité des vitesses soit une propriété vraiment importante.
Si le rendu va plus vite que la physique, tu as plusieurs options :
- Une fois le rendu du frame terminé, tu dors jusqu'à ce que le timer relance la physique.
- Tu augmentes la fréquence du jeu pour profiter de tes super perfs.
Si le rendu n'arrive pas à suivre sur la physique, tu vas perdre des frames à intervalles réguliers. Si c'est trop visible et tout moche, tu peux ajuster la vitesse de ton moteur physique.
Edit : Évidemment la solution que l'on veut tous c'est la première parce que c'est la plus fluide et la plus régulière (pas de « sauts de frames »).
Ajouté le 20/04/2018 à 11:26 :
C'est à ce genre de timers que je pensais aussi. Pour t'en sortir avec moins de 90 timers en parallèle, l'idée serait de tous les coller à des fréquences multiples pour qu'un seul timer puisse les gérer en même temps.
Citer : Posté le 20/04/2018 11:29 | #
Comment marcherait cette technique ?
Citer : Posté le 20/04/2018 11:54 | #
Eh bien, tu décides par exemple que les utilisations de potions, les rechargements d'armes et tous les autres phénomènes du même type ont des délais multiples de 25 ms. Tu crées un ensemble dans lequel tu mets tous les événements en attente, et tu lances un timer avec une fréquence de 25 ms.
Chaque fois que le timer arrive à expiration, tu réduis le temps d'attente de tous les phénomènes dans l'ensemble de 1, puis tu exécutes (et supprimes de l'ensemble) tous ceux dont le temps d'attente tombe à 0.
Parenthèse algorithmique : pour que ce soit efficace il faut une structure de données qui tienne la route, mais en gros avec un ABR en style delta-list (ou un compteur de temps) tu dois pouvoir insérer un nouvel élément dans l'ensemble et sortir tous ceux qui ont besoin d'être exécutés en temps logarithmique.
Citer : Posté le 20/04/2018 12:30 | #
Si le rendu va plus vite que la physique, tu as plusieurs options :
- Une fois le rendu du frame terminé, tu dors jusqu'à ce que le timer relance la physique.
- Tu augmentes la fréquence du jeu pour profiter de tes super perfs.
Si le rendu n'arrive pas à suivre sur la physique, tu vas perdre des frames à intervalles réguliers. Si c'est trop visible et tout moche, tu peux ajuster la vitesse de ton moteur physique.
Et bien là tu cherches à ne pas faire de "sur-affichage" en bridant le rendu ou en accélérant la physique. Ce qui revient à synchroniser les deux ^^’
Autant mettre les deux à la suite dans le même timer.
Pourquoi utiliser des timers pour les cooldown ?
Tu enregistres dans un tableau le temps de départ (quand tu bois ta potion) et le temps de pause (le cooldown)
Ensuite il suffit de faire le ratio avec le temps actuel pour savoir si le cooldown est fini ou en cours.
Ce calcul se fait pendant la physique. Si le cooldown est fini on retire l’element du tableau
Citer : Posté le 20/04/2018 12:38 | #
J'avoue avoir du mal à percevoir cette technique mais ça a l'air d'être en phase avec la structure de notre modèle objet. Mon binôme qui m'a tout expliqué n'utilise que des valeurs entre 0 et 1 dans son modèle objet qu'il multiplie lors de l'affichage et des appels de fonctions pour avoir les valeurs que nous voulons.
Citer : Posté le 20/04/2018 12:40 | #
Je pense pas, car si le jeu laggue à cause de l'affichage, la physique va elle aussi avoir du mal. N'oublions pas que sur nos chères caltos, c'est l'affichage qui coince la majeure partie du temps.
Citer : Posté le 20/04/2018 13:14 | #
Pour moi il y a un point important, c'est que dans la majorité des cas, il faut y avoir un synchronisme exacte entre la physique et le rendu. C'est un débat qu'on a déjà eu, mais grosso modo, si le rendu va plus vite que la physique, il va dessiner plusieurs fois la même frame.
Non, les animations par exemple ne dépendent pas de l'update, et un jeu peut tout à fait être fluide avec les animations à 60 fps et la physique à 30. Réciproquement, on peut louper quelques frames mais vouloir une physique qui tient la route pour gérer par exemple les collisions.
Après, ta remarque est légitime sur calto où il n'y a aucun parallélisme, et où les graphismes ont un coût processeur plus important. Sur ordi, on peut avoir intérêt à prendre le maximum qu'on peut tirer sans surcoût pour la logique comme l'affichage. Néanmoins, je pense que pas mal de moteurs font le choix de la synchronisation, surtout pour des raisons de simplicité pour le programmeur.
sur G85, ça ne donne que 2 timers pour coder le jeu, j'avais peur que ce soit short.
Ce serait short si les composants étaient gérés individuellement par des timers, mais puisqu'on a vu que la fonction update de la scène appelle les fonctions update des différents acteurs, il n'y effectivement pas besoin de plus de timers.
Du reste, comme mon but ultime derrière gint c'est de coder TLT (sur G90 probablement, j'ai trop bavé -pun intended- de mes tests sur G85), toutes ces histoires de moteur m'intéressent beaucoup. Enfin TLT est très 2D donc ça simplifie les choses.
Il serait intéressant de coder un moteur de jeu haut niveau basé sur des scènes, qui elles-mêmes contiennent des acteurs. Ça sera un peu de boulot de faire le moteur mais après d'autres programmeurs pourront plus facilement faire des jeux propres et efficaces.
Une idée qui me passe par la tête, pour implémenter la Game Loop en optimisant le framerate, en considérant le fait qu'on ne travaille pas sur une architecture parallèle :
L'utilisateur définit le framerate souhaité et le ratio entre le nombre d'update de la logic et le nombre de draw.
Un timer est appelé à intervalles très courts.
Le timer vérifie qu'aucune des fonctions n'est en exécution (par exemple avec un système de verrous).
Si aucune fonction n'est en exécution, et si on n'est pas en avance sur le framerate souhaité, on choisit d'appeler soit draw soit update selon le ratio souhaité et selon un historique des appels (buffer ou moving average).
Avantages :
Utilisation correcte des performances.
Pas mal de contrôle.
Inconvénient :
Si le timer est réglé à intervalles trop courts, on fait beaucoup d'interruptions pour rien. Trop long et on risque de laisser le processeur inactif.
Citer : Posté le 20/04/2018 13:39 | #
J'ai pas trop regardé comment fonctionne le C-Engine, mais est-ce que c'est un truc comme ça à quoi tu penses ?
Citer : Posté le 20/04/2018 13:45 | #
Autant mettre les deux à la suite dans le même timer.
Faire du "sur-affichage", pour ce que je vois, c'est triplement inutile :
- On recalcule deux fois le même frame donc rien ne change à l'écran
- On bouffe de la puissance de calcul donc de la batterie
- On perd en réactivité si on est au milieu d'un frame quand le frame physique suivant devient disponible
Je ne vois pas ce que tu veux faire en espérant rendre plus d'un frame graphique par frame physique. Pour moi il n'y a aucun intérêt à faire des calculs compliqués pour extrapoler entre deux frames alors qu'il suffirait de faire tourner le moteur physique deux fois plus souvent pour obtenir deux fois plus de données.
Non parce que même si l'affichage lagge mon joueur (je dis mon parce que c'est la façon dont je vois les choses) s'attend à ce que la physique tourne quand même à la même vitesse.
Surtout en cas de lag soudain et très passager (1-2s), où les actions de l'utilisateur sur le clavier ne prennent alors pas de « retard ». Je sais que ce genre de délais est chiant ; pour moi il faut chercher à garantir que la physique du jeu soit régulière.
Je suis d'accord. Je propose juste (pour répondre à la première question) que les calculs physiques sont a priori lourds donc pas fait trop souvent. On peut vouloir, si la fréquence du timer physique est pas assez élevé, vérifier les cooldowns plus souvent. Dans l'update, on pourra alors dit « la régen n'a été active que pendant la moitié du frame donc je régénère moitié moins de PV ». C'est un exemple théorique surtout.
Je valide donc le changement d'API qui ne garantit pas plus de 2 timers. On pourra toujours en avoir 3 en changeant une option de compilation pour passer le clavier sur la RTC.
Par où devrais-je commencer la lecture pour appréhender ces sujets ? Je n'y connais pas grand-chose et je n'ai probablement pas le luxe temporel d'implémenter plus qu'un petit jeu pour me faire la main.
C'est pas mal, mais je pense qu'on peut affiner un peu :
- Si le rendu graphique lagge, actuellement on n'a pas de garanties sur la vitesse de la physique (voir au-dessus).
- Le ratio souhaité f_physique / f_graphique (en fréquences) sera toujours plus grand que 1 pour les histoires de "sur-affichage" dont on parlait (sauf si Ninestars me détrompe sur la définition ! ).
- Pas besoin de verrous a priori puisqu'on peut désactiver les interruptions du pendant qu'une fonction est en train de s'exécuter.
Si l'on prend ces facteurs en compte, je pense qu'il n'y a qu'une différence entre ta proposition et le mienne : dans la mienne, update a la priorité sur draw et l’interrompt en cas de conflit (plus la synchro nécessaire pour que ça marche).
Pour la vitesse de timers, en jouant avec l'interrupt handler on peut minimiser le coût de la chose. Mais je préfère ce que j'ai dit plus haut, n'activer les interruptions que quand on sait qu'on est libre.
Ajouté le 20/04/2018 à 15:06 :
Le document de Louloux, qui introduit les Δtime et le frame rate variable, corrobore mon argument (@Ninestars) :
Maintenant les histoires de Δtime sont intéressantes parce qu'il n'y a pas de risque que le moteur graphique affiches des informations obsolètes (il tourne toujours juste après le moteur physique). Le problème si le moteur physique tourne à échéances fixes, c'est que si le moteur graphique met 1.1 fois la durée allouée il va accumuler du retard et afficher les infos de plus en plus tard. Je pense que le « saut » qui en résulte (au bout de 10 tours) est vraiment à éviter, et je vais sans doute revoir mon modèle.
Citer : Posté le 20/04/2018 15:55 | #
Je réponds à tous ça ce soir