Séparer le moteur de jeu du code de jeu dans des jeux similaires, avec versionnage

15

J'ai un jeu fini, que je veux décliner dans d'autres versions. Ce seraient des jeux similaires, avec plus ou moins le même type de design, mais pas toujours, fondamentalement, les choses pourraient changer, parfois peu, parfois grandes.

Je voudrais que le code de base soit versionné séparément du jeu, de sorte que si je corrige un bug trouvé dans le jeu A, le correctif sera présent dans le jeu B.

J'essaie de trouver la meilleure façon de gérer cela. Mes premières idées sont les suivantes:

  • Créez un enginemodule / dossier / autre, qui contient tout ce qui peut être généralisé et est 100% indépendant du reste du jeu. Cela comprendrait du code, mais aussi des actifs génériques partagés entre les jeux.
  • Mettez ce moteur dans son propre gitréférentiel, qui sera inclus dans les jeux en tant quegit submodule

La partie avec laquelle je me bats est de savoir comment gérer le reste du code. Disons que vous avez votre scène de menu, ce code est spécifique au jeu, mais la plupart a tendance à être générique et pourrait être réutilisé dans d'autres jeux. Je ne peux pas le mettre dans le engine, mais le recoder pour chaque jeu serait inefficace.

Peut-être que l'utilisation d'une sorte de variation des branches git pourrait être efficace pour gérer cela, mais je ne pense pas que ce soit la meilleure façon de procéder.

Quelqu'un a-t-il des idées, de l'expérience à partager ou quelque chose à ce sujet?

Malharhak
la source
Dans quelle langue est votre moteur? Certaines langues ont des gestionnaires de packages dédiés qui pourraient avoir plus de sens que d'utiliser des sous-modules git. Par exemple, NodeJS a npm (qui peut cibler les dépôts Git comme sources).
Dan Pantry
Est-ce que vous vous demandez comment gérer au mieux le code générique ou comment gérer le code "semi-générique" ou comment concevoir le code, comment concevoir le code ou quoi?
Dunk
Cela peut varier dans chaque environnement de langage de programmation, mais vous pouvez considérer non seulement le logiciel de version de contrôle, mais aussi savoir commencer par diviser le moteur de jeu du code de jeu (comme les packages, les dossiers et l'API), et plus tard. , appliquez la version de contrôle.
umlcat
Comment avoir un historique propre d'un dossier dans une branche: Refactorisez votre moteur afin que les repo (futurs) séparés soient dans des dossiers séparés, c'est votre dernier commit. Créez ensuite une nouvelle branche, supprimez tout en dehors de ce dossier et validez. Accédez ensuite au premier commit du dépôt et fusionnez-le avec votre nouvelle branche. Vous avez maintenant une branche avec uniquement ce dossier: retirez-la dans d'autres projets et / ou fusionnez-la avec votre projet existant. Cela m'a beaucoup aidé à séparer les moteurs en branches, si votre code est déjà séparé. Je n'ai pas besoin de modules git.
Barry Staes

Réponses:

13

Créez un module moteur / dossier / autre, qui contient tout ce qui peut être généralisé et est 100% indépendant du reste du jeu. Cela comprendrait du code, mais aussi des actifs génériques partagés entre les jeux.

Mettez ce moteur dans son propre référentiel git, qui sera inclus dans les jeux en tant que sous-module git

C'est exactement ce que je fais et cela fonctionne très bien. J'ai un cadre d'application et une bibliothèque de rendu, et chacun d'eux est traité comme des sous-modules de mes projets. Je trouve que SourceTree est utile en ce qui concerne les sous-modules, car il les gère bien et ne vous laissera rien oublier, par exemple si vous avez mis à jour le sous-module du moteur dans le projet A, il vous avertira de retirer les modifications dans le projet B.

Avec l'expérience vient la connaissance de ce que le code devrait être dans le moteur par rapport à ce qui devrait être par projet. Je suggère que si vous êtes même légèrement incertain, vous devez le conserver dans chaque projet pour l'instant. Au fil du temps, vous verrez parmi vos différents projets ce qui reste le même, puis vous pourrez progressivement en tenir compte dans votre code moteur. En d'autres termes: dupliquez du code jusqu'à ce que vous soyez presque sûr à 100% qu'il ne change pas discrètement par projet, puis généralisez-le.

Remarque sur le contrôle de code source et les fichiers binaires

N'oubliez pas que si vous vous attendez à ce que vos ressources binaires changent souvent, vous ne voudrez peut-être pas les mettre en contrôle de code source comme git. Je dis juste ... qu'il y a de meilleures solutions pour les binaires. La chose la plus simple que vous puissiez faire pour l'instant pour aider à garder votre référentiel "moteur-source" propre et performant est d'avoir un référentiel "moteur-binaires" séparé qui ne contient que des binaires, que vous incluez également comme sous-module dans votre projet. De cette façon, vous atténuez les dommages causés aux performances de votre référentiel "moteur-source", qui change tout le temps et sur lesquels vous avez donc besoin d'itérations rapides: commit, push, pull, etc. Les systèmes de gestion du contrôle de source comme git fonctionnent sur des deltas de texte , et dès que vous introduisez des binaires, vous introduisez des deltas massifs du point de vue du texte - ce qui vous coûte finalement du temps de développement.Annexe GitLab . Google est votre ami.

Ingénieur
la source
Ils ne changent pas vraiment souvent, mais cela m'intéresse cependant. Je ne connais rien au versioning binaire. Quelles solutions existe-t-il?
Malharhak
@Malharhak Modifié pour répondre à votre commentaire.
Ingénieur
@Malharak Voici quelques informations intéressantes sur ce sujet.
Ingénieur
1
+1 pour garder les choses dans le projet aussi longtemps que possible. Le code commun accorde une complexité plus élevée. Il doit être évité jusqu'à ce qu'il soit absolument nécessaire.
Gusdor
1
@Malharhak Non, d'autant plus que votre objectif est uniquement de conserver des "copies" jusqu'à ce que vous notiez que ledit code est immuable et peut être considéré comme courant. Gusdor a réitéré cela - soyez averti - on peut facilement perdre des tas de temps en factorisant les choses trop tôt, puis en essayant de trouver des moyens de garder ce code assez général pour rester commun, mais suffisamment adaptable pour s'adapter à divers projets ... vous vous retrouvez avec beaucoup de paramètres et de commutateurs et cela se transforme en un désordre laid qui n'est toujours pas ce dont vous avez besoin parce que vous finissez par le changer par nouveau projet de toute façon . Ne tenez pas compte trop tôt . Avoir de la patience.
Ingénieur
6

À un moment donné, un moteur DOIT se spécialiser et connaître des choses sur le jeu. Je vais partir sur une tangente ici.

Prenez des ressources dans un RTS. Un jeu peut avoir Creditset Crystalun autre MetaletPotatoes

Vous devez utiliser correctement les concepts OO et opter pour max. réutilisation de code. Il est clair qu'un concept Resourceexiste ici.

Nous décidons donc que les ressources sont les suivantes:

  1. Un crochet dans la boucle principale pour s'incrémenter / décrémenter
  2. Un moyen d'obtenir le montant actuel (retourne un int)
  3. Une façon de soustraire / ajouter arbitrairement (joueurs transférant des ressources, achats ....)

Notez que cette notion d'un Resourcepourrait représenter des éliminations ou des points dans un jeu! Ce n'est pas très puissant.

Réfléchissons maintenant à un jeu. Nous pouvons en quelque sorte avoir de la monnaie en traitant des sous et en ajoutant un point décimal à la sortie. Ce que nous ne pouvons pas faire, ce sont des ressources "instantanées". Comme dire "génération de réseau électrique"

Disons que vous ajoutez une InstantResourceclasse avec des méthodes similaires. Vous êtes maintenant (en train de) polluer votre moteur avec des ressources.


Le problème

Reprenons l'exemple RTS. Supposons que le joueur en fasse don Crystalà un autre joueur. Vous voulez faire quelque chose comme:

if(transfer.target == engine.getPlayerId()) {
    engine.hud.addIncoming("You got "+transfer.quantity+" of "+
        engine.resourceDictionary.getNameOf(transfer.resourceId)+
        " from "+engine.getPlayer(transfer.source).name);
}
engine.getPlayer(transfer.target).getResourceById(transfer.resourceId).add(transfer.quantity)
engine.getPlayer(transfer.source).getResourceById(transfer.resourceId).add(-transfer.quantity)

Cependant, c'est vraiment assez compliqué. C'est un usage général, mais désordonné. Déjà, bien qu'il impose un resourceDictionaryqui signifie que maintenant vos ressources doivent avoir des noms! ET c'est par joueur, donc vous ne pouvez plus avoir de ressources d'équipe.

C'est "trop" d'abstraction (ce n'est pas un exemple brillant, je l'admets). Au lieu de cela, vous devriez atteindre un point où vous acceptez que votre jeu a des joueurs et du cristal, alors vous pouvez simplement avoir (par exemple)

engine.getPlayer(transfer.target).crystal().receiveDonation(transfer)
engine.getPlayer(transfer.source).crystal().sendDonation(transfer)

Avec une classe Playeret une classe CurrentPlayeroù l CurrentPlayer' crystalobjet montrera automatiquement les trucs sur le HUD pour le transfert / envoi de dons.

Cela pollue le moteur avec du cristal, le don de cristal, les messages sur le HUD pour les joueurs actuels et tout ça. Il est à la fois plus rapide et plus facile à lire / écrire / maintenir (ce qui est plus important, car il n'est pas beaucoup plus rapide)


Remarques finales

Le dossier de ressources n'est pas brillant. J'espère que vous pouvez toujours voir le point. Si quoi que ce soit, j'ai démontré que "les ressources n'appartiennent pas au moteur", car ce dont un jeu spécifique a besoin et ce qui est applicable à toutes les notions de ressources sont des choses TRÈS différentes. Ce que vous trouverez généralement, ce sont 3 (ou 4) "couches"

  1. Le "Core" - c'est la définition classique du moteur, c'est un graphique de scène avec des hooks d'événements, il traite des shaders et des paquets réseau et une notion abstraite de joueurs
  2. Le "GameCore" - C'est assez générique pour le type de jeu mais pas pour tous les jeux - par exemple les ressources en RTS ou les munitions en FPS. La logique du jeu commence à s'infiltrer ici. C'est là que serait notre notion antérieure de ressources. Nous avons ajouté ces éléments qui ont du sens pour la plupart des ressources RTS.
  3. "GameLogic" TRÈS spécifique au jeu en cours de réalisation. Vous trouverez des variables avec des noms comme creatureou shipou squad. En utilisant l' héritage que vous obtiendrez des cours qui couvrent les 3 couches (par exemple Crystal est un Resource qui est un GameLoopEventListener exemple)
  4. Les "atouts" sont inutiles à tout autre jeu. Prenez par exemple les scripts AI combinés dans Half Life 2, ils ne seront pas utilisés dans un RTS avec le même moteur.

Créer un nouveau jeu à partir d'un ancien moteur

C'est TRÈS courant. La phase 1 consiste à extraire les couches 3 et 4 (et 2 si le jeu est d'un type TOTALEMENT différent) Supposons que nous fabriquons un RTS à partir d'un ancien RTS. Nous avons encore des ressources, juste pas de cristal et d'autres choses - donc les classes de base dans les couches 2 et 1 ont toujours du sens, tout ce cristal référencé en 3 et 4 peut être jeté. C'est ce que nous faisons. Nous pouvons cependant le vérifier comme référence pour ce que nous voulons faire.


Pollution dans la couche 1

Cela peut arriver. L'abstraction et la performance sont des ennemis. UE4, par exemple, fournit de nombreux cas de composition optimisés (donc si vous voulez X et Y, quelqu'un a écrit du code qui fait X et Y ensemble très rapidement - il sait qu'il fait les deux) et, par conséquent, est VRAIMENT assez grand. Ce n'est pas mauvais mais cela prend du temps. La couche 1 décidera des choses comme "comment vous transmettez les données aux shaders" et comment vous animez les choses. Le faire de la meilleure façon pour votre projet est TOUJOURS bon. Essayez simplement de planifier pour l'avenir, la réutilisation du code est votre ami, héritez là où cela a du sens.


Classification des couches

ENFIN (je le promets), n'ayez pas trop peur des couches. Moteur est un terme archaïque de l'ancien temps des pipelines à fonction fixe où les moteurs fonctionnaient à peu près de la même manière graphiquement (et, par conséquent, avaient beaucoup en commun), le pipeline programmable a renversé la situation et en tant que telle, la "couche 1" est devenue polluée. avec tous les effets que les développeurs voulaient atteindre. L'IA était la caractéristique distinctive (en raison de la myriade d'approches) des moteurs, maintenant c'est l'IA et les graphiques.

Votre code ne doit pas être déposé dans ces couches. Même le célèbre moteur Unreal a BEAUCOUP de versions différentes chacune spécifique à un jeu différent. Il y a peu de fichiers (autres que des structures de données similaires peut-être) qui seraient restés inchangés. C'est bon! Si vous voulez créer un nouveau jeu à partir d'un autre, cela prendra plus de 30 minutes. La clé est de planifier, de savoir quels bits copier et coller et quoi laisser derrière.

Alec Teal
la source
1

Ma suggestion personnelle sur la façon de gérer le contenu qui est un mélange de générique et spécifique est de le rendre dynamique. Je vais prendre votre écran de menu comme exemple. Si j'ai mal compris ce que vous demandiez, faites-moi savoir ce que vous vouliez savoir et j'adapterai ma réponse.

Il y a 3 choses qui sont (presque) toujours présentes sur une scène de menu: l'arrière-plan, le logo du jeu et le menu lui-même. Ces choses sont généralement différentes selon le jeu. Ce que vous pouvez faire pour ce contenu est de créer un MenuScreenGenerator dans votre moteur, qui prend 3 paramètres d'objet: BackGround, Logo et Menu. La structure de base de ces 3 parties fait également partie de votre moteur, mais votre moteur ne dit pas réellement comment ces pièces sont générées, mais quels paramètres vous devez leur donner.

Ensuite, dans votre code de jeu réel, vous créez des objets pour un BackGround, un logo et un menu, et vous le transmettez à votre MenuScreenGenerator. Encore une fois, votre jeu lui-même ne gère pas la façon dont le menu est généré, c'est pour le moteur. Votre jeu n'a qu'à dire au moteur à quoi il devrait ressembler et où il devrait être.

Essentiellement, votre moteur doit être une API que le jeu indique quoi afficher. Si cela est fait correctement, votre moteur devrait faire le gros du travail et votre jeu lui-même devrait seulement dire au moteur quels actifs utiliser, quelles actions prendre et à quoi ressemble le monde.

Nzall
la source