Haftungsausschluss:
Meiner Meinung nach sollte und kann ein Artikel zur Softwarearchitektur nicht ideal sein. Jede beschriebene Lösung deckt möglicherweise nicht die für einen Programmierer erforderliche Ebene ab, und für einen anderen Programmierer wird die Architektur zu unnötig kompliziert. Aber es muss eine Lösung für die Aufgaben geben, die es sich selbst gestellt hat. Und diese Erfahrung, zusammen mit all dem anderen Wissensgepäck eines Programmierers, der lernt, Informationen organisiert, Neulinge schärft und sich selbst und andere kritisiert - diese Erfahrung wird zu hervorragenden Softwareprodukten. Der Artikel wechselt zwischen dem technischen und dem technischen Teil. Dies ist ein kleines Experiment und ich hoffe, es wird interessant sein.- Hören Sie, ich hatte eine großartige Spielidee! - Spieledesigner Vasya war zerzaust und seine Augen waren rot. Ich trank immer noch Kaffee und Holivar auf Habré, um die Zeit vor dem Aufstehen zu vertreiben. Er sah mich erwartungsvoll an, bis ich mit dem Schreiben der Kommentare an den Mann fertig war, worüber er sich geirrt hatte. Vasya wusste, dass es keinen Sinn machte, das Gespräch mit mir fortzusetzen, bis Gerechtigkeit herrschte und die Wahrheit nicht geschützt wurde. Ich fügte den letzten Satz hinzu und sah ihn an.
- Kurz gesagt - Magier mit Mana können Zauber wirken, und Krieger können im Nahkampf kämpfen und Ausdauer ausgeben. Sowohl Zauberer als auch Krieger können sich bewegen. Ja, dort wird es weiterhin möglich sein, die Kühe auszurauben, aber wir werden es bereits in der nächsten Version tun, kurz gesagt. Zeigen Sie den Prototyp nach dem Aufstehen, okay?
Er lief in seinem Spieledesign-Geschäft davon und ich öffnete die IDE.
Tatsächlich sind die Themen „Zusammensetzung versus Vererbung“, „Bananen-Affen-Problem“, „Rautenproblem (Mehrfachvererbung)“ bei Interviews in verschiedenen Formaten und aus gutem Grund häufig gestellte Fragen. Eine falsche Verwendung der Vererbung kann die Architektur komplizieren, und unerfahrene Programmierer wissen nicht, wie sie damit umgehen sollen, und beginnen infolgedessen, OOP als Ganzes zu kritisieren und prozeduralen Code zu schreiben. Erfahrene Programmierer (oder diejenigen, die intelligente Dinge im Internet gelesen haben) sehen es daher als ihre Pflicht an, bei einem Interview in verschiedenen Formen nach solchen Dingen zu fragen. Die universelle Antwort lautet: "Zusammensetzung ist besser als Vererbung, und es sollten keine Graustufen angewendet werden." Diejenigen, die gerade jede solche Antwort gelesen haben, werden zu 100% zufrieden sein.
Aber wie ich am Anfang des Artikels sagte, passt jede Architektur zu Ihrem Projekt. Wenn die Vererbung für Ihr Projekt ausreicht und alles, was zur Lösung des Problems erforderlich ist, ist, Monkey with Banana zu erstellen - erstellen Sie es. Unser Programmierer war in einer ähnlichen Situation. Es macht keinen Sinn, die Vererbung abzulehnen, nur weil der FPS Sie auslacht.
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--; } }
Der Stand-up zog sich wie immer hin. Ich schaukelte auf einem Stuhl und legte auf, während June Petya versuchte, den Tester davon zu überzeugen, dass die Unmöglichkeit einer schnellen Steuerung mit der rechten Maustaste kein Fehler ist, da es nirgendwo eine solche Möglichkeit gab, was bedeutet, dass Sie die Aufgabe an die Vorproduktionsabteilung übergeben müssen. Der Tester argumentierte, dass dies für Benutzer ein Fehler und keine Funktion ist, da für Benutzer die Steuerung über die rechte Schaltfläche obligatorisch zu sein scheint. Als einziger Spieler in unserem Team, der unser Spiel auf Kampfservern spielt, wollte er diese Funktion so schnell wie möglich hinzufügen, aber er wusste, dass die bürokratische Maschine sie frühestens in 4 veröffentlichen würde, wenn Sie sie in die Vorproduktionsabteilung legen Monate, und nachdem Sie es als Fehler ausgegeben haben, können Sie es bereits im nächsten Build erhalten. Der Projektmanager war wie immer zu spät und die Jungs fluchten so heftig, dass sie bereits auf Matten umgestiegen waren und es wahrscheinlich bald zu einer Schlägerei gekommen wäre, wenn der Studiodirektor nicht in den Fluch geraten wäre und nicht beide in sein Büro gebracht hätte. Wahrscheinlich wieder für 300 Dollar Geldstrafe.
Als ich den Rallye-Raum verließ, kam ein Spieledesigner auf mich zu und sagte glücklich, dass jeder den Prototyp mochte, sie akzeptierten ihn und jetzt ist dies unser neues Projekt für die nächsten sechs Monate. Während wir zu meinem Tisch gingen, erzählte er begeistert, welche neuen Funktionen in unserem Spiel sein würden. Wie viele verschiedene Zauber er erfunden hat und natürlich, dass es einen Paladin geben wird, der sowohl kämpfen als auch Magie werfen kann. Und die gesamte Künstlerabteilung arbeitet bereits an neuen Animationen, und China hat bereits eine Vereinbarung unterzeichnet, nach der unser Spiel auf den Markt gebracht wird. Ich schaute schweigend auf den Code meines Prototyps, dachte tief nach, hob alles hervor und löschte ihn.
Ich glaube, dass jeder Programmierer im Laufe der Zeit aufgrund seiner Erfahrung offensichtliche Probleme sieht, auf die er stoßen kann. Vor allem, wenn Sie lange Zeit in einem Team mit einem Spieledesigner arbeiten. Wir haben eine Reihe neuer Anforderungen und Funktionen. Und unsere alte "Architektur" kann offensichtlich nicht damit umgehen.
Wenn Sie bei einem Interview nach einer ähnlichen Aufgabe gefragt werden, werden sie sicherlich versuchen, Sie zu fangen. Sie können in vielen verschiedenen Formen vorliegen - Krokodile, die sowohl schwimmen als auch rennen können. Panzer, die eine Kanone oder ein Maschinengewehr abfeuern können und so weiter. Die wichtigste Eigenschaft solcher Aufgaben ist, dass Sie ein Objekt haben, das verschiedene Aufgaben ausführen kann. Und Ihre Vererbung kann in keiner Weise bewältigt werden, da es unmöglich ist, sowohl von FlyingObject als auch von SwimmingObject zu erben. Und verschiedene Objekte können unterschiedliche Aktionen ausführen. An diesem Punkt geben wir die Vererbung auf und fahren mit der Komposition fort:
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; } }
Jede mögliche Aktion ist jetzt eine separate Klasse mit einem eigenen Status. Bei Bedarf können wir eindeutige Charaktere erstellen, indem wir ihnen die erforderliche Anzahl von Fähigkeiten verleihen. Zum Beispiel ist es sehr einfach, einen unsterblichen Zauberbaum zu erstellen:
createMagicTree () { return new Character().addAbility( new SpellCastAbility() ); }
Wir haben das Erbe verloren und stattdessen erschien eine Komposition. Jetzt erstellen wir einen Charakter und listen seine möglichen Fähigkeiten auf. Dies bedeutet jedoch nicht, dass die Vererbung immer schlecht ist, nur in diesem Fall passt sie nicht. Der beste Weg zu verstehen, ob Vererbung richtig ist, besteht darin, die Frage selbst zu beantworten, welche Beziehung sie darstellt. Wenn diese Verbindung "is-a" ist, das heißt, Sie geben an, dass MeleeFightAbility eine Fähigkeit ist, dann ist sie perfekt. Wenn die Verbindung nur erstellt wird, weil Sie eine Aktion hinzufügen möchten und "has-a" anzeigen, sollten Sie über die Komposition nachdenken.
Ich habe es genossen, ein großartiges Ergebnis zu sehen. Es funktioniert intelligent und fehlerfrei, Traumarchitektur! Ich bin sicher, dass es mehr als einen Test der Zeit bestehen wird und wir es für eine lange Zeit nicht neu schreiben müssen. Ich war so begeistert von meinem Code, dass ich nicht einmal bemerkte, wie June Petya auf mich zukam.
Die Straße war bereits dunkel, was es noch deutlicher machte, wie er vor Glück strahlte. Anscheinend gelang es ihm, die Aufgabe abzuschieben und die Strafe für Matten in Richtung Kollegen loszuwerden, die letzte Woche angekündigt wurde.
"Die Künstler haben einfach göttliche Animationen gemalt", schwatzte er schnell, "ich kann es kaum erwarten, sie zu verarschen." Besonders schöne fliegende Pluspunkte, wenn ein Heilzauber angewendet wird. Sie sind so grün und solche Pluspunkte!
Ich fluchte vor mich hin, weil ich völlig vergessen hatte, dass wir immer noch die Sicht vermasseln müssen. Verdammt, es scheint, Architektur neu schreiben zu müssen.
Solche Artikel beschreiben normalerweise nur die Arbeit mit dem Modell, da es abstrakt und erwachsen ist und Sie auch bis Juni „Bilder zum Zeigen“ geben können, unabhängig von der Architektur. Trotzdem sollte unser Modell maximale Informationen für die Ansicht bereitstellen, damit es seine Aufgabe erfüllen kann. In GameDev wird normalerweise das "Team" -Muster verwendet. Kurz gesagt - wir haben einen Zustand ohne Logik, und jede Änderung sollte in den entsprechenden Teams erfolgen. Dies mag kompliziert erscheinen, bietet aber viele Vorteile:
- Sie lassen sich sehr gut kombinieren, wenn ein Team ein anderes anruft
- Jeder Befehl ist, wenn er ausgeführt wird, tatsächlich ein Ereignis, das Sie abonnieren können
- Wir können sie leicht serialisieren.
Ein Schadensbefehl könnte beispielsweise so aussehen. Dann wird der Krieger es benutzen, wenn er mit einem Schwert getroffen wird, und der Magier, wenn er von einem Feuerzauber getroffen wird. Der Einfachheit halber habe ich die Befehlsüberprüfung durch Ausnahmen implementiert, aber dann können sie als Rückkehrcodes umgeschrieben werden.
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 ); } }
Ich mache gerne hierarchische Befehle - wenn einer ausgeführt wird, werden viele Kinder geboren, die die Engine dann ausführt. Jetzt, da wir die Fähigkeit haben, Schaden zu verursachen, können wir versuchen, einen Nahkampfschlag auszuführen
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); ]); } }
Diese beiden Teams haben alles, was Sie für unsere Animationen benötigen. Ein Renderer kann einfach Ereignisse abonnieren und mit dem folgenden Code alles anzeigen, was Künstler wünschen:
async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); }
Ich habe die Zählung verloren, und einmal in Folge bleibe ich bis zur Dunkelheit bei der Arbeit. Seit meiner Kindheit hat mich die Entwicklung von Spielen angezogen, es schien mir etwas Magisches zu sein, und selbst jetzt, wo ich das seit vielen Jahren mache, bin ich besorgt darüber. Trotz der Tatsache, dass ich ein Geheimnis darüber gelernt habe, wie sie geschaffen werden, habe ich das Vertrauen in die Magie nicht verloren. Und diese Magie lässt mich nachts mit solcher Inspiration sitzen und meinen Code schreiben. Vasya kam auf mich zu. Er weiß absolut nicht, wie man programmiert, teilt aber meine Einstellung zu Spielen.
- Hier - der Spieledesigner stellte mir einen Talmud von 200 Seiten vor, der auf A4-Blättern gedruckt war. Obwohl das Designdokument in Konfluenz ausgeführt wurde, haben wir es gerne in wichtigen Phasen gedruckt, um diese Arbeit in physischer Verkörperung zu spüren. Ich öffnete es auf einer zufälligen Seite und bekam eine riesige Liste mit einer Vielzahl von Zaubersprüchen, die ein Magier und ein Paladin ausführen können, eine Beschreibung ihrer Effekte, Intelligenzanforderungen, Manapreis und eine ungefähre Beschreibung für Künstler, wie sie angezeigt werden sollen. Arbeite viele Monate, denn heute werde ich wieder bei der Arbeit bleiben.
Unsere Architektur macht es einfach, komplexe Kombinationen von Zaubersprüchen zu erstellen. Es ist nur so, dass jeder Zauber eine Liste von Befehlen zurückgeben kann, die während des Zaubers ausgeführt werden müssen
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) ]; } }
Eineinhalb Jahre später
Der Stand-up zog sich wie immer hin. Ich schwankte auf dem Stuhl und hing im Laptop, während Middle Petya mit dem Tester über den Fehler diskutierte, der gestartet worden war. Mit aller Aufrichtigkeit versuchte er den Tester davon zu überzeugen, dass die mangelnde Kontrolle über die rechte Maustaste in unserem neuen Spiel nicht als Fehler markiert werden sollte, da eine solche Aufgabe nie stand und nicht von Spieledesignern oder Spaßvögeln ausgearbeitet wurde. Ich hatte ein Gefühl von Deja Vu, aber eine neue Botschaft in der Zwietracht lenkte mich ab:
- Hören Sie - der Spieledesigner schrieb - Ich habe eine großartige Idee ...