35 votes

Les structures de données pour le passage des messages dans un programme ?

J'essaie d'écrire un simple RPG. Jusqu'à présent, à chaque fois que j'ai essayé de commencer, c'est devenu un véritable fouillis et je ne sais pas comment organiser quoi que ce soit. Je recommence donc à zéro, en essayant de prototyper une nouvelle structure qui est essentiellement le framework MVC. Mon application commence à s'exécuter dans le contrôleur, où elle va créer la vue et le modèle. Ensuite, elle entre dans la boucle de jeu, et la première étape de la boucle de jeu consiste à recueillir les données de l'utilisateur.

L'entrée de l'utilisateur sera collectée par une partie de la vue, car elle peut varier (une vue 3D interrogera directement l'entrée de l'utilisateur, tandis qu'une vue distante la recevra peut-être par une connexion telnet, ou une vue en ligne de commande utilisera System.in). L'entrée sera traduite en messages, et chaque message sera donné au contrôleur (par un appel de méthode) qui peut alors interpréter le message pour modifier les données du modèle, ou envoyer des données sur le réseau (car j'espère avoir une option de réseau).

Cette technique de traitement des messages peut également être utilisée, dans le cas d'un jeu en réseau, pour traiter les messages du réseau. Est-ce que je garde l'esprit du MVC jusqu'ici ?

Bref, ma question est la suivante : quelle est la meilleure façon de représenter ces messages ?

Voici un cas d'utilisation, avec chaque message en italique : Disons que l'utilisateur démarre le jeu et choisit le personnage 2 . Ensuite, l'utilisateur se déplace aux coordonnées (5,2) . Puis il dit au chat public, "Salut !" . Puis il choisit de sauvegarder et quitter .

Comment la vue doit-elle envelopper ces messages dans quelque chose que le contrôleur peut comprendre ? Ou pensez-vous que je devrais avoir des méthodes de contrôleur séparées comme chooseCharacter(), moveCharacterTo(), publicChat() ? Je ne suis pas sûr qu'une mise en œuvre aussi simple fonctionnera lorsque je passerai à un jeu en réseau. Mais à l'autre extrême, je ne veux pas simplement envoyer des chaînes de caractères au contrôleur. C'est difficile parce que l'action "choisir un caractère" prend un entier, le "déplacer vers" prend deux entiers, et le chat prend une chaîne de caractères (et une portée (public private global) et dans le cas de private, un utilisateur de destination) ; il n'y a pas de véritable type de données pour tout cela.

Toute suggestion d'ordre général est également la bienvenue : est-ce que je m'inquiète au bon moment ? Suis-je sur la bonne voie pour créer une application MVC bien conçue ? Y a-t-il quelque chose que j'ai oublié ?

Gracias.

67voto

haffax Points 2837

(Disclaimer : je n'ai jamais programmé de jeux en Java, seulement en C++. Mais l'idée générale devrait être applicable en Java également. Les idées que je présente ne sont pas les miennes, mais un mélange de solutions que j'ai trouvées dans des livres ou "sur internet", voir la section des références. J'emploie tout cela moi-même et jusqu'à présent il en résulte un design propre où je sais exactement où placer les nouvelles fonctionnalités que j'ajoute).

Je crains que ce ne soit une longue réponse, elle pourrait ne pas être claire lors de la première lecture, car je ne peux pas le décrire de haut en bas très bien, donc il y aura des références en avant et en arrière, c'est dû à mon manque de compétence d'explication, pas parce que la conception est défectueuse. Rétrospectivement, je suis allé trop loin et je suis peut-être même hors sujet. Mais maintenant que j'ai écrit tout cela, je ne peux pas me résoudre à le jeter. Il suffit de demander si quelque chose n'est pas clair.

Avant de commencer à concevoir l'un des paquets et des classes, commencez par une analyse. Quelles sont les fonctionnalités que vous voulez avoir dans le jeu. Ne prévoyez pas un "peut-être que j'ajouterai ceci plus tard", car il est presque certain que les décisions de conception que vous prendrez d'emblée avant de commencer à ajouter cette fonctionnalité pour de bon, le stub que vous avez prévu pour elle sera insuffisant.

Et pour la motivation, je parle d'expérience ici, ne pensez pas que votre tâche consiste à écrire un moteur de jeu, écrivez un jeu ! Quoi que vous pensiez de ce qui serait cool d'avoir pour un projet futur, rejetez-le à moins de le mettre dans le jeu que vous écrivez en ce moment. Pas de code mort non testé, pas de problèmes de motivation dus au fait de ne pas pouvoir résoudre un problème qui n'est même pas un problème pour le projet immédiat à venir. Il n'y a pas de conception parfaite, mais il y en a une suffisamment bonne. Cela vaut la peine de garder cela à l'esprit.

Comme dit plus haut, je ne crois pas que MVC soit d'une quelconque utilité dans la conception d'un jeu. La séparation modèle/vue n'est pas un problème, et le contrôleur est assez compliqué, trop pour être simplement appelé "contrôleur". Si vous voulez avoir des sous-paquets nommés modèle, vue, contrôle, allez-y. Les éléments suivants peuvent être intégrés dans ce schéma d'emballage, bien que d'autres soient au moins aussi judicieux.

Il est difficile de trouver un point de départ dans ma solution, alors je commence simplement par le haut :

Dans le programme principal, je crée simplement l'objet Application, je l'init et je le démarre. L'objet de l'application init() va créer les serveurs de fonctionnalités (voir ci-dessous) et les initialiser. Le premier état de jeu est également créé et poussé au sommet (voir aussi ci-dessous).

Les serveurs de fonctionnalités encapsulent les fonctionnalités orthogonales du jeu. Ils peuvent être mis en œuvre indépendamment et sont faiblement couplés par des messages. Exemples de fonctionnalités : Son, représentation visuelle, détection des collisions, intelligence artificielle/décision, physique, etc. La façon dont les fonctionnalités elles-mêmes sont organisées est décrite ci-dessous.

Entrée, flux de contrôle et boucle de jeu

Les états de jeu constituent un moyen d'organiser le contrôle des entrées. J'ai généralement une seule classe qui recueille les événements d'entrée ou qui capture l'état d'entrée et l'interroge plus tard (InputServer/InputManager). Si l'on utilise l'approche basée sur les événements, les événements sont donnés à l'état de jeu actif enregistré.

Au démarrage du jeu, ce sera l'état de jeu du menu principal. Un état de jeu a init/destroy y resume/suspend fonction. Init() initialisera l'état du jeu, dans le cas du menu principal, il affichera le niveau le plus élevé du menu. Resume() donnera le contrôle à cet état, il prend maintenant l'entrée du InputServer. Suspend() effacera la vue du menu de l'écran et destroy() libérera toutes les ressources dont le menu principal a besoin.

Les états de jeu peuvent être empilés. Lorsqu'un utilisateur lance le jeu en utilisant l'option "nouveau jeu", l'état de jeu MainMenu est suspendu et l'état de jeu PlayerControlGameState est placé sur la pile et reçoit désormais les événements d'entrée. De cette façon, vous pouvez gérer les entrées en fonction de l'état de votre jeu. Avec un seul contrôleur actif à un moment donné, vous simplifiez énormément le flux de contrôle.

La collecte des données est déclenchée par la boucle de jeu. La boucle de jeu détermine essentiellement le temps de trame de la boucle en cours, met à jour les serveurs de fonctionnalités, collecte les données et met à jour l'état du jeu. Le temps de trame est soit donné à une fonction de mise à jour de chacun de ces éléments, soit fourni par un singleton Timer. Il s'agit du temps canonique utilisé pour déterminer la durée depuis le dernier appel de mise à jour.

Objets et caractéristiques du jeu

Le cœur de cette conception est l'interaction des objets et des fonctions du jeu. Comme nous l'avons vu plus haut, une caractéristique est, dans ce sens, un élément de la fonctionnalité du jeu qui peut être mis en œuvre indépendamment les uns des autres. Un objet de jeu est tout ce qui interagit avec le joueur ou tout autre objet de jeu de quelque manière que ce soit. Exemples : L'avatar du joueur lui-même est un objet de jeu. Une torche est un objet de jeu, les PNJ sont des objets de jeu tout comme les zones d'éclairage et les sources sonores ou toute combinaison de ces éléments.

Traditionnellement, les objets de jeu RPG sont la classe supérieure d'une hiérarchie de classes sophistiquée, mais cette approche est tout simplement erronée. Beaucoup d'aspects orthogonaux ne peuvent pas être mis dans une hiérarchie et même en utilisant des interfaces, vous devez avoir des classes concrètes. Un objet est un objet de jeu, un objet pouvant être ramassé est un objet de jeu, un coffre est un conteneur est un objet, mais faire en sorte qu'un coffre puisse être ramassé ou non est une décision à prendre ou non avec cette approche, car vous devez avoir une hiérarchie unique. Et cela se complique lorsque vous voulez avoir un coffre magique parlant qui ne s'ouvre que lorsqu'on répond à une énigme. Il n'existe tout simplement pas de hiérarchie unique et adaptée.

Une meilleure approche consiste à n'avoir qu'une seule classe d'objet de jeu et à placer chaque aspect orthogonal, qui est généralement exprimé dans la hiérarchie des classes, dans sa propre classe de composant/fonctionnalité. L'objet de jeu peut-il contenir d'autres éléments ? Ajoutez-lui la ContainerFeature, s'il peut parler, ajoutez-lui la TalkTargetFeature et ainsi de suite.

Dans ma conception, un GameObject n'a qu'un identifiant unique intrinsèque, un nom et une propriété de localisation, tout le reste est ajouté en tant que composant de fonctionnalité. Les composants peuvent être ajoutés à l'exécution via l'interface GameObject en appelant addComponent(), removeComponent(). Ainsi, pour le rendre visible, ajoutez un VisibleComponent, pour qu'il émette des sons, ajoutez un AudableComponent, pour qu'il soit un conteneur, ajoutez un ContainerComponent.

Le VisibleComponent est important pour votre question, car il s'agit de la classe qui assure le lien entre le modèle et la vue. Tout n'a pas besoin d'une vue au sens classique du terme. Une zone de déclenchement ne sera pas visible, une zone de son ambiant ne le sera pas non plus. Seuls les objets du jeu ayant le VisibleComponent seront visibles. La représentation visuelle est mise à jour dans la boucle principale, lorsque le VisibleFeatureServer est mis à jour. Il met alors à jour la vue en fonction des VisibleComponents qui lui sont enregistrés. Le fait qu'il interroge l'état de chacun d'entre eux ou qu'il mette simplement en file d'attente les messages reçus de ces derniers dépend de votre application et de la bibliothèque de visualisation sous-jacente.

Dans mon cas, j'utilise Ogre3D. Ici, quand un VisibleComponent est attaché à un objet de jeu, il crée un SceneNode qui est attaché au graphe de scène et au noeud de scène une Entity (représentation d'un mesh 3d). Chaque TransformMessage (voir ci-dessous) est traité immédiatement. Le VisibleFeatureServer fait ensuite en sorte qu'Ogre3d redessine la scène vers la RenderWindow (en substance, les détails sont plus compliqués, comme toujours).

Messages

Comment ces fonctions, états de jeu et objets de jeu communiquent-ils entre eux ? Par le biais de messages. Dans cette conception, un message est simplement une sous-classe de la classe Message. Chaque message concret peut avoir sa propre interface qui convient à sa tâche.

Les messages peuvent être envoyés d'un objet de jeu à d'autres objets de jeu, d'un objet de jeu à ses composants et des FeatureServers aux composants dont ils sont responsables.

Lorsqu'un FeatureComponent est créé et ajouté à un objet de jeu, il s'enregistre auprès de l'objet de jeu en appelant myGameObject.registerMessageHandler(this, MessageID) pour chaque message qu'il souhaite recevoir. Il s'enregistre également auprès de son serveur de fonctionnalités pour chaque message qu'il souhaite recevoir de celui-ci.

Si le joueur essaie de parler à un personnage qu'il a dans son focus, alors l'utilisateur déclenchera d'une manière ou d'une autre l'action parler. Par exemple : Si le personnage dans le focus est un NPC amical, alors en appuyant sur le bouton de la souris, l'interaction standard est déclenchée. L'action standard de l'objet de jeu cible est demandée en lui envoyant un message GetStandardActionMessage. L'objet de jeu cible reçoit le message et, en commençant par le premier enregistré, notifie ses composants fonctionnels qui veulent connaître le message. Le premier composant pour ce message définira alors l'action standard à celle qui se déclenchera (TalkTargetComponent définira l'action standard à Talk, qu'il recevra aussi en premier.) et marquera ensuite le message comme consommé. Le GameObject testera la consommation et verra que le message est effectivement consommé et retournera à l'appelant. Le message maintenant modifié est alors évalué et l'action résultante est invoquée.

Oui, cet exemple semble compliqué, mais c'est déjà l'un des plus compliqués. D'autres comme TransformMessage pour notifier un changement de position et d'orientation sont plus faciles à traiter. Un TransformMassage est intéressant pour de nombreux serveurs de fonctionnalités. VisualisationServer en a besoin pour mettre à jour la représentation visuelle du GameObject à l'écran. SoundServer pour mettre à jour la position du son 3d et ainsi de suite.

L'avantage d'utiliser des messages plutôt que d'invoquer des méthodes devrait être clair. Le couplage entre les composants est plus faible. Lorsqu'il invoque une méthode, l'appelant doit connaître le destinataire. En revanche, l'utilisation de messages permet un découplage total. S'il n'y a pas de récepteur, cela n'a pas d'importance. De même, la façon dont le récepteur traite le message, si tant est qu'il le fasse, ne concerne pas l'appelant. Les délégués sont peut-être un bon choix ici, mais Java n'a pas d'implémentation propre pour ceux-ci et dans le cas du jeu en réseau, vous devez utiliser une sorte de RPC, qui a une latence plutôt élevée. Or, une faible latence est cruciale pour les jeux interactifs.

Persistence et marshalling

Cela nous amène à la manière de faire passer des messages sur le réseau. En encapsulant l'interaction GameObject/Feature dans des messages, nous n'avons plus qu'à nous préoccuper de la manière de faire passer les messages sur le réseau. Dans l'idéal, il faut donner aux messages une forme universelle, les placer dans un paquet UDP et l'envoyer. Le récepteur décompose le message en une instance de la classe appropriée et le canalise vers le récepteur ou le diffuse, en fonction du message. Je ne sais pas si la sérialisation intégrée de Java est à la hauteur de la tâche. Mais même si ce n'est pas le cas, il existe de nombreuses librairies qui peuvent le faire.

Les GameObjects et les composants rendent leur état persistant disponible via des propriétés (le C++ ne dispose pas de la sérialisation intégrée). Ils ont une interface similaire à un PropertyBag en Java avec laquelle leur état peut être récupéré et restauré.

Références

  • Le vidage de cerveau : Le blog d'un développeur de jeux professionnel. Également auteurs du moteur open source Nebula, un moteur de jeu utilisé dans des jeux à succès commerciaux. La plupart du design que je présente ici est tiré de la couche d'application de Nebula.
  • Article à noter sur le blog ci-dessus, il expose la couche d'application du moteur. Un autre angle à ce que j'ai essayé de décrire ci-dessus.
  • Une longue discussion sur la façon de concevoir l'architecture d'un jeu. Principalement spécifique à Ogre, mais suffisamment général pour être utile à d'autres.
  • Un autre argument en faveur des conceptions basées sur les composants avec des références utiles en bas de page.

2voto

JeeBee Points 11882

Je ne suis pas sûr qu'un framework MVC soit approprié pour un jeu, mais je suppose que vous créez un serveur de jeu pour, par exemple, un MUD ou un simple MMPROGOOGPRG, et que la lisibilité et l'évolutivité du code sont plus importantes pour vous que les performances brutes.

Cela dépend du nombre d'utilisateurs que vous voulez prendre en charge en même temps, et des capacités de votre serveur de jeu. Vous pourriez commencer par une entrée/sortie en mode texte, puis passer à une représentation binaire ou XML au fur et à mesure que votre projet mûrit.

J'aurais certainement des actions différentes, avec une classe différente pour chaque commande possible.

Votre analyseur frontal créerait des objets UserAction (en fait des sous-classes, T extends UserAction) à partir de la couche réseau/vue->contrôleur. Cela vous permet de modifier le fonctionnement de votre réseau en cours de route sans avoir à démolir votre application principale. Vous pensez probablement déjà que vous pourriez utiliser une sérialisation personnalisée ou similaire pour les messages avec ces objets UserAction. Cet objet UserAction serait transmis à son implémentation UserActionHandler (Command) via une usine ou en vérifiant simplement un champ CommandEnum dans un commutateur. Ledit Handler effectuera alors la magie nécessaire sur le modèle, et le contrôleur remarquera les changements d'état du modèle et enverra des notifications aux autres joueurs/vues, et ainsi de suite.

2voto

Kylotan Points 14114

Proposez-moi une autre réponse à "MVC considéré comme potentiellement dangereux dans les jeux". Si votre rendu 3D est une "vue" et que votre trafic réseau est une "vue", alors ne vous retrouvez-vous pas avec des clients distants qui traitent essentiellement une vue comme un modèle ? (Le trafic réseau peut ressembler à un autre mécanisme de vue lorsque vous l'envoyez, mais à l'extrémité réceptrice, c'est votre modèle définitif sur lequel votre jeu est basé). Gardez MVC là où il doit être - séparation de la présentation visuelle de la logique.

En général, vous voulez travailler en envoyant un message au serveur et en attendant de recevoir une réponse. Que ce serveur soit sur un autre continent ou dans le même processus n'a pas d'importance si vous le gérez de la même manière.

Disons que l'utilisateur démarre le jeu et choisit le personnage 2. Ensuite, l'utilisateur se déplace aux coordonnées (5,2). Puis il dit au chat public, "salut !". Puis il choisit de sauvegarder et de quitter.

Restez simple. Les MUDs avaient l'habitude d'envoyer simplement la commande en texte brut (par exemple : "SELECT character2", "MOVE TO 5,2", "SAY Hi") et il n'y a pas de raison pour que vous ne puissiez pas le faire, si vous êtes à l'aise pour écrire l'analyseur de texte.

Une alternative plus structurée serait d'envoyer un simple objet XML, puisque je sais que vous, les gars de Java, aimez le XML ;)

<message>
    <choose-character number='2'/>
</message>

<message>
    <move-character x='5' y='2'/>
</message>

<!--- etc --->

Dans les jeux commerciaux, nous avons tendance à avoir une structure binaire qui contient un identifiant de type de message, puis une charge utile arbitraire, avec une sérialisation pour emballer et déballer ces messages à chaque extrémité. Vous n'auriez pas besoin de ce genre d'efficacité ici cependant.

0voto

Justin Niessner Points 144953

Bien que je ne sois pas totalement convaincu que MVC se prête bien à la conception de jeux, il existe quelques articles qui expliquent les bases de l'emplacement des différents éléments de la logique de jeu en utilisant une architecture MVC. Voici une référence rapide qui répond à un certain nombre de vos questions :

Architecture de jeu : Modèle-Vue-Contrôleur

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X