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?
architecture
utilisateur441521
la source
la source
Réponses:
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é.
la source
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 fichierclass Inventory
. C'est un membre pourclass Player
, mais en interneclass Inventory
peut encapsuler beaucoup de code.Autre exemple: un personnage peut avoir des relations avec les PNJ, vous pouvez donc avoir un objet
class Relation
référençant à la fois l'Player
objet et l'NPC
objet, mais n'appartenant à aucun des deux.la source
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
.IState
a dans notre cas a 4 méthodes juste pour un exemple.Loot() Run() Walk() Attack()
De plus, nous
class InputController
vérifions chaque entrée de l'utilisateur.Passons maintenant à l’exemple réel:
InputController
nous vérifions si le joueur appuie sur l’un des boutonsWASD or arrows
puis s’il appuie également sur le boutonShift
. S'il appuie seulementWASD
nous appeler_currentPlayerState.Walk();
quand ce happends et nous devonscurrentPlayerState
être égalWalkState
alors àWalkState.Walk()
nous tous les composants nécessaires à cet état - dans ce casMovementSystem
, donc nous faisons le déménagement du joueurpublic 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
+Shift
pressé? Mais notre état précédent étaitWalkState
. Dans ce cas,Run()
sera appelé dansInputController
(ne pas mélanger cela,Run()
est appelé parce que nous avonsWASD
+Shift
check inInputController
pas à cause de laWalkState
). Quand nous appelons_currentPlayerState.Run();
àWalkState
- nous savons que nous devons commutateur_currentPlayerState
àRunState
et nous le faisons dansRun()
deWalkState
et 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
LootState
quand 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 aE
été enfoncé, nous appelons et_currentPlayerState.Loot();
nousLootState
appelons 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 appuieWASD
? -_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 pourIDLEState
.En outre, vous pouvez créer un autre script appelé
class BaseState : IState
dont le comportement de toutes ces méthodes par défaut est implémenté, maisvirtual
que vous pouvez donc lesoverride
insérer dans unclass LootState : BaseState
type 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 commePutItemToInventory()
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()
, maisBuilding
s’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 joueurBuild(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
.IDamagable
bien sûr aTakeDamage()
.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.
la source
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
Player
classe 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 "
Entity
s" 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
Controller
classes qui sont utilisées pour contrôlerEntity
s. 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
Controller
dansInputController
etAIController
, il permet au joueur de contrôler efficacement tout ce qui se trouveEntity
dans la pièce. Cette approche est également utile pour le mode multijoueur en disposant d’une classeRemoteController
ou d’ uneNetworkController
classe opérant via des commandes provenant d’un flux réseau.Cela peut entraîner une grande partie de la logique,
Controller
si vous ne faites pas attention. Le moyen d'éviter cela est d'avoir desController
s qui sont composés d'autresController
s ou de faire en sorte que laController
fonctionnalité dépende de diverses propriétés duController
. Par exemple, leAIController
aurait unDecisionTree
attaché à celui-ci, et lePlayerCharacterController
pourrait être composé de divers autresController
s tels qu'unMovementController
, uneJumpController
(contenant une machine à états avec les états OnGround, Ascending et Descending), unInventoryUIController
. Un avantage supplémentaire est que de nouvelles fonctionnalitésController
peuvent ê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.la source