Mettre en œuvre un comportement dans un jeu d'aventure simple

11

Je me suis amusé récemment en programmant un simple jeu d'aventure basé sur du texte, et je suis coincé sur ce qui semble être un problème de conception très simple.

Pour donner un bref aperçu: le jeu se décompose en Roomobjets. Chacun Rooma une liste d' Entityobjets qui se trouvent dans cette pièce. Chacun Entitya un état d'événement, qui est une simple carte string-> booléenne, et une liste d'actions, qui est une map-> fonction map.

L'entrée utilisateur prend la forme [action] [entity]. Le Roomutilise le nom de l'entité pour renvoyer l' Entityobjet approprié , qui utilise ensuite le nom de l'action pour trouver la fonction correcte et l'exécute.

Pour générer la description de la pièce, chaque Roomobjet affiche sa propre chaîne de description, puis ajoute les chaînes de description de chaque Entity. La Entitydescription peut changer en fonction de son état ("La porte est ouverte", "La porte est fermée", "La porte est verrouillée", etc.).

Voici le problème: en utilisant cette méthode, le nombre de fonctions de description et d'action que je dois implémenter rapidement devient incontrôlable. Ma salle de départ à elle seule a environ 20 fonctions entre 5 entités.

Je peux combiner toutes les actions en une seule fonction et if-else / passer à travers, mais cela reste deux fonctions par entité. Je peux également créer des Entitysous-classes spécifiques pour des objets communs / génériques comme les portes et les clés, mais cela ne me permet que de me rendre compte jusqu'à présent.

EDIT 1: Comme demandé, des exemples de pseudo-code de ces fonctions d'action.

string outsideDungeonBushesSearch(currentRoom, thisEntity, player)
    if thisEntity["is_searched"] then
        return "There was nothing more in the bushes."
    else
        thisEntity["is_searched"] := true
        currentRoom.setEntity("dungeonDoorKey")
        return "You found a key in the bushes."
    end if

string dungeonDoorKeyUse(currentRoom, thisEntity, player)
    if getEntity("outsideDungeonDoor")["is_locked"] then
        getEntity("outsideDungeonDoor")["is_locked"] := false
        return "You unlocked the door."
    else
        return "The door is already unlocked."
    end if

Les fonctions de description agissent à peu près de la même manière, vérifiant l'état et renvoyant la chaîne appropriée.

EDIT 2: Révision du libellé de ma question. Supposons qu'il puisse y avoir un nombre important d'objets en jeu qui ne partagent pas un comportement commun (réponses basées sur l'état à des actions spécifiques) avec d'autres objets. Existe-t-il un moyen de définir ces comportements uniques d'une manière plus propre et plus maintenable que d'écrire une fonction personnalisée pour chaque action spécifique à l'entité?

Eric
la source
1
Je pense que vous devez expliquer ce que font ces "fonctions d'action" et peut-être publier du code, car je ne suis pas sûr de ce dont vous parlez.
jhocking
Ajout du code.
Eric

Réponses:

5

Plutôt que de créer une fonction distincte pour chaque combinaison de noms et de verbes, vous devez configurer une architecture où il existe une interface commune que tous les objets du jeu implémentent.

Une approche du haut de ma tête serait de définir un objet Entité que tous les objets spécifiques de votre jeu étendent. Chaque entité aura une table (quelle que soit la structure de données utilisée par votre langage pour les tableaux associatifs) qui associe différentes actions à différents résultats. Les actions dans le tableau seront probablement des chaînes (par exemple, "open") tandis que le résultat associé pourrait même être une fonction privée dans l'objet si votre langage prend en charge les fonctions de première classe.

De même, l'état de l'objet est stocké dans différents champs de l'objet. Ainsi, par exemple, vous pouvez avoir un tableau de choses dans un Bush, puis la fonction associée à "recherche" agira sur ce tableau, soit en renvoyant l'objet trouvé ou la chaîne "Il n'y avait plus rien dans les buissons."

Pendant ce temps, l'une des méthodes publiques est quelque chose comme Entity.actOn (String action) Ensuite, dans cette méthode, comparez l'action transmise avec le tableau des actions pour cet objet; si cette action est dans le tableau, retournez le résultat.

Maintenant, toutes les différentes fonctions nécessaires pour chaque objet seront contenues dans l'objet, ce qui facilitera la répétition de cet objet dans d'autres pièces (par exemple, instancier l'objet Porte dans chaque pièce qui a une porte)

Enfin, définissez toutes les salles en XML ou JSON ou autre afin que vous puissiez avoir beaucoup de salles uniques sans avoir besoin d'écrire du code séparé pour chaque chambre. Chargez ce fichier de données au démarrage du jeu et analysez les données pour instancier les objets qui peuplent votre jeu. Quelque chose comme:

<rooms>
  <room id="room1">
    <description>Outside the dungeon you see some bushes and a heavy door over the entrance.</description>
    <entities>
      <bush>
        <description>The bushes are thick and leafy.</description>
        <contains>
          <key />
        </contains>
      </bush>
      <door connection="room2" isLocked="true">
        <description>It's an oak door with stout iron clasps.</description>
      </door>
    </entities>
  </room>

  <room id="room2">
    etc.

ADDITION: aha, je viens de lire la réponse de FxIII et ce bit vers la fin m'a sauté aux yeux:

(no things like <item triggerFlamesOnPicking="true"> that you will use just once)

Bien que je ne sois pas d'accord sur le fait qu'un déclencheur de flamme déclenché est quelque chose qui ne se produirait qu'une seule fois (je pouvais voir ce piège être réutilisé pour de nombreux objets différents), je pense que je comprends enfin ce que vous vouliez dire sur les entités qui réagissent uniquement aux entrées de l'utilisateur. J'aborderais probablement des choses comme faire une porte dans votre donjon avoir un piège à boules de feu en construisant toutes mes entités avec une architecture de composants (expliqué en détail ailleurs).

De cette façon, chaque entité Door est construite comme un ensemble de composants, et je peux mélanger et assortir de manière flexible des composants entre différentes entités. Par exemple, la majorité des portes auraient des configurations comme

<entity name="door">
  <description>It's an oak door with stout iron clasps.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room2" />
  </components>
</entity>

mais la seule porte avec un piège à boules de feu serait

<entity name="door">
  <description>There are strange runes etched into the wood.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room7" />
    <fireballTrap />
  </components>
</entity>

puis le seul code unique que j'aurais à écrire pour cette porte est le composant FireballTrap. Il utiliserait les mêmes composants Lock et Portal que toutes les autres portes, et si je décidais plus tard d'utiliser le FireballTrap sur un coffre au trésor ou quelque chose d'aussi simple que d'ajouter le composant FireballTrap à ce coffre.

Que vous définissiez ou non tous les composants dans le code compilé ou dans un langage de script séparé n'est pas une grande distinction dans mon esprit (de toute façon vous allez écrire le code quelque part ), mais l'important est que vous pouvez réduire considérablement le quantité de code unique que vous devez écrire. Heck, si vous n'êtes pas préoccupé par la flexibilité pour les concepteurs / moddeurs de niveau (vous écrivez ce jeu par vous-même après tout), vous pouvez même faire hériter toutes les entités de Entity et ajouter des composants dans le constructeur plutôt qu'un fichier de configuration ou un script ou peu importe:

Door extends Entity {
  public Door() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
  }
}

TrappedDoor extends Entity {
  public TrappedDoor() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
    addComponent(new FireballTrap());
  }
}
jhocking
la source
1
Cela fonctionne pour les éléments courants et reproductibles. Mais qu'en est-il des entités qui répondent uniquement aux entrées des utilisateurs? La création d'une sous-classe de Entityjuste pour un seul objet regroupe le code mais ne réduit pas la quantité de code que je dois écrire. Ou est-ce un écueil inévitable à cet égard?
Eric
1
J'ai abordé les exemples que vous avez donnés. Je ne peux pas lire dans tes pensées; quels objets et entrées voulez-vous avoir?
jhocking
Modification de mon message pour mieux expliquer mes intentions. Si je comprends bien votre exemple, il semble que chaque balise d'entité correspond à une sous-classe de Entityet les attributs définissent son état initial. Je suppose que les balises enfants de l'entité agissent comme des paramètres pour l'action à laquelle cette balise est associée, non?
Eric
oui, c'est l'idée.
jhocking
J'aurais dû penser que les composants auraient fait partie de la solution. Merci pour l'aide.
Eric
1

Le problème dimensionnel que vous abordez est tout à fait normal et presque inévitable. Vous voulez trouver un moyen d'exprimer vos entités qui soit à la fois coïncident et flexible .

Un "conteneur" (le buisson dans la réponse jhocking) est une manière coïncidente mais vous voyez que ce n'est pas assez flexible .

Je ne vous suggère pas d'essayer de trouver une interface générique puis d'utiliser des fichiers de configuration pour spécifier les comportements, car vous aurez toujours la sensation désagréable d'être entre un rocher (entités standard et ennuyeuses, faciles à décrire) et un endroit dur ( entités fantastiques uniques mais trop longues à mettre en œuvre).

Ma suggestion est d' utiliser un langage interprété pour coder les comportements.

Pensez à l'exemple de la brousse: c'est un conteneur, mais notre brousse doit contenir des éléments spécifiques; l'objet conteneur peut avoir:

  • une méthode pour le conteur d'ajouter un élément,
  • une méthode permettant au moteur d'afficher l'élément qu'il contient,
  • une méthode pour le joueur de choisir un élément.

L'un de ces articles a une corde qui déclenche un engin qui déclenche à son tour une flamme qui brûle le buisson ... (vous voyez, je peux lire dans vos pensées, donc je connais les choses que vous aimez).

Vous pouvez utiliser un script pour décrire ce buisson au lieu d'un fichier de configuration mettant le code supplémentaire pertinent dans un crochet que vous exécutez à partir de votre programme principal chaque fois que quelqu'un choisit un élément dans un conteneur.

Maintenant, vous avez beaucoup de choix d'architecture: vous pouvez définir des outils comportementaux en tant que classes de base en utilisant votre langage de code ou le langage de script (des choses comme des conteneurs, des portes, etc.). Le but de ces choses est de vous permettre de décrire les entités en agrégeant facilement des comportements simples et en les configurant à l'aide de liaisons sur un langage de script .

Toutes les entités doivent être accessibles au script: vous pouvez associer un identifiant à chaque entité et les placer dans un conteneur qui est exporté dans le prolongement du script du langage de script.

L'utilisation de stratégies de script vous permet de garder votre configuration simple (rien de tel <item triggerFlamesOnPicking="true">que vous n'utiliserez qu'une seule fois) tout en vous permettant d'exprimer des beaviours étranges (les plus amusants) en ajoutant une ligne de code

En quelques mots: scripts sous forme de fichier de configuration pouvant exécuter du code.

FxIII
la source