Boucle de jeu «optimale» pour le défilement latéral 2D

14

Est-il possible de décrire une disposition "optimale" (en termes de performances) pour une boucle de jeu à défilement horizontal 2D? Dans ce contexte, la "boucle de jeu" prend la saisie de l'utilisateur, met à jour les états des objets de jeu et dessine les objets de jeu.

Par exemple, avoir une classe de base GameObject avec une hiérarchie d'héritage profonde pourrait être bon pour la maintenance ... vous pouvez faire quelque chose comme ceci:

foreach(GameObject g in gameObjects) g.update();

Cependant, je pense que cette approche peut créer des problèmes de performances.

D'un autre côté, toutes les données et fonctions des objets de jeu pourraient être globales. Ce qui serait un casse-tête de maintenance mais pourrait être plus proche d'une boucle de jeu performante.

Des pensées? Je m'intéresse aux applications pratiques d'une structure de boucle de jeu presque optimale ... même si j'ai un mal de tête de maintenance en échange de bonnes performances.

MrDatabase
la source

Réponses:

45

Par exemple, avoir une classe de base GameObject avec une hiérarchie d'héritage profonde pourrait être bon pour la maintenance ...

En fait, les hiérarchies profondes sont généralement pires pour la maintenabilité que celles peu profondes, et le style architectural moderne des objets de jeu tend vers des approches superficielles basées sur l'agrégation .

Cependant, je pense que cette approche peut créer des problèmes de performances. D'un autre côté, toutes les données et fonctions des objets de jeu pourraient être globales. Ce qui serait un casse-tête de maintenance mais pourrait être plus proche d'une boucle de jeu performante.

La boucle que vous avez montrée présente potentiellement des problèmes de performances, mais pas, comme l'implique votre instruction suivante, car vous avez des données d'instance et des fonctions membres dans la GameObjectclasse. Au contraire, le problème est que si vous traitez chaque objet du jeu exactement de la même manière, vous ne regroupez probablement pas ces objets de manière intelligente - ils sont probablement dispersés au hasard dans cette liste. Potentiellement, alors, chaque appel à la méthode de mise à jour pour cet objet (que cette méthode soit une fonction globale ou non, et que cet objet ait des données d'instance ou des "données globales" flottant dans une table dans laquelle vous indexez ou autre) est différent de l'appel de mise à jour dans les dernières itérations de boucle.

Cela peut exercer une pression accrue sur le système car vous devrez peut-être paginer la mémoire avec la fonction appropriée dans et hors de la mémoire et remplir plus fréquemment le cache d'instructions, ce qui entraînera une boucle plus lente. Que cela soit ou non observable à l'œil nu (ou même dans un profileur) dépend exactement de ce qui est considéré comme un «objet de jeu», du nombre d'entre eux qui existent en moyenne et de ce qui se passe dans votre application.

Les systèmes d'objets orientés composants sont une tendance populaire en ce moment, tirant parti de la philosophie selon laquelle l' agrégation est préférable à l'héritage . De tels systèmes vous permettent potentiellement de diviser la logique de «mise à jour» des composants (où «composant» est grossièrement défini comme une unité de fonctionnalité, comme la chose qui représente la partie physiquement simulée d'un objet, qui est traitée par le système physique ) sur plusieurs threads - différenciés par type de composant - si possible et souhaité, ce qui pourrait avoir un gain de performances. À tout le moins, vous pouvez organiser les composants de telle sorte que tous les composants d'un type donné se mettent à jour ensemble , en utilisant de manière optimale le cache du processeur. Un exemple d'un tel système orienté composant est discuté dans ce fil .

Ces systèmes sont souvent très découplés, ce qui est également une aubaine pour la maintenance.

La conception orientée données est une approche connexe - il s'agit de vous orienter autour des données requises des objets en priorité, afin que ces données puissent être traitées efficacement en masse (par exemple). Cela signifie généralement une organisation qui essaie de garder les données utilisées pour le même cluster ensemble et exploitées en même temps. Ce n'est pas fondamentalement incompatible avec la conception OO, et vous pouvez trouver quelques discussions sur le sujet ici à GDSE dans cette question .

En effet, une approche plus optimale de la boucle de jeu serait, au lieu de votre original

foreach(GameObject g in gameObjects) g.update();

quelque chose de plus comme

ProcessUserInput();
UpdatePhysicsForAllObjects();
UpdateScriptsForAllObjects();
UpdateRenderDataForAllObjects();
RenderEverything();

Dans un tel monde, chacun GameObjectpeut avoir un pointeur ou une référence à son propre PhysicsDataou Scriptou RenderData, pour les cas où vous pourriez avoir besoin d'interagir avec des objets sur une base individuelle, mais le réel PhysicsData, Scripts, RenderData, et ainsi de suite seraient tous la propriété de leurs sous - systèmes respectifs (simulateur physique, environnement d'hébergement de scripts, moteur de rendu) et traité en masse comme indiqué ci-dessus.

Il est très important de noter que cette approche n'est pas une solution miracle et ne produira pas toujours une amélioration des performances (bien qu'il s'agisse généralement d'une meilleure conception qu'un arbre d'héritage profond). Vous êtes particulièrement susceptible de remarquer essentiellement aucune différence de performances si vous avez très peu d'objets, ou de très nombreux objets pour lesquels vous ne pouvez pas paralléliser efficacement les mises à jour.

Malheureusement, il n'y a pas une telle boucle magique qui soit la plus optimale - chaque jeu est différent et peut nécessiter un réglage des performances de différentes manières. Il est donc très important de mesurer (profil) les choses avant d'aller aveuglément suivre les conseils d'un gars au hasard sur Internet.

Communauté
la source
5
Seigneur, c'est une excellente réponse.
Raveline
2

Le style de programmation des objets de jeu est bien pour les petits projets. Comme le projet devient vraiment énorme, vous vous retrouverez avec une hiérarchie d'héritage profonde et la base des objets de jeu deviendra très lourde!

Par exemple ... Supposons que vous ayez commencé à créer une base d'objet de jeu avec des attributs minimaux, comme la position (pour x et y), displayObject (cela fait référence à l'image s'il s'agit d'un élément de dessin), la largeur, la hauteur, etc.

Maintenant, plus tard, si nous devons ajouter une sous-classe qui aura un état d'animation, vous déplacez probablement le «code de contrôle d'animation» (par exemple currentAnimationId, nombre d'images pour cette animation, etc.) vers GameObjectBase en pensant que la plupart des les objets auront une animation.
Plus tard, si vous souhaitez ajouter un corps rigide qui est statique (n'a pas d'animation), vous devez vérifier le `` code de contrôle d'animation '' dans la fonction de mise à jour de GameObject Base (même si c'est une vérification si l'animation est là ou non. ..il importe!) Contrairement à cela, si vous suivez la structure basée sur les composants, vous aurez un `` composant de contrôle d'animation '' attaché à votre objet.Si vous en avez besoin, vous l'attacherez.Pour un corps statique, vous n'attacherez pas ce composant.Cela façon, nous pouvons éviter les contrôles inutiles.

Ainsi, la sélection du choix du style dépend de la taille du projet.La structure de GameObject n'est facile à maîtriser que pour les petits projets.J'espère que cela aide un peu.

Ayyappa
la source
@Josh Petrie - Vraiment une excellente réponse!
Ayyappa
@Josh Petrie - Vraiment une bonne réponse avec des liens utiles! (Je ne sais pas comment placer ce commentaire à côté de votre message: P)
Ayyappa
Vous pouvez normalement cliquer sur le lien "ajouter un commentaire" sous ma réponse, mais cela nécessite plus de réputation que vous n'en avez actuellement (voir gamedev.stackexchange.com/privileges/comment ). Et merci!