Avertissement:
À mon avis, un article sur l'architecture logicielle ne devrait pas et ne peut pas être idéal. Toute solution décrite peut ne pas couvrir le niveau nécessaire pour un programmeur, et pour un autre programmeur, cela compliquera trop inutilement l'architecture. Mais il doit apporter une solution aux tâches qu'il s'est fixées. Et cette expérience, ainsi que tout le bagage de connaissances d'un programmeur qui apprend, organise des informations, affine les nouveaux arrivants et se critique ainsi que les autres - cette expérience se transforme en d'excellents produits logiciels. L'article bascule entre l'art et la partie technique. Il s'agit d'une petite expérience et j'espère qu'elle sera intéressante.- Écoutez, j'ai trouvé une excellente idée de jeu! - le concepteur de jeu Vasya était échevelé et ses yeux étaient rouges. J'ai encore bu du café et de l'holivar sur Habré pour tuer le temps avant le stand-up. Il me regarda dans l'expectative jusqu'à ce que j'aie fini d'écrire dans les commentaires à l'homme ce qu'il avait tort. Vasya savait que tant que la justice ne prévaudrait pas et que la vérité ne serait pas protégée, il était inutile de poursuivre la conversation avec moi. J'ai ajouté la dernière phrase et je l'ai regardé.
- En un mot - les magiciens avec du mana peuvent lancer des sorts, et les guerriers peuvent se battre au corps à corps et dépenser de l'endurance. Les magiciens et les guerriers peuvent se déplacer. Oui, il sera toujours possible de voler les vaches, mais nous le ferons déjà dans la prochaine version, en bref. Allez-vous montrer le prototype après le stand-up, d'accord?
Il s'est enfui dans son entreprise de conception de jeux et j'ai ouvert l'IDE.
En fait, le sujet «composition versus héritage», «problème banane-singe», «problème losange (héritage multiple)» sont des questions courantes lors des entretiens sous différents formats et pour une bonne raison. Une mauvaise utilisation de l'héritage peut compliquer l'architecture et les programmeurs inexpérimentés ne savent pas comment y faire face et, par conséquent, commencent à critiquer la POO dans son ensemble et commencent à écrire du code procédural. Par conséquent, les programmeurs expérimentés (ou ceux qui ont lu des choses intelligentes sur Internet) considèrent qu'il est de leur devoir de poser des questions à ce sujet lors d'une interview sous diverses formes. La réponse universelle est «la composition est meilleure que l'héritage et aucune nuance de gris ne doit être appliquée». Ceux qui viennent de lire chacune de ces réponses seront 100% satisfaits.
Mais, comme je l'ai dit au début de l'article, chaque architecture conviendra à votre projet et si l'héritage est suffisant pour votre projet et tout ce qui est nécessaire pour résoudre le problème est de créer Monkey avec Banana - créez-le. Notre programmeur était dans une situation similaire. Cela n'a aucun sens de refuser l'héritage simplement parce que le FPS se moquera de vous.
class Character { x = 0; y = 0; moveTo (x, y) { this.x = x; this.y = y; } } class Mage extends Character { mana = 100; castSpell () { this.mana--; } } class Warrior extends Character { stamina = 100; meleeHit () { this.stamina--; } }
Le stand-up, comme toujours, a traîné. J'ai basculé sur une chaise et accroché au téléphone pendant que June Petya tentait de convaincre le testeur que l'impossibilité d'un contrôle rapide via le bouton droit de la souris n'est pas un bug, car il n'y avait aucune possibilité de ce type nulle part, ce qui signifie que vous devez abandonner la tâche au département de pré-production. Le testeur a fait valoir que, puisque pour les utilisateurs, le contrôle via le bouton droit semble être obligatoire, alors c'est un bug, pas une fonctionnalité. En fait, en tant que seul joueur de notre équipe à jouer à notre jeu sur des serveurs de combat, il voulait ajouter cette fonctionnalité dès que possible, mais il savait que si vous la déposiez dans le département de pré-production, la machine bureaucratique la laisserait sortir au plus tôt en 4. mois, et après l'avoir publié en tant que bogue - vous pouvez l'obtenir déjà dans la prochaine version. Le chef de projet, comme toujours, était en retard, et les gars maudissaient si férocement qu'ils étaient déjà passés à des tapis et, probablement, les choses seraient bientôt arrivées à une bagarre si le directeur du studio n'avait pas rencontré la malédiction et n'avait pas emmené les deux dans son bureau. Probablement à nouveau pour 300 dollars d'amende.
Quand j'ai quitté la salle de rallye, un concepteur de jeu s'est précipité vers moi et a dit avec bonheur que tout le monde aimait le prototype, ils l'ont mis au travail et maintenant c'est notre nouveau projet pour les six prochains mois. Alors que nous nous dirigions vers ma table, il a dit avec enthousiasme quelles nouvelles fonctionnalités seraient dans notre jeu. Combien de sorts différents il a inventés et, bien sûr, qu'il y aura un paladin qui pourra à la fois combattre et lancer de la magie. Et tout le département des artistes travaille déjà sur de nouvelles animations, et la Chine a déjà signé un accord en vertu duquel notre jeu sortira sur leur marché. J'ai regardé en silence le code de mon prototype, réfléchi profondément, tout mis en évidence et supprimé.
Je crois qu'au fil du temps, chaque programmeur, sur la base de son expérience, commence à voir des problèmes évidents qu'il peut rencontrer. Surtout si vous travaillez longtemps en équipe avec un seul game designer. Nous avons un tas de nouvelles exigences et fonctionnalités. Et notre ancienne "architecture" ne peut évidemment pas y faire face.
Lorsqu'on vous demande une tâche similaire lors d'une entrevue, ils essaieront certainement de vous attraper. Ils peuvent prendre de nombreuses formes différentes - les crocodiles, qui peuvent à la fois nager et courir. Des chars qui peuvent tirer un canon ou une mitrailleuse et ainsi de suite. La propriété la plus importante de ces tâches est que vous avez un objet qui peut faire plusieurs choses différentes. Et votre héritage ne peut en aucun cas faire face, car il est impossible d'hériter à la fois de FlyingObject et de SwimmingObject. Et différents objets peuvent effectuer différentes actions. À ce stade, nous abandonnons l'héritage et passons à la composition:
class Character { abilities = []; addAbility (...abilities) { for (const a of abilities) { this.abilities.push(a); } return this; } getAbility (AbilityClass) { for (const a of this.abilities) { if (a instanceof AbilityClass) { return a; } } return null; } }
Chaque action possible est maintenant une classe distincte avec son propre état, et si nécessaire, nous pouvons créer des personnages uniques en leur jetant le nombre de capacités requis. Par exemple, il est très facile de créer un arbre magique immortel:
createMagicTree () { return new Character().addAbility( new SpellCastAbility() ); }
Nous avons perdu l'héritage et une composition est apparue à la place. Maintenant, nous créons un personnage et listons ses capacités possibles. Mais cela ne signifie pas que l'héritage est toujours mauvais, mais dans ce cas, il ne convient pas. La meilleure façon de comprendre si l'héritage est juste est de répondre vous-même à la question de savoir quelle relation cela représente. Si cette connexion est "is-a", c'est-à-dire que vous indiquez que MeleeFightAbility est une capacité, alors elle est parfaite. Si la connexion est créée uniquement parce que vous souhaitez ajouter une action et affiche "has-a", vous devez penser à la composition.
J'ai aimé regarder un excellent résultat. Il fonctionne intelligemment et sans bugs, architecture de rêve! Je suis sûr qu'il passera plus d'une épreuve du temps et pendant longtemps nous n'aurons pas à le réécrire. J'étais tellement enthousiaste à propos de mon code que je n'ai même pas remarqué comment June Petya m'a approché.
La rue était déjà sombre, ce qui rendait encore plus visible comment il brillait de bonheur. Apparemment, il a réussi à repousser la tâche et à se débarrasser de la pénalité pour les tapis en direction de ses collègues, qui a été annoncée la semaine dernière.
"Les artistes ont peint des animations simplement divines", a-t-il rapidement bavardé, "J'ai hâte de les faire baiser." Des avantages volants particulièrement magnifiques lorsqu'un sort de soin est appliqué. Ils sont tellement verts et quels avantages!
Je me suis maudit, parce que j'avais complètement oublié qu'il nous restait à visser la vue. Merde, il semble qu'il faille réécrire l'architecture.
Dans de tels articles, seul le modèle est généralement décrit, car il est abstrait et adulte, et vous pouvez également donner des «images à montrer» à June, quelle que soit l'architecture. Néanmoins, notre modèle devrait fournir un maximum d'informations pour la vue afin qu'elle puisse faire son travail. Dans GameDev, le motif «Team» est généralement utilisé pour cela. En un mot - nous avons un état sans logique, et tout changement devrait se produire dans les équipes correspondantes. Cela peut sembler compliqué, mais il offre de nombreux avantages:
- Ils se combinent très bien quand une équipe en appelle une autre
- Chaque commande, une fois exécutée, est en fait un événement auquel vous pouvez vous abonner
- Nous pouvons facilement les sérialiser.
Par exemple, une commande damage peut ressembler à ceci. C'est alors que le guerrier l'utilisera lorsqu'il sera frappé avec une épée et le magicien lorsqu'il sera frappé par un sort de feu. Maintenant, pour plus de simplicité, j'ai implémenté la validation des commandes via des exceptions, mais elles peuvent ensuite être réécrites sous forme de codes retour.
class DealDamageCommand extends Command { constructor (target, damage) { this.target = target; this.damage = damage; } execute () { const healthAbility = this.target.getAbility(HealthAbility); if (healthAbility == null) { throw new Error('NoHealthAbility'); } const resultHealth = healthAbility.health - this.damage; healthAbility.health = Math.max( 0, resultHealth ); } }
J'aime faire des commandes hiérarchiques - lorsqu'une est exécutée, elle
donne naissance à de nombreux enfants, que le moteur exécute ensuite. Alors maintenant que nous avons la possibilité d'infliger des dégâts, nous pouvons essayer de mettre en œuvre une frappe de mêlée
class MeleeHitCommand extends Command { constructor (source, target, damage) { this.source = source; this.target = target; this.damage = damage; } execute () { const fightAbility = this.source.getAbility(MeleeFightAbility); if (fightAbility == null) { throw new Error('NoFightAbility'); } this.addChildren([ new DealDamageCommand(this.target, fightAbility.power); ]); } }
Ces deux équipes ont tout ce dont vous avez besoin pour nos animations. Un moteur de rendu peut simplement s'abonner à des événements et afficher tout ce que les artistes désirent avec le code suivant:
async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); }
J'ai perdu le compte, qui une fois de suite je reste au travail jusqu'à la tombée de la nuit. Depuis l'enfance, le développement des jeux m'attire, il me semble quelque chose de magique, et même maintenant, alors que je fais ça depuis de nombreuses années, j'en suis inquiet. Malgré le fait que j'ai appris un secret sur la façon dont ils sont créés - je n'ai pas perdu confiance en la magie. Et cette magie me fait m'asseoir avec une telle inspiration la nuit et écrire mon code. Vasya est venu vers moi. Il ne sait absolument pas comment programmer, mais partage mon attitude envers les jeux.
- Ici - le game designer a mis devant moi un Talmud de 200 pages imprimé sur des feuilles A4. Bien que le document de conception ait été réalisé en confluence, nous avons aimé l'imprimer à des étapes importantes afin de ressentir ce travail dans l'incarnation physique. Je l'ai ouvert sur une page aléatoire et j'ai obtenu une liste énorme d'une variété de sorts qu'un mage et un paladin peuvent faire, une description de leurs effets, des besoins en intelligence, du prix du mana et une description approximative pour les artistes comment les afficher. Travailler pendant plusieurs mois, car aujourd'hui je vais de nouveau rester au travail.
Notre architecture facilite la création de combinaisons complexes de sorts. C'est juste que chaque sort peut renvoyer une liste de commandes qui doivent être exécutées pendant le lancement
class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility); if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } this.addChildren(new PayManaCommand(this.source, this.spell.manaCost)); this.addChildren(this.spell.getCommands(this.source, this.target)); } } class Spell { manaCost = 0; getCommands (source, target) { return []; } } class DamageSpell extends Spell { manaCost = 3; constructor (damageValue) { this.damageValue = damageValue; } getCommands (source, target) { return [ new DealDamageCommand(target, this.damageValue) ]; } } class HealSpell extends Spell { manaCost = 2; constructor (healValue) { this.healValue = healValue; } getCommands (source, target) { return [ new HealDamageCommand(target, this.healValue) ]; } } class VampireSpell extends Spell { manaCost = 5; constructor (value) { this.value = value; } getCommands (source, target) { return [ new DealDamageCommand(target, this.value), new HealDamageCommand(source, this.value) ]; } }
Un an et demi plus tard
Le stand-up, comme toujours, a traîné. Je me suis balancé sur la chaise et j'ai accroché l'ordinateur portable pendant que Middle Petya se disputait avec le testeur au sujet du bug qui avait été déclenché. En toute sincérité, il a essayé de convaincre le testeur que le manque de contrôle via le bouton droit de la souris dans notre nouveau jeu ne devrait pas être marqué comme un bug, car une telle tâche n'a jamais été maintenue et n'a pas été élaborée par des concepteurs de jeux ou des bouffons. J'avais un sentiment de déjà-vu, mais un nouveau message dans la discorde m'a distrait:
- Écoutez - le concepteur du jeu a écrit - J'ai une excellente idée ...