Comment séparer la logique du jeu des animations et de la boucle de dessin?

9

Je n'ai fait que des jeux flash auparavant, en utilisant MovieClips et autres pour séparer mes animations de ma logique de jeu. Maintenant, je commence à essayer de créer un jeu pour Android, mais la théorie de la programmation de jeu autour de la séparation de ces choses me confond toujours. Je viens d'un milieu de développement d'applications Web non liées aux jeux, je suis donc versé dans plus de modèles similaires à MVC et je suis coincé dans cet état d'esprit à l'approche de la programmation de jeux.

Je veux faire des choses comme résumer mon jeu en ayant, par exemple, une classe de plateau de jeu qui contient les données d'une grille de tuiles avec des instances d'une classe de tuiles qui contiennent chacune des propriétés. Je peux donner à ma boucle de dessin un accès à cela et lui faire dessiner le plateau de jeu en fonction des propriétés de chaque tuile sur le plateau de jeu, mais je ne comprends pas exactement où l'animation devrait aller. Pour autant que je sache, l'animation se situe en quelque sorte entre la logique de jeu abstraite (modèle) et la boucle de dessin (vue). Avec mon état d'esprit MVC, il est frustrant d'essayer de décider où l'animation est censée aller. Il aurait un peu de données associées comme un modèle, mais doit apparemment être très étroitement couplé avec la boucle de dessin afin d'avoir des choses comme une animation indépendante de l'image.

Comment puis-je sortir de cet état d'esprit et commencer à penser à des modèles qui ont plus de sens pour les jeux?

TMV
la source

Réponses:

6

L'animation peut toujours parfaitement être partagée entre la logique et le rendu. L'état abstrait des données d'animation serait l'information nécessaire à votre API graphique pour restituer l'animation.

Dans les jeux 2D par exemple, cela pourrait être une zone rectangle qui marque la zone qui affiche la partie actuelle de votre feuille de sprite qui doit être dessinée (lorsque vous avez une feuille composée de disons 30 dessins 80x80 contenant les différentes étapes de votre personnage sauter, s'asseoir, bouger, etc.). Il peut également s'agir de tout type de données dont vous n'avez pas besoin pour le rendu, mais peut-être pour la gestion des états d'animation eux-mêmes, comme le temps restant jusqu'à l'expiration de l'étape d'animation en cours ou le nom de l'animation ("marche", "debout" etc.) Tout cela peut être représenté comme vous le souhaitez. C'est la partie logique.

Dans la partie de rendu, vous le faites comme d'habitude, récupérez ce rectangle à partir de votre modèle et utilisez votre moteur de rendu pour effectuer les appels à l'API graphique.

Dans le code (en utilisant ici la syntaxe C ++):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Voilà les données. Votre moteur de rendu prendra ces données et les dessinera. Puisque les Sprites normaux et animés sont dessinés de la même manière, vous pouvez utiliser la polymorphie ici!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

J'ai trouvé un autre exemple. Disons que vous avez un RPG. Votre modèle qui représente la carte du monde, par exemple, aurait probablement besoin de stocker la position du personnage dans le monde sous forme de coordonnées de tuile sur la carte. Cependant, lorsque vous déplacez le personnage, il marche de quelques pixels à la fois jusqu'au carré suivant. Stockez-vous cette position "entre les tuiles" dans un objet d'animation? Comment mettez-vous à jour le modèle lorsque le personnage est finalement «arrivé» à la coordonnée de tuile suivante sur la carte?

La carte du monde ne connaît pas directement la position des joueurs (elle n'a pas de Vector2f ou quelque chose comme ça qui stocke directement la position des joueurs =, au lieu de cela, elle a une référence directe à l'objet joueur lui-même, qui dérive à son tour d'AnimatedSprite afin que vous puissiez le transmettre facilement au moteur de rendu et en obtenir toutes les données nécessaires.

En général cependant, votre tilemap ne devrait pas être capable de tout faire - j'aurais une classe "TileMap" qui prend en charge la gestion de toutes les tuiles, et peut-être aussi la détection de collision entre les objets que je lui remets et les tuiles sur la carte. Ensuite, j'aurais une autre classe "RPGMap", ou comme vous voudriez l'appeler, qui a à la fois une référence à votre tilemap et la référence au joueur et fait les appels Update () réels à votre joueur et à votre tilemap.

La façon dont vous souhaitez mettre à jour le modèle lorsque le joueur se déplace dépend de ce que vous voulez faire.

Votre joueur est-il autorisé à se déplacer entre les tuiles indépendamment (style Zelda)? Manipulez simplement l'entrée et déplacez le lecteur en conséquence à chaque image. Ou voulez-vous que le joueur appuie sur "droite" et que votre personnage déplace automatiquement une tuile vers la droite? Laissez votre classe RPGMap interpoler la position des joueurs jusqu'à ce qu'il arrive à destination et en attendant verrouillez toute la gestion des entrées de clé de mouvement.

Quoi qu'il en soit, si vous voulez vous faciliter la tâche, tous vos modèles auront des méthodes Update () s'ils ont réellement besoin de logique pour se mettre à jour (au lieu de simplement changer les valeurs des variables) - Vous ne donnez pas le contrôleur dans le modèle MVC de cette façon, vous déplacez simplement le code de "une étape au-dessus" (le contrôleur) vers le modèle, et tout ce que le contrôleur fait est d'appeler cette méthode Update () du modèle (le contrôleur dans notre cas serait le RPGMap). Vous pouvez toujours facilement échanger le code logique - vous pouvez simplement changer directement le code de la classe ou si vous avez besoin d'un comportement complètement différent, vous pouvez simplement dériver de votre classe de modèle et remplacer uniquement la méthode Update ().

Cette approche réduit beaucoup les appels de méthode et des choses comme ça - ce qui était l'un des principaux inconvénients du modèle MVC pur (vous finissez par appeler très souvent GetThis () GetThat ()) - cela rend le code à la fois plus long et plus un peu plus difficile à lire et aussi plus lent - même si cela peut être pris en charge par votre compilateur qui optimise beaucoup de choses comme ça.

TravisG
la source
Souhaitez-vous conserver les données d'animation dans la classe contenant la logique du jeu, la classe contenant la boucle de jeu, ou les séparer des deux? En outre, il appartient entièrement à la boucle ou à la classe contenant la boucle de comprendre comment traduire les données d'animation en dessinant réellement l'écran, non? Il ne serait souvent pas aussi simple que d'obtenir un rect qui représente une section de la feuille de sprite et de l'utiliser pour couper un dessin bitmap de la feuille de sprite.
TMV
J'ai trouvé un autre exemple. Disons que vous avez un RPG. Votre modèle qui représente la carte du monde, par exemple, aurait probablement besoin de stocker la position du personnage dans le monde sous forme de coordonnées de tuile sur la carte. Cependant, lorsque vous déplacez le personnage, il marche de quelques pixels à la fois jusqu'au carré suivant. Stockez-vous cette position "entre les tuiles" dans un objet d'animation? Comment mettez-vous à jour le modèle lorsque le personnage est finalement "arrivé" à la coordonnée de tuile suivante sur la carte?
TMV
J'ai modifié la réponse à votre question, car les commentaires ne permettent pas assez de caractères pour cela.
TravisG
Si je comprends tout correctement:
TMV
Vous pourriez avoir une instance d'une classe "Animator" dans votre vue, et elle aurait une méthode "mise à jour" publique qui est appelée chaque image par la vue. La méthode de mise à jour appelle les méthodes de «mise à jour» des instances de divers types d'objets d'animation individuels à l'intérieur. L'animateur et les animations qu'il contient ont une référence au modèle (transmis par le biais de leurs constructeurs) afin qu'ils puissent mettre à jour les données du modèle si une animation le modifiait. Ensuite, dans la boucle de dessin, vous obtenez les données des animations à l'intérieur de l'animateur d'une manière qui peut être comprise par la vue et dessinée.
TMV
2

Je peux développer cela si vous le souhaitez, mais j'ai un moteur de rendu central qui se fait dire de dessiner dans la boucle. Plutôt que

handle input

for every entity:
    update entity

for every entity:
    draw entity

J'ai un système plus comme

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

La classe de rendu contient simplement une liste de références aux composants dessinables des objets. Ceux-ci sont attribués dans les constructeurs pour plus de simplicité.

Pour votre exemple, j'aurais une classe GameBoard avec un certain nombre de tuiles. Chaque tuile connaît évidemment sa position, et je suppose une sorte d'animation. Factorisez cela dans une sorte de classe d'animation que la tuile possède et faites-lui passer une référence d'elle-même à une classe de rendu. Là, tous séparés. Lorsque vous mettez à jour Tile, il appelle Mettre à jour l'animation ... ou la met à jour elle-même. Quand Renderer.Draw()est appelé, il dessine l'animation.

L'animation indépendante de l'image ne devrait pas avoir trop à voir avec la boucle de dessin.

Le canard communiste
la source
0

J'ai moi-même appris les paradigmes récemment, donc si cette réponse est incomplète, je suis sûr que quelqu'un y ajoutera.

La méthodologie qui semble la plus logique pour la conception de jeux est de séparer la logique de la sortie d'écran.

Dans la plupart des cas, vous voudriez utiliser une approche multi-thread, si vous n'êtes pas familier avec ce sujet, c'est une question qui lui est propre, voici l'amorce du wiki . Essentiellement, vous voulez que votre logique de jeu s'exécute dans un seul thread, le verrouillage variables auxquelles elle doit accéder afin de garantir l'intégrité des données. Si votre boucle logique est incroyablement rapide (super méga pong 3D animé?), Vous pouvez essayer de fixer la fréquence d'exécution de la boucle en dormant le fil pendant de petites durées (120 hz a été suggéré dans ce forum pour les boucles de physique des jeux). Parallèlement, l'autre thread redessine l'écran (60 Hz a été suggéré dans d'autres rubriques) avec les variables mises à jour, demandant à nouveau un verrou sur les variables avant d'y accéder.

Dans ce cas, les animations ou les transitions, etc., entrent dans le fil de dessin mais vous devez signaler à travers un drapeau d'une certaine sorte (une variable d'état globale peut-être) que le fil de logique de jeu ne doit rien faire (ou faire quelque chose de différent ... configurez le nouveaux paramètres cartographiques peut-être).

Une fois que vous maîtrisez la concurrence, le reste est assez compréhensible. Si vous n'avez pas d'expérience avec la concurrence, je vous suggère fortement d'écrire quelques programmes de test simples afin que vous puissiez comprendre comment le flux se produit.

J'espère que cela t'aides :)

[edit] Sur les systèmes qui ne prennent pas en charge le multithreading, l'animation peut toujours entrer dans la boucle de dessin, mais vous voulez définir l'état de manière à signaler à la logique que quelque chose de différent se produit et ne pas continuer à traiter le niveau actuel / carte / etc ...

Stephen
la source
1
Je suis en désaccord ici. Dans la plupart des cas, vous ne voulez pas utiliser plusieurs threads, surtout s'il s'agit d'un petit jeu.
The Communist Duck
@TheCommunistDuck Assez juste, les frais généraux et la complexité du multithreading peuvent définitivement le rendre excessif, et si le jeu est petit, il devrait pouvoir être mis à jour rapidement.
Stephen