Comment puis-je éviter les classes de joueurs géants?

46

Il y a presque toujours une classe de joueur dans une partie. Le joueur peut généralement faire beaucoup dans le jeu, ce qui signifie pour moi que cette classe finit par être énorme avec une tonne de variables pour prendre en charge chaque fonctionnalité que le joueur peut faire. Chaque morceau est assez petit en soi, mais combiné, je me retrouve avec des milliers de lignes de code et il devient difficile de trouver ce dont vous avez besoin et effrayant de faire des changements. Avec quelque chose qui est fondamentalement un contrôle général pour le jeu entier, comment éviter ce problème?

utilisateur441521
la source
26
Plusieurs fichiers ou un fichier, le code doit aller quelque part. Les jeux sont complexes. Pour trouver ce dont vous avez besoin, écrivez de bons noms de méthodes et commentaires descriptifs. N'ayez pas peur de faire des changements - testez simplement. Et sauvegardez votre travail :)
Chris McFarland
7
Je comprends que cela doit aller quelque part, mais la conception du code est importante en termes de flexibilité et de maintenance. Avoir une classe ou un groupe de codes de milliers de lignes ne me semble pas non plus.
user441521
17
@ChrisMcFarland ne suggère pas de sauvegarder, suggère de version XD code.
GameDeveloper
1
@ChrisMcFarland Je suis d'accord avec GameDeveloper. Avoir un contrôle de version comme Git, svn, TFS, ... rend le développement beaucoup plus facile, car il est plus facile d'annuler de gros changements et de récupérer facilement des éléments tels que la suppression accidentelle de votre projet, une défaillance matérielle ou la corruption de fichiers.
Nzall
3
@TylerH: Je suis fortement en désaccord. Les sauvegardes ne permettent pas de fusionner plusieurs modifications exploratoires, ni ne lient autant de métadonnées utiles à des ensembles de modifications, ni ne permettent des flux de travail sains multi-développeurs. Vous pouvez utiliser le contrôle de version comme un très puissant système de sauvegarde à un point dans le temps, mais il manque beaucoup du potentiel de ces outils.
Phoshi

Réponses:

67

Vous utiliseriez généralement un système de composant d'entité (un système de composant d'entité est une architecture basée sur des composants). Cela facilite également la création d’autres entités et permet également aux ennemis / PNJ d’avoir les mêmes composants que le joueur.

Cette approche va exactement dans le sens opposé à une approche orientée objet. Tout dans le jeu est une entité. L'entité est juste une affaire sans aucune mécanique de jeu intégrée. Il contient une liste de composants et un moyen de les manipuler.

Par exemple, le joueur a un composant de position, un composant d’animation et un composant d’entrée. Lorsque vous appuyez sur la touche espace, vous voulez que le lecteur saute.

Vous pouvez y parvenir en donnant à l'entité du joueur un composant de saut qui, lorsqu'il est appelé, fait passer le composant d'animation à l'animation de saut et donne au joueur une vélocité y positive dans le composant de position. Dans le composant d’entrée, vous écoutez la touche espace et vous appelez le composant de saut. (Ceci est juste un exemple, vous devriez avoir un composant de contrôleur pour le mouvement).

Cela aide à diviser le code en modules plus petits et réutilisables et peut aboutir à un projet plus organisé.

Bálint
la source
Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
MichaelHouse
8
Bien que je comprenne les commentaires émouvants qui doivent être déplacés, ne déplacez pas ceux qui remettent en question l'exactitude de la réponse. Cela devrait être évident, non?
bug-a-lot
20

Les jeux ne sont pas uniques en cela; les classes divines sont un anti-modèle partout.

Une solution courante consiste à décomposer la grande classe dans un arbre composé de classes plus petites. Si le joueur a un inventaire, n'en faites pas partie class Player. Au lieu de cela, créez un fichier class Inventory. C'est un membre pour class Player, mais en interne class Inventorypeut encapsuler beaucoup de code.

Autre exemple: un personnage peut avoir des relations avec les PNJ, vous pouvez donc avoir un objet class Relationréférençant à la fois l' Playerobjet et l' NPCobjet, mais n'appartenant à aucun des deux.

MSalters
la source
Oui, je cherchais simplement des idées sur la façon de procéder. L'état d'esprit était parce qu'il y a beaucoup de fonctionnalités de petits morceaux, donc lors du codage, il n'est pas naturel, pour moi en tout cas, de décomposer ces petites fonctionnalités. Cependant, il devient évident que toutes ces fonctionnalités commencent à rendre la classe de joueurs énorme.
user441521
1
Les gens disent généralement que quelque chose est une classe ou un objet divin, quand il contient et gère toutes les autres classes / objets du jeu.
Bálint
11

1) Player: Architecture à base de machine à états et de composants.

Composants habituels pour Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Ce sont toutes des classes comme class HealthSystem.

Je ne recommande pas d’utiliser cette Update()option (dans les cas habituels, cela n’a aucun sens d’avoir une mise à jour du système de santé sauf si vous en avez besoin pour chaque action, chaque fois que cela se produit, cela se produit rarement. pour perdre de la santé de temps en temps - ici, je suggère d'utiliser des coroutines. Une autre régénère constamment la santé ou le pouvoir courant, il suffit de prendre l'état actuel de la santé ou du pouvoir et d'appeler la coroutine pour atteindre ce niveau le moment venu. il a été endommagé ou il a recommencé à courir et ainsi de suite… OK, c'était un peu décalé mais j'espère que cela a été utile) .

États: LootState, RunState, WalkState, AttackState, IDLEState.

Chaque État hérite de interface IState. IStatea dans notre cas a 4 méthodes juste pour un exemple.Loot() Run() Walk() Attack()

De plus, nous class InputControllervérifions chaque entrée de l'utilisateur.

Passons maintenant à l’exemple réel: InputControllernous vérifions si le joueur appuie sur l’un des boutons WASD or arrowspuis s’il appuie également sur le bouton Shift. S'il appuie seulement WASDnous appeler _currentPlayerState.Walk();quand ce happends et nous devons currentPlayerStateêtre égal WalkStatealors à WalkState.Walk() nous tous les composants nécessaires à cet état - dans ce cas MovementSystem, donc nous faisons le déménagement du joueur public void Walk() { _playerMovementSystem.Walk(); }- vous voyez ce que nous avons ici? Nous avons une deuxième couche de comportement, ce qui est très bon pour la maintenance du code et le débogage.

Passons maintenant au second cas: et si on avait WASD+ Shiftpressé? Mais notre état précédent était WalkState. Dans ce cas, Run()sera appelé dans InputController(ne pas mélanger cela, Run()est appelé parce que nous avons WASD+ Shiftcheck in InputControllerpas à cause de la WalkState). Quand nous appelons _currentPlayerState.Run();à WalkState- nous savons que nous devons commutateur _currentPlayerStateà RunStateet nous le faisons dans Run()de WalkStateet appelons à nouveau à l' intérieur de cette méthode , mais maintenant avec un état différent parce que nous ne voulons pas perdre ce cadre l' action. Et maintenant, bien sûr, nous appelons _playerMovementSystem.Run();.

Mais que faire LootStatequand un joueur ne peut pas marcher ou courir avant d'avoir relâché le bouton? Eh bien, dans ce cas, lorsque nous avons commencé à piller, lorsque, par exemple, un bouton a Eété enfoncé, nous appelons et _currentPlayerState.Loot();nous LootStateappelons maintenant. Là, nous appelons par exemple la méthode de collecte pour obtenir s’il ya quelque chose à piller à portée de main. Et nous appelons coroutine où nous avons une animation ou nous commençons et vérifions également si le joueur tient toujours le bouton, sinon la coroutine casse, si oui nous lui donnons du butin à la fin de la coroutine. Mais que se passe-t-il si le joueur appuie WASD? - _currentPlayerState.Walk();est appelé, mais voici la belle chose à propos de la machine d'état, dansLootState.Walk()nous avons une méthode vide qui ne fait rien ou comme je le ferais comme caractéristique - les joueurs disent: "Hé mec, je n'ai pas encore pillé ça, tu peux attendre?". Quand il finit de piller, nous changeons pour IDLEState.

En outre, vous pouvez créer un autre script appelé class BaseState : IStatedont le comportement de toutes ces méthodes par défaut est implémenté, mais virtualque vous pouvez donc les overrideinsérer dans un class LootState : BaseStatetype de classe.


Le système à base de composants est génial, la seule chose qui me dérange à ce sujet sont les instances, beaucoup d'entre elles. Et cela prend plus de mémoire et de travail pour garbage collector. Par exemple, si vous avez 1000 instances d'ennemi. Chacun d'entre eux ayant 4 composants. 4000 objets au lieu de 1000. Mo Ce n'est pas si grave (je n'ai pas encore fait de tests de performance) si on considère tous les composants du gameobject de l'unité.


2) Architecture basée sur l'héritage. Vous remarquerez que nous ne pouvons pas nous débarrasser complètement des composants. En réalité, c'est impossible si nous voulons avoir un code propre et fonctionnel. De même, si nous voulons utiliser des modèles de conception qu'il est fortement recommandé d'utiliser dans les cas appropriés (ne les abusez pas aussi, cela s'appelle une ingénierie excessive).

Imaginons que nous ayons une classe de joueurs qui possède toutes les propriétés nécessaires pour se retrouver dans un jeu. Il a la santé, le mana ou l'énergie, peut se déplacer, courir et utiliser ses capacités, dispose d'un inventaire, peut fabriquer des objets, piller des objets, voire même construire des barricades ou des tourelles.

D' abord tout ce que je vais dire que l' inventaire, Crafting, Mouvement, bâtiment devrait être composante parce qu'il est responsable non joueur d'avoir des méthodes telles que AddItemToInventoryArray()- si joueur peut avoir une méthode comme PutItemToInventory()cela appeler la méthode décrite précédemment (2 couches - nous pouvons ajouter certaines conditions en fonction des différentes couches).

Un autre exemple avec la construction. Le joueur peut appeler quelque chose comme OpenBuildingWindow(), mais Buildings’occupe de tout le reste, et lorsque l’utilisateur décide de construire un bâtiment spécifique, il transmet toutes les informations nécessaires au joueur Build(BuildingInfo someBuildingInfo)et ce dernier commence à le construire avec toutes les animations nécessaires.

Principes SOLID - OOP. S - responsabilité unique: c'est ce que nous avons vu dans les exemples précédents. Ouais ok, mais où est l'héritage?

Ici: la santé et les autres caractéristiques du joueur doivent-elles être gérées par une autre entité? Je crois que non. Il ne peut y avoir un joueur sans santé, s'il en existe un, nous n'héritons pas. Par exemple, nous avons IDamagable, LivingEntity, IGameActor, GameActor. IDamagablebien sûr a TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

Donc ici, je ne pouvais pas réellement séparer les composants de l'héritage, mais nous pouvons les mélanger comme vous le voyez. Nous pouvons également créer des classes de base pour Building system, par exemple, si nous en avons différents types et que nous ne voulons pas écrire plus de code que nécessaire. En effet, nous pouvons également avoir différents types de bâtiments et il n’existe aucun moyen de le faire par composant!

OrganicBuilding : Building, TechBuilding : Building. Vous n'avez pas besoin de créer 2 composants et d'écrire du code deux fois pour les opérations courantes ou les propriétés du bâtiment. Et puis ajoutez-les différemment, vous pouvez utiliser le pouvoir de l'héritage et plus tard du polymorphisme et de l'incapsulation.


Je suggère d'utiliser quelque chose entre les deux. Et n'abusez pas des composants.


Je recommande fortement de lire ce livre sur les modèles de programmation de jeux - il est gratuit sur WEB.

Lune candidate _Max_
la source
Je vais creuser plus tard ce soir, mais pour info, je n'utilise pas l'unité, je vais donc devoir en ajuster quelques-uns, ce qui est correct.
user441521
Oh, je pensais que c'était un tag Unity, mon mauvais. MonoBehavior est la seule chose. Il s'agit simplement d'une classe de base pour chaque instance de la scène dans l'éditeur Unity. Quant à Physics.OverlapSphere (), c’est une méthode qui crée un collisionneur de sphères au cours de l’image et vérifie ce qu’elle touche. Les Coroutines sont comme de fausses mises à jour, leurs appels peuvent être réduits à des montants plus faibles que les fps sur les lecteurs PC - bon pour la performance. Start () - juste une méthode appelée une fois lorsque Instance est créée. Tout le reste devrait s'appliquer partout ailleurs. La prochaine partie je n'utiliserai rien avec Unity. Sry. J'espère que cela a clarifié quelque chose.
Candid Moon _Max_ 22/02/2017
J'ai déjà utilisé Unity, alors je comprends l'idée. J'utilise Lua, qui contient également des coroutines, donc les choses devraient bien se traduire.
user441521
Cette réponse semble un peu trop spécifique à Unity compte tenu du manque de balise Unity. Si vous le rendiez plus générique et que le contenu de l'unité devenait plus un exemple, ce serait une bien meilleure réponse.
Pharap
@CandidMoon Ouais, c'est mieux.
Pharap
4

Il n’ya pas de solution miracle à ce problème, mais il existe différentes approches, qui tournent presque toutes autour du principe de «séparation des préoccupations». D'autres réponses ont déjà abordé l'approche populaire basée sur les composants, mais il existe d'autres approches qui peuvent être utilisées à la place ou avec la solution basée sur les composants. Je vais discuter de l'approche entité-contrôleur car c'est l'une de mes solutions préférées à ce problème.

Premièrement, l'idée même d'une Playerclasse est trompeuse en premier lieu. Beaucoup de gens ont tendance à penser que les personnages joueurs, les personnages NPC et les monstres / ennemis appartiennent à des classes différentes, alors qu'en réalité, tous ont beaucoup en commun: tous ont des stocks, etc.

Cette façon de penser conduit à une approche dans laquelle les personnages joueurs, les personnages non joueurs et les monstres / ennemis sont tous traités comme des " Entitys" plutôt que d'être traités différemment. Naturellement cependant, ils doivent se comporter différemment - le personnage du joueur doit être contrôlé via une entrée et les NPCs ont besoin de ai.

La solution à cela est d'avoir des Controllerclasses qui sont utilisées pour contrôler Entitys. Ce faisant, toute la logique lourde aboutit dans le contrôleur et toutes les données et les points communs sont stockés dans l'entité.

De plus, en sous-classant Controllerdans InputControlleret AIController, il permet au joueur de contrôler efficacement tout ce qui se trouve Entitydans la pièce. Cette approche est également utile pour le mode multijoueur en disposant d’une classe RemoteControllerou d’ une NetworkControllerclasse opérant via des commandes provenant d’un flux réseau.

Cela peut entraîner une grande partie de la logique, Controllersi vous ne faites pas attention. Le moyen d'éviter cela est d'avoir des Controllers qui sont composés d'autres Controllers ou de faire en sorte que la Controllerfonctionnalité dépende de diverses propriétés du Controller. Par exemple, le AIControlleraurait un DecisionTreeattaché à celui-ci, et le PlayerCharacterControllerpourrait être composé de divers autres Controllers tels qu'un MovementController, une JumpController(contenant une machine à états avec les états OnGround, Ascending et Descending), un InventoryUIController. Un avantage supplémentaire est que de nouvelles fonctionnalités Controllerpeuvent être ajoutées au fur et à mesure que de nouvelles fonctionnalités sont ajoutées: si un jeu démarre sans système d'inventaire et si une autre est ajoutée, un contrôleur peut être ajouté plus tard.

Pharap
la source
J'aime l'idée, mais il semble que tout le code ait été transféré à la classe de contrôleur, ce qui me laisse le même problème.
user441521
@ user441521 Je viens juste de réaliser qu'il y avait un paragraphe supplémentaire que j'allais ajouter mais je l'ai perdu lorsque mon navigateur est tombé en panne. Je vais l'ajouter maintenant. Fondamentalement, vous pouvez avoir différents contrôleurs qui peuvent les composer en contrôleurs agrégés afin que chaque contrôleur gère des choses différentes. Par exemple, AggregateController.Controllers = {JumpController (keybinds), MoveController (keybinds), InventoryUIController (keybinds, uisystem)}
Pharap