Comment éviter le codage en dur dans les moteurs de jeu

22

Ma question n'est pas une question de codage; il s'applique à l'ensemble de la conception du moteur de jeu en général.

Comment évitez-vous le codage en dur?

Cette question est beaucoup plus profonde qu'il n'y paraît. Dites, si vous voulez exécuter un jeu qui charge les fichiers nécessaires au fonctionnement, comment éviter de dire quelque chose comme load specificfile.waddans le code du moteur? De plus, lorsque le fichier est chargé, comment éviter de le dire load aspecificmap in specificfile.wad?

Cette question s'applique à la quasi-totalité de la conception du moteur, et le moins possible du moteur doit être codé en dur. Quelle est la meilleure façon d'y parvenir?

Marcus Cramer
la source

Réponses:

42

Codage basé sur les données

Chaque chose que vous mentionnez est quelque chose qui peut être spécifié dans les données. Pourquoi chargez-vous aspecificmap? Parce que la configuration du jeu indique qu'il s'agit du premier niveau lorsqu'un joueur démarre une nouvelle partie, ou parce que c'est le nom du point de sauvegarde actuel dans le fichier de sauvegarde du joueur qu'il vient de charger, etc.

Comment trouvez-vous aspecificmap? Parce qu'il se trouve dans un fichier de données qui répertorie les identifiants de carte et leurs ressources sur disque.

Il ne doit y avoir qu'un ensemble particulièrement restreint de ressources "de base" qui sont légitimement dures ou impossibles à éviter le codage en dur. Avec un peu de travail, cela peut être limité à un seul nom d'actif par défaut codé en dur comme main.wadou similaire. Ce fichier peut potentiellement être modifié au moment de l'exécution en passant un argument de ligne de commande au jeu, alias game.exe -wad mymain.wad.

L'écriture de code piloté par les données repose sur quelques autres principes. Par exemple, on peut éviter que des systèmes ou des modules demandent une ressource particulière et inversent plutôt ces dépendances. Autrement dit, ne faites pas de DebugDrawerchargement debug.fontdans son code d'initialisation; au lieu de cela, DebugDrawerprenez un descripteur de ressource dans son code d'initialisation. Cette poignée peut être chargée à partir du fichier de configuration de jeu principal.

Comme exemples concrets de notre base de code, nous avons un objet "données globales" qui est chargé à partir de la base de données des ressources (qui est lui-même par défaut le ./resourcesdossier mais peut être surchargé avec un argument de ligne de commande). L'ID de la base de données des ressources de ces données globales est le seul nom de ressource codé en dur nécessaire dans la base de code (nous en avons d'autres parce que parfois les programmeurs deviennent paresseux, mais nous finissons généralement par les corriger / supprimer éventuellement). Cet objet de données global regorge de composants dont le seul but est de fournir des données de configuration. L'un des composants est le composant UI Global Data qui contient des descripteurs de ressources pour toutes les principales ressources de l'interface utilisateur (polices, fichiers Flash, icônes, données de localisation, etc.) parmi un certain nombre d'autres éléments de configuration. Lorsqu'un développeur d'interface utilisateur décide de renommer l'actif d'interface utilisateur principal de /ui/mainmenu.swfà/ui/lobby.swfils mettent simplement à jour cette référence de données globale; aucun code moteur n'a besoin de changer du tout.

Nous utilisons ces données globales pour tout. Tous les personnages jouables, tous les niveaux, l'interface utilisateur, l'audio, les ressources principales, la configuration du réseau, tout. (enfin, pas tout , mais ces autres choses sont des bugs à corriger.)

Cette approche présente de nombreux autres avantages. D'une part, il intègre le regroupement et le regroupement des ressources à l'ensemble du processus. Les chemins de codage en dur dans le moteur ont également tendance à signifier que ces mêmes chemins doivent être codés en dur dans les scripts ou les outils qui emballent les actifs du jeu, et ces chemins peuvent alors se désynchroniser. En s'appuyant plutôt sur un seul actif principal et des chaînes de référence à partir de là, nous pouvons créer un ensemble d'actifs avec une seule commande comme bundle.exe -root config.data -out main.wadet savoir qu'il comprendra tous les actifs dont nous avons besoin. De plus, étant donné que le bundler ne ferait que suivre les références de ressources, nous savons qu'il n'inclura que les actifs dont nous avons besoin et ignorera toutes les peluches résiduelles qui s'accumulent inévitablement pendant la durée de vie d'un projet (en plus, nous pouvons générer automatiquement des listes de ces ressources). peluches pour l'élagage).

Un cas de coin délicat de tout cela est dans les scripts. Conceptuellement, il est facile de rendre le moteur piloté par les données, mais j'ai vu tellement de projets (passe-temps pour AAA) où les scripts sont considérés comme des données et sont donc "autorisés" à utiliser les chemins de ressources sans discrimination. Ne fais pas ça. Si un fichier Lua a besoin d'une ressource et qu'il appelle simplement une fonction comme celle-ci, textures.lua("/path/to/texture.png")le pipeline d'actifs aura beaucoup de mal à savoir que le script nécessite /path/to/texture.pngde fonctionner correctement et pourrait considérer cette texture comme inutilisée et inutile. Les scripts doivent être traités comme tout autre code: toutes les données dont elles ont besoin, y compris les ressources ou les tables, doivent être spécifiées dans une entrée de configuration que le moteur et le pipeline de ressources peuvent inspecter pour les dépendances. Les données qui indiquent "charger le script foo.lua" devraient plutôt indiquer "foo.luaet lui donner ces paramètres "où les paramètres incluent toutes les ressources nécessaires. Si un script engendre aléatoirement des ennemis par exemple, passez la liste des ennemis possibles dans le script à partir de ce fichier de configuration. Le moteur peut alors pré-charger les ennemis avec le niveau ( car il connaît la liste complète des apparitions possibles) et le pipeline de ressources sait regrouper tous les ennemis avec le jeu (car ils sont définitivement référencés par les données de configuration). Si les scripts génèrent des chaînes de noms de chemin et appellent simplement une loadfonction, alors ni l' un ni l'autre le moteur ou le pipeline de ressources n'ont aucun moyen de savoir précisément quels actifs le script peut essayer de charger.

Sean Middleditch
la source
Bonne réponse, très pratique, et explique également les pièges et les erreurs que les gens commettent lors de la mise en œuvre! +1
quand
+1. Ajoutera que suivre le modèle de pointage vers des ressources qui contiennent des données de configuration est également très utile si vous souhaitez activer le modding. Il est tellement plus difficile et plus risqué de modifier des jeux qui vous obligent à modifier les fichiers de données d'origine plutôt que de créer les vôtres et de les pointer. Encore mieux si vous pouvez pointer plusieurs fichiers avec un ordre de priorité défini.
Jeutnarg
12

De la même manière, vous évitez le codage en dur dans les fonctions générales.

Vous passez des paramètres et vous conservez vos informations dans des fichiers de configuration.

Dans cette situation, il n'y a absolument aucune différence en génie logiciel entre l'écriture d'un moteur et l'écriture d'une classe.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Ensuite, votre code client lit un fichier de configuration "maître" ( celui-ci est codé en dur ou passé en argument de ligne de commande) qui contient les informations qui indiquent où se trouvent les fichiers d'actifs et quelle carte ils contiennent.

De là, tout est piloté par le fichier de configuration "maître".

Vaillancourt
la source
1
Oui, cela plus une sorte de mécanisme pour apporter une logique personnalisée. Peut-être en incorporant un langage comme C #, python, etc. afin d'étendre les fonctionnalités principales du moteur par des fonctionnalités définies par l'utilisateur
qCring
3

J'aime les autres réponses, donc je vais être un peu contraire. ;)

Vous ne pouvez pas éviter de coder les connaissances sur vos données dans votre moteur. D'où que viennent les informations, le moteur doit savoir les chercher. Cependant, vous pouvez éviter d'encoder les informations elles-mêmes dans votre moteur.

Une approche «pure» basée sur les données vous obligerait à démarrer l'exécutable avec les paramètres de ligne de commande nécessaires pour qu'il charge la configuration initiale, mais le moteur devra être codé pour savoir comment interpréter ces informations. Par exemple, si vos fichiers de configuration sont JSON, vous devez coder en dur les variables que vous recherchez, par exemple le moteur devra savoir rechercher"intro_movies" et "level_list"et ainsi de suite.

Cependant, un moteur "bien construit" peut fonctionner pour de nombreux jeux différents simplement en échangeant les données de configuration et les données auxquelles il fait référence.

Le mantra n'est donc pas tant pour éviter un codage dur que pour s'assurer que vous pouvez apporter des modifications avec le moins d'effort possible.

Pour contraster avec l'approche des fichiers de données (que je soutiens sans réserve), il se peut que vous soyez d'accord pour compiler les données dans votre moteur. Si le «coût» de cette opération est plus faible, il n'y a pas de réel préjudice; si vous êtes le seul à y travailler, vous pouvez différer la gestion des fichiers pour une date ultérieure et ne pas vous visser nécessairement. Mes premiers projets de jeu avaient de grandes tables de données codées en dur dans le jeu lui-même, par exemple une liste d'armes et leurs données assorties:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Vous mettez donc ces données dans un endroit facile à référencer et il est facile de les modifier si nécessaire. L'idéal serait de mettre ces trucs dans un fichier de configuration, mais ensuite vous devez faire de l'analyse et de la traduction et tout ce jazz, plus le raccordement de références inter-structures peut devenir une douleur supplémentaire que vous ne voulez vraiment pas traiter avec.

dash-tom-bang
la source
Ce n'est pas terriblement difficile d'analyser json. Le seul «coût» impliqué est l' apprentissage. (Plus précisément, apprendre à utiliser le module ou la bibliothèque appropriée. Go a un bon support json, par exemple.)
Wildcard
Ce n'est pas très difficile, mais cela nécessite de le faire au-delà de l'apprentissage. Par exemple, je sais comment analyser techniquement JSON, j'ai écrit des analyseurs pour de nombreux autres formats de fichiers, mais je devrais soit trouver et installer une solution tierce (et comprendre les dépendances et comment la construire), soit rouler la mienne. Prend plus de temps que de ne pas le faire.
dash-tom-bang
4
Tout prend plus de temps que de ne pas le faire. Mais les outils dont vous avez besoin ont déjà été écrits. Tout comme vous n'avez pas à concevoir un compilateur pour écrire un jeu ou à jouer avec le code machine, mais vous devez apprendre un langage pour la plate-forme avec laquelle vous travaillez. Alors, apprenez également à utiliser un analyseur json.
Wildcard
Je ne sais pas quel est votre argument. Dans cette réponse, je préconise YAGNI; si vous n'avez pas besoin de passer / perdre du temps à faire quelque chose qui ne vous aidera pas, alors ne le faites pas. Si vous voulez y consacrer du temps, tant mieux. Peut-être que vous devrez passer du temps plus tard, peut-être pas, mais le faire à l'avance ne fait que vous distraire de la tâche de créer le jeu. Le développement de jeux est trivial; chaque tâche qui entre dans la création d'un jeu est simple. C'est juste que la plupart des jeux ont un million de tâches simples et un développeur responsable choisit ceux qui atteignent ce but le plus rapidement.
dash-tom-bang
2
En fait, j'ai voté contre votre réponse; aucun véritable argument en tant que tel. Je voulais juste noter que JSON n'est pas difficile à analyser. En relisant, je suppose que je répondais principalement à l'extrait de code "mais ensuite vous devez faire l'analyse syntaxique et la traduction et tout ce jazz." Mais je suis d'accord que pour les jeux de projets personnels et autres, YAGNI. :)
Wildcard