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 boutons WASD or arrows
puis s’il appuie également sur le bouton Shift
. S'il appuie seulement WASD
nous appeler _currentPlayerState.Walk();
quand ce happends et nous devons currentPlayerState
être égal WalkState
alors à 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
+ Shift
pressé? 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
+ Shift
check in InputController
pas à cause de la WalkState
). Quand nous appelons _currentPlayerState.Run();
à WalkState
- nous savons que nous devons commutateur _currentPlayerState
à RunState
et nous le faisons dans Run()
de WalkState
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 a E
été enfoncé, nous appelons et _currentPlayerState.Loot();
nous LootState
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 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 : IState
dont le comportement de toutes ces méthodes par défaut est implémenté, mais virtual
que vous pouvez donc les override
insérer dans un class 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 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 Building
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 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
. IDamagable
bien 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.