le liste d'action est une structure de données simple utile pour de nombreuses tâches différentes dans un moteur de jeu. On pourrait faire valoir que la liste d’actions devrait toujours être utilisée à la place d’une certaine forme de machine à états..
La forme la plus courante (et la plus simple) d’organisation comportementale est une machine à états finis. Généralement implémenté avec des commutateurs ou des tableaux en C ou C ++, ou une série de si
et autre
déclarations dans d’autres langues, les machines d’état sont rigides et inflexibles. La liste d'actions est un schéma d'organisation plus solide dans la mesure où elle modélise de manière claire comment les choses se passent généralement dans la réalité. Pour cette raison, la liste d'actions est plus intuitive et flexible qu'une machine à états finis.
La liste d’actions est simplement un schéma d’organisation du concept de action chronométrée. Les actions sont stockées dans un ordre premier entré premier sorti (FIFO). Cela signifie que lorsqu'une action est insérée dans une liste d'actions, la dernière action insérée au premier plan sera supprimée. La liste d'actions ne suit pas explicitement le format FIFO, mais elle reste fondamentalement la même.
Chaque boucle de jeu, la liste d'actions est mise à jour et chaque action de la liste est mise à jour dans l'ordre. Une fois qu'une action est terminée, elle est retirée de la liste..
Un action est une sorte de fonction à appeler qui fait une sorte de travail. Voici quelques types de domaines et le travail que les actions pourraient effectuer au sein de ceux-ci:
Les éléments de bas niveau tels que la recherche de chemin ou le flocage ne sont pas représentés efficacement avec une liste d'actions. Le combat et d’autres zones de jeu très spécialisées, spécifiques au jeu, sont également des choses qu’il ne faut probablement pas mettre en œuvre via une liste d’actions..
Voici un aperçu de ce qui devrait figurer dans la structure de données de la liste d'actions. Veuillez noter que des détails plus spécifiques suivront plus tard dans l'article.
class ActionList public: void Update (float dt); void PushFront (Action * action); annuler PushBack (Action * action); vide InsertBefore (Action * action); vide InsertAfter (Action * action); Action * Supprimer (Action * action); Action * Begin (vide); Action * Fin (vide); bool IsEmpty (void) const; float TimeLeft (void) const; bool IsBlocking (void) const; private: durée du flottant; float timeElapsed; float percentDone; blocage de bole; voies non signées; Action ** actions; // peut être un vecteur ou une liste chaînée;
Il est important de noter que le stockage réel de chaque action ne doit pas nécessairement être une liste chaînée chaînée - quelque chose comme le C++ std :: vector
fonctionnerait parfaitement bien. Ma propre préférence est de regrouper toutes les actions dans un allocateur et des listes de liens avec des listes liées de manière intrusive. Habituellement, les listes d'actions sont utilisées dans des domaines moins sensibles aux performances. Par conséquent, une optimisation lourde axée sur les données ne sera probablement pas nécessaire lors du développement d'une structure de données de liste d'actions..
Le point crucial de tout ce charabia réside dans les actions elles-mêmes. Chaque action doit être entièrement autonome de sorte que la liste d'actions elle-même ne connaisse rien des éléments internes de l'action. Cela fait de la liste des actions un outil extrêmement flexible. Une liste d'actions ne se souciera pas de savoir si elle exécute des actions dans l'interface utilisateur ou gère les mouvements d'un personnage modélisé en 3D..
Un bon moyen de mettre en œuvre des actions consiste à utiliser une seule interface abstraite. Quelques fonctions spécifiques de l'objet action sont exposées dans la liste d'actions. Voici un exemple d’action de base:
classe Action public: mise à jour virtuelle (float dt); OnStart virtuel (void); OnEnd virtuel (void); bool isFinished; bool isBlocking; voies non signées; le flottement s'est écoulé; durée du flottement; private: ActionList * ownerList; ;
le OnStart ()
et Fin ()
les fonctions sont intégrales ici. Ces deux fonctions doivent être exécutées chaque fois qu'une action est insérée dans une liste et à la fin de l'action, respectivement. Ces fonctions permettent aux actions d'être entièrement autonomes.
Une extension importante de la liste d’actions est la possibilité de désigner des actions comme blocage et non bloquant. La distinction est simple: une action bloquante met fin à la routine de mise à jour de la liste d'actions et aucune autre action n'est mise à jour; une action non bloquante permet de mettre à jour l'action suivante.
Une seule valeur booléenne peut être utilisée pour déterminer si une action est bloquante ou non bloquante. Voici un psuedocode démontrant une liste d’actions mettre à jour
routine:
void ActionList :: Update (float dt) int i = 0; while (i! = numActions) Action * action = actions + i; action-> mise à jour (dt); si (action-> isBlocking) se brise; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (action); ++ i;
Un bon exemple d'utilisation d'actions non bloquantes serait de permettre à certains comportements de s'exécuter en même temps. Par exemple, si nous avons une file d’actions pour courir et agiter les mains, le personnage qui exécute ces actions devrait pouvoir faire les deux à la fois. Si un ennemi fuit le personnage, il serait très maladroit de courir, puis arrêtez-vous et agitez ses mains avec frénésie, puis continuez à courir.
En fin de compte, le concept d’actions bloquantes et non bloquantes correspond intuitivement à la plupart des types de comportements simples devant être implémentés dans un jeu..
Permet de couvrir un exemple de ce à quoi ressemblerait la gestion d’une liste d’actions dans un scénario réel. Cela aidera à développer l’intuition sur l’utilisation d’une liste d’actions et sur l’utilité de ces listes..
Un ennemi dans un simple jeu 2D de haut en bas doit effectuer des patrouilles. Chaque fois que cet ennemi est à portée du joueur, il doit lancer une bombe en direction du joueur et mettre en pause sa patrouille. Il devrait y avoir un petit temps de recharge après le lancement d'une bombe où l'ennemi est complètement immobile. Si le joueur est toujours à sa portée, une autre bombe suivie d'un temps de recharge devrait être lancée. Si le joueur est hors de portée, la patrouille doit continuer exactement là où elle s'est arrêtée..
Chaque bombe doit flotter dans le monde 2D et respecter les lois de la physique basée sur les tuiles mise en œuvre dans le jeu. La bombe attend juste que son minuteur se termine, puis explose. L’explosion doit consister en une animation, un son et l’enlèvement de la boîte de collision de la bombe et de l’image-objet visuelle..
Construire une machine à états pour ce comportement sera possible et pas trop difficile, mais cela prendra du temps. Les transitions de chaque état doivent être codées à la main, et sauvegarder les états précédents pour continuer plus tard pourrait causer des maux de tête..
Heureusement, c'est un problème idéal à résoudre avec des listes d'actions. Imaginons d’abord une liste d’actions vides. Cette liste d'actions vide représente une liste d'éléments "à faire" que l'ennemi doit compléter. une liste vide indique un ennemi inactif.
Il est important de réfléchir à la manière de "compartimenter" le comportement souhaité en petites pépites. La première chose à faire serait de mettre fin aux comportements de la patrouille. Supposons que l'ennemi doit patrouiller à distance, puis à droite et à la même distance, et répéter.
Voici ce que le patrouille à gauche l'action pourrait ressembler à:
class PatrolLeft: Action publique Mise à jour virtuelle (float dt) // Déplace l'ennemi gauche ennemi-> position.MoveLeft (); // Minuterie jusqu'à la fin de l'action + = dt; if (écoulé> = durée) isFinished = true; virtuel OnStart (void); // ne fait rien virtuel OnEnd (void) // Insère une nouvelle action dans la liste list-> Insert (new PatrolRight ()); bool isFinished = false; bool isBlocking = true; Ennemi * ennemi; durée de flottement = 10; // secondes jusqu'à la fin du flottant = 0; // secondes;
PatrolRight
semblera presque identique, avec les directions retournées. Lorsque l'une de ces actions est placée dans la liste d'actions de l'ennemi, celui-ci effectuera une patrouille infiniment à gauche et à droite..
Voici un court diagramme montrant le déroulement d'une liste d'actions, avec quatre instantanés de l'état de la liste d'actions en cours pour la patrouille:
Le prochain ajout devrait être la détection du moment où le joueur est à proximité. Cela pourrait être fait avec une action non bloquante qui ne se termine jamais. Cette action vérifiera si le joueur est proche de l'ennemi et, le cas échéant, créera une nouvelle action appelée ThrowBomb
directement devant lui dans la liste d'actions. Il placera également un Retard
action juste après la ThrowBomb
action.
L'action non bloquante restera là et sera mise à jour, mais la liste d'actions continuera à mettre à jour toutes les actions ultérieures. Actions de blocage (telles que Patrouille
) sera mis à jour et la liste des actions cessera de mettre à jour les actions ultérieures. Rappelez-vous, cette action est uniquement là pour voir si le joueur est à sa portée et ne quittera jamais la liste des actions.!
Voici à quoi cette action pourrait ressembler:
class DetectPlayer: action publique Mise à jour virtuelle (float dt) // Lance une bombe et met en pause si le joueur est à proximité si (PlayerNearby ()) this-> InsertInFrontOfMe (new ThrowBomb ()); // Pause pendant 2 secondes this-> InsertInFrontOfMe (new Pause (2.0)); virtuel OnStart (void); // ne fait rien virtuel OnEnd (void) // ne fait rien bool isFinished = false; bool isBlocking = false; ;
le ThrowBomb
action sera une action de blocage qui lance une bombe vers le joueur. Il devrait probablement être suivi d'un ThrowBombAnimation
, qui bloque et joue une animation ennemie, mais je l’ai laissée de côté pour des raisons de concision. La pause derrière la bombe aura lieu l'animation, et attendez un peu avant de terminer.
Jetons un coup d'oeil à un diagramme de ce à quoi cette liste d'actions pourrait ressembler lors de la mise à jour:
La bombe elle-même devrait être un objet de jeu entièrement nouveau et comporter environ trois actions dans sa propre liste d'actions. La première action est un blocage Pause
action. Suite à cela devrait être une action pour jouer une animation pour une explosion. Le sprite de la bombe lui-même, ainsi que la zone de collision, devront être retirés. Enfin, un effet sonore d'explosion doit être joué.
En tout, il devrait y avoir environ six à dix types d’actions qui doivent être utilisés ensemble pour créer le comportement requis. La meilleure partie de ces actions est qu’elles peuvent être réutilisé dans le comportement de n'importe quel type d'ennemi, pas seulement celui démontré ici.
Chaque liste d’actions dans sa forme actuelle a un seul voie dans lequel des actions peuvent exister. Une piste est une séquence d'actions à mettre à jour. Une voie peut être bloquée ou non bloquée.
La mise en œuvre parfaite des voies fait usage de bitmasks. (Pour plus de détails sur ce qu'est un masque de bits, veuillez vous reporter à la rubrique Procédure rapide de masque de bits pour programmeurs et à la page Wikipedia pour une introduction rapide.) Il est possible de construire 32 voies différentes à l'aide d'un seul entier 32 bits..
Une action doit avoir un entier pour représenter toutes les voies sur lesquelles elle réside. Cela permet à 32 voies différentes de représenter différentes catégories d’actions. Chaque voie peut être bloquée ou non pendant la routine de mise à jour de la liste elle-même..
Voici un exemple rapide de la Mettre à jour
méthode d'une liste d'actions avec des lignes de masque de bits:
void ActionList :: Update (float dt) int i = 0; voies non signées = 0; while (i! = numActions) Action * action = actions + i; si (voies et action-> voies) continuez; action-> mise à jour (dt); if (action-> isBlocking) lanes | = action-> lanes; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (action); ++ i;
Cela offre un niveau de flexibilité accru, car une liste d'actions peut maintenant exécuter 32 types d'actions différents, où auparavant, 32 listes d'actions seraient nécessaires pour obtenir la même chose..
Une action qui ne fait que retarder toutes les actions pendant un laps de temps déterminé est une chose très utile. L'idée est de retarder toutes les actions ultérieures jusqu'à la fin du délai imparti..
La mise en œuvre de l'action delay est très simple:
class Delay: public Action public: void Mise à jour (float dt) elapsed + = dt; if (écoulé> durée) isFinished = true; ;
Un type d'action utile est celui qui bloque jusqu'à ce qu'il s'agisse de la première action de la liste. Ceci est utile lorsque plusieurs actions non bloquantes différentes sont en cours d'exécution, mais vous ne savez pas dans quel ordre elles vont se terminer. synchroniser action garantit qu'aucune action non bloquante antérieure n'est en cours d'exécution avant de continuer.
L'implémentation de l'action de synchronisation est aussi simple qu'on pourrait l'imaginer:
class Sync: public Action public: void Mise à jour (float dt) if (ownerList-> Begin () == this) isFinished = true; ;
La liste d'actions décrite jusqu'ici est un outil assez puissant. Cependant, il y a quelques ajouts qui peuvent être faits pour vraiment laisser la liste d'actions briller. Celles-ci sont un peu avancées et je ne recommande pas de les mettre en œuvre sauf si vous pouvez le faire sans trop de peine.
La possibilité d'envoyer un message directement à une action ou de permettre à une action d'envoyer des messages à d'autres actions et objets de jeu est extrêmement utile. Cela permet aux actions d'être extrêmement flexibles. Souvent, une liste d'actions de cette qualité peut servir de "langage de script du pauvre".
Certains messages très utiles à publier à partir d’une action peuvent inclure: démarré; terminé en pause; a repris; terminé; annulé; bloqué. Le blocage est assez intéressant - chaque fois qu’une nouvelle action est placée dans une liste, elle peut bloquer d’autres actions. Ces autres actions voudront le savoir et informeront éventuellement les autres abonnés de l’événement..
Les détails d'implémentation de la messagerie sont spécifiques à la langue et ne sont pas triviaux. En tant que tel, les détails de la mise en œuvre ne seront pas abordés ici, car la messagerie n'est pas l'objet de cet article..
Il existe différentes manières de représenter les hiérarchies d’actions. Une solution consiste à autoriser une liste d'actions à être une action dans une autre liste d'actions. Cela permet à la construction de listes d'actions de regrouper de grands groupes d'actions sous un identifiant unique. Cela améliore la convivialité et rend plus facile la liste des actions à développer et à déboguer.
Une autre méthode consiste à avoir des actions dont le seul but est de générer d’autres actions juste avant d’être dans la liste d’actions correspondantes. Je préfère moi-même cette méthode à la précédente, bien que cela puisse être un peu plus difficile à mettre en œuvre.
Le concept de liste d'actions et sa mise en œuvre ont été discutés en détail afin de fournir une alternative aux machines à états rigides et ad-hoc. La liste d'actions fournit un moyen simple et flexible de développer rapidement un large éventail de comportements dynamiques. La liste d'actions est une structure de données idéale pour la programmation de jeux en général.