Brainstorming mit Umsetzung
Manchmal stecke ich fest und muss nach Wegen suchen, um die Aufgabe aus einem anderen Blickwinkel zu betrachten. Es gibt Aufgaben, die in Form einer Matrix oder Tabelle angezeigt werden können. Ihre Struktur sieht ungefähr so aus:
Die Zellen, mit denen ich arbeite, sind in Spalten und Zeilen angeordnet. Nehmen wir ein Beispiel aus einem einfachen Spiel:
Linien sind Charakterklassen: Krieger, Magier, Dieb.
Spalten sind Arten von Aktionen: Angriff, Verteidigung, Spezialaktion.
Die Matrix enthält den gesamten Code zum Verarbeiten der einzelnen Aktionstypen für jeden Zeichentyp.
Wie sieht der Code aus? Typischerweise sind solche Strukturen in solchen Modulen organisiert:
Fighter
enthält einen Code für den Umgang mit Schwertschlägen, der den Schaden durch Rüstung und einen besonders starken Schlag verringert.Mage
enthält einen Code zum Umgang mit Feuerbällen, Schadensreflexion und einen speziellen Frostangriff.Thief
enthält Code, um Dolchangriffe zu handhaben, Ausweichschäden zu vermeiden und einen speziellen Entwaffnungsangriff durchzuführen.
Es ist manchmal nützlich, die Matrix zu transponieren. Wir können es auf einer anderen Achse anordnen
::
Attack
enthält einen Code für den Umgang mit Schaukeln, Feuerbällen und Dolchangriffen.Defend
enthält einen Code für den Umgang mit Schadensreduzierung, der den Schaden widerspiegelt und Schäden vermeidet.Special
enthält einen leistungsstarken Code zum Behandeln, Einfrieren und Deaktivieren.
Mir wurde beigebracht, dass ein Stil "gut" und der andere "schlecht" ist. Aber mir ist nicht klar, warum alles so sein sollte. Der Grund ist die
Annahme, dass wir häufig neue Klassen von Zeichen (Substantiven) und selten neue Arten von Aktionen (Verben) hinzufügen. Auf diese Weise kann ich mit dem neuen Modul Code hinzufügen, ohne
alle verfügbaren zu berühren. In Ihrem Spiel kann alles anders sein. Wenn ich mir die transponierte Matrix anschaue, bin ich mir der Existenz der Annahme bewusst und kann Zweifel daran aufkommen lassen. Dann werde ich über die Art der Flexibilität nachdenken, die ich brauche, und erst dann werde ich die Codestruktur auswählen.
Schauen wir uns ein anderes Beispiel an.
Interpretationen von Programmiersprachen haben verschiedene Arten von Knoten, die Primitiven entsprechen: Konstanten, Operatoren, Schleifen, Verzweigungen, Funktionen, Typen usw. Wir müssen Code für alle generieren.
Großartig! Sie können für jeden Knotentyp eine Klasse erstellen, die alle vom
Node
der Basisklasse erben können. Wir gehen jedoch davon aus, dass wir Zeilen und seltener Spalten hinzufügen. Was passiert im optimierenden Compiler? Wir fügen neue Optimierungsdurchläufe hinzu. Und jeder von ihnen ist eine neue Spalte.
Wenn ich einen neuen Optimierungsdurchlauf hinzufügen möchte, muss ich jeder Klasse eine neue Methode hinzufügen, und der gesamte Code für den Optimierungsdurchlauf wird auf verschiedene Module verteilt. Ich möchte eine solche Situation vermeiden! Daher wird in einigen Systemen eine weitere Schicht hinzugefügt. Mithilfe des Besuchermusters kann ich den gesamten Code zum Zusammenführen von Schleifen in einem Modul speichern, anstatt ihn in mehrere Dateien aufzuteilen.
Wenn Sie sich die transponierte Matrix ansehen, werden wir einen anderen Ansatz eröffnen:
Anstelle von
Klassen mit
Methoden kann ich jetzt
Tagged Union und
Pattern Matching verwenden (sie werden nicht in allen Programmiersprachen unterstützt). Aus diesem Grund wird der gesamte Code jedes Optimierungsdurchlaufs zusammen gespeichert und kann auf die Indirektheit des Besuchermusters verzichten.
Es ist oft nützlich, das Problem aus der Sicht der Matrix zu betrachten. Wenn Sie es auf eine objektorientierte Struktur anwenden, über die jeder nachdenkt, kann es mich zu etwas anderem führen, zum Beispiel zum Muster „Entity-Component-System“, zu relationalen Datenbanken oder zur reaktiven Programmierung.
Und das gilt nicht nur für den Code. Hier ist ein Beispiel für die Anwendung dieser Idee auf Produkte. Angenommen, es gibt Menschen mit unterschiedlichen Interessen:
Wenn ich eine Social-Networking-Site entwickeln würde, könnte ich den Leuten erlauben,
die Nachrichten anderer
Leute zu verfolgen. Nick kann sich für Alice anmelden, weil beide an Autos interessiert sind, und für Fenya, weil sie beide an Reisen interessiert sind. Nick wird aber auch Alices Beiträge zur Mathematik und Fenyas Beiträge zur Politik erhalten. Wenn ich über eine transponierte Matrix nachdenken würde, könnte ich den Leuten erlauben,
Themen zu abonnieren. Nick konnte sich einer Gruppe von Autoliebhabern sowie einer Gruppe von Reisenden anschließen. Facebook und Reddit begannen ungefähr zur gleichen Zeit, aber sie sind transponierte Matrizen voneinander. Facebook ermöglicht es Ihnen, Menschen zu folgen; Mit Reddit können Sie Themen abonnieren.
Wenn ich zum Stillstand komme oder Alternativen in Betracht ziehen möchte, schaue ich mir das Problem an und suche darin nach verschiedenen Ordnungsachsen. Manchmal kann es eine bessere Lösung sein, ein Problem aus einem anderen Blickwinkel zu betrachten.
Zersetzung Brainstorming
Ich benutze eine andere Technik namens Zersetzung.
In der Algebra transformiert die
Zerlegungsoperation ein Polynom der Form 5x² + 8x - 21 in (x + 3) · (5x - 7). Um die Gleichung 5x² + 8x - 21 = 0 zu lösen, können wir sie zuerst in (x + 3) · (5x - 7) = 0 zerlegen. Dann können wir sagen, dass x + 3 = 0
oder 5x - 7 = 0. Expansion macht aus einer schwierigen Aufgabe ein paar einfachere Aufgaben.
Schauen wir uns ein Beispiel an: Ich habe sechs Klassen:
File
,
EncryptedFile
,
GzipFile
,
EncryptedGzipFile
,
BzipFile
,
EncryptedBzipFile
. Ich kann sie in eine Matrix zerlegen:
Unter Verwendung des "Dekorator" -Musters (oder von Verunreinigungen) habe ich sechs verschiedene Dateitypen in vier Komponenten umgewandelt: normal, gzip, bzip, verschlüsseln. Dies scheint nicht viel zu sparen, aber wenn ich weitere Variationen hinzufüge, werden die Einsparungen steigen. Durch Zerlegung werden O (M * N) -Komponenten in O (M + N) -Komponenten umgewandelt.
Ein anderes Beispiel: Manchmal stellen mir Leute Fragen wie
"Wie schreibe ich
lineare Interpolation in C #?" . Ich kann viele mögliche Tutorials schreiben:
Wenn es M Themen und N Sprachen gibt, kann ich M * N Tutorials schreiben. Dies ist jedoch
viel Arbeit. Stattdessen schreibe ich ein Tutorial über
Interpolation , jemand anderes schreibt ein Tutorial über C #, und dann kombiniert der Leser das Wissen über C # mit dem Wissen über Interpolation und schreibt seine Version der Interpolation in C #.
Wie die Transposition hilft auch die Zerlegung nicht immer, kann aber gegebenenfalls sehr nützlich sein.
Rückwärts Brainstorming
In den beiden vorhergehenden Teilen habe ich darüber gesprochen, wie ich mich manchmal der Aufgabe nähere und versuche, sie in einer Matrix anzuordnen. Manchmal hilft das nicht und dann versuche ich, die Aufgabe in die entgegengesetzte Richtung zu betrachten. Schauen wir uns zum Beispiel die prozedurale Kartengenerierung an. Oft beginne ich mit einer Rauschfunktion, füge dann Oktaven hinzu, passe Parameter an und füge Ebenen hinzu. Ich mache das, weil ich Karten brauche, die bestimmte Eigenschaften haben.
Es ist durchaus möglich, mit Experimenten mit Parametern zu beginnen, aber der Parameterraum ist ziemlich groß, und es ist nicht bekannt, ob ich die Parameter finde, die für meine Anforderungen am besten geeignet sind. Nachdem ich ein wenig experimentiert habe, höre ich auf und beginne in umgekehrter Reihenfolge zu denken: Wenn ich beschreiben kann, was ich brauche, kann dies beim Finden der Parameter helfen.
Es war diese Motivation, die mich dazu brachte, Algebra zu studieren. Wenn wir eine Gleichung der Form
5x² + 8x - 21 = 0 haben , was ist dann
x ? Wenn ich die Algebra nicht kannte, löste ich diese Gleichung, versuchte, verschiedene Werte von
x zu ersetzen, wählte sie zuerst zufällig aus und passte sie dann an, wenn ich das Gefühl hatte, der Lösung nahe zu sein. Die Algebra gibt uns ein Werkzeug, um in eine andere Richtung zu gehen. Anstatt die Antworten zu erraten, gibt sie mir einen Apparat (Zerlegung oder quadratische Gleichungen oder die Newtonsche Methode der iterativen Suche nach Wurzeln), mit dem ich bewusster nach
x- Werten suchen kann (-3 oder 7/5).
Ich habe das Gefühl, dass ich oft in eine solche Programmiersituation gerate. Während ich an der Erstellung von prozeduralen Karten arbeitete, hielt ich nach längerem Experimentieren mit den Parametern an und stellte eine Liste zusammen, was in den Spielwelten
eines Projekts enthalten sein sollte :
- Die Spieler müssen das Spiel weit von der Küste entfernt starten.
- Beim Aufstieg müssen die Spieler bergauf klettern.
- Spieler sollten nicht in der Lage sein, den Rand der Karte zu erreichen.
- Mit zunehmendem Level müssen sich die Spieler Gruppen anschließen.
- Es sollte einfache Monster an den Küsten geben, ohne viel Abwechslung.
- In den Ebenen sollte es eine Vielzahl von Monstern mit mittlerem Schwierigkeitsgrad geben.
- In Berggebieten muss es komplexe Boss-Monster geben.
- Es muss eine Art Orientierungspunkt geben, der es den Spielern ermöglicht, auf dem gleichen Schwierigkeitsgrad zu bleiben, und einen weiteren Orientierungspunkt, mit dem Sie den Schwierigkeitsgrad erhöhen oder senken können.
Die Zusammenstellung dieser Liste führte zur Schaffung der folgenden Einschränkungen:
- Spielwelten sollten Inseln mit vielen Küsten und einem kleinen Gipfel in der Mitte sein.
- Die Höhe sollte der Komplexität der Monster entsprechen.
- In niedrigen und hohen Höhen sollte die Variabilität der Biome geringer sein als in mittleren Höhen.
- Die Straßen sollten auf dem gleichen Schwierigkeitsgrad bleiben.
- Flüsse sollten von großen zu kleinen Höhen fließen und den Spielern die Möglichkeit geben, sich auf und ab zu bewegen.
Diese Einschränkungen veranlassten mich, einen Kartengenerator zu entwerfen. Und er führte zur Erzeugung eines
viel besseren Kartensatzes als der, die ich durch Anpassen der Parameter erhielt, wie ich es normalerweise tue. Der
daraus resultierende Artikel interessierte viele Menschen für die Erstellung von Karten auf der Grundlage von Voronoi-Diagrammen.
Unit-Tests sind ein weiteres Beispiel. Es wird empfohlen, eine Liste mit zu testenden Beispielen zu erstellen. Zum Beispiel könnte ich für Sechseckgitter denken, dass ich die Bedingung
add(Hex(1, 2), Hex(3, 4)) == Hex(4, 6)
überprüfen muss. Dann kann ich mich daran erinnern, dass Sie die Nullen überprüfen müssen:
add(Hex(0, 1), Hex(7, 9)) == Hex(7, 10)
. Dann kann ich mich daran erinnern, dass Sie negative Werte überprüfen müssen:
add(Hex(-3, 4) + Hex(7, -8)) == Hex(4, -4)
. Gut, großartig, ich habe ein paar Unit-Tests.
Aber wenn Sie etwas weiter überlegen, dann überprüfe ich
tatsächlich add(Hex(A, B), Hex(C, D)) == Hex(A+C, B+D)
. Ich habe die drei oben gezeigten Beispiele basierend auf dieser allgemeinen Regel entwickelt. Ich gehe in die entgegengesetzte Richtung von dieser Regel, um zu Unit-Tests zu kommen. Wenn ich diese Regel direkt in ein Testsystem codieren kann, kann das System selbst in umgekehrter Reihenfolge arbeiten, um Beispiele für Tests zu erstellen. Dies wird als eigenschaftsbasiertes Testen bezeichnet. (Siehe auch:
metamorphe Tests )
Ein weiteres Beispiel: Löser von Einschränkungen. In solchen Systemen beschreibt der Benutzer, was er in der Ausgabe sehen möchte, und das System findet einen Weg, diese Einschränkungen zu erfüllen. Zitat aus dem Procedural Content Generation Book,
Kapitel 8 :
Mit den konstruktiven Methoden aus Kapitel 3 sowie den Fraktal- und Rauschmethoden aus Kapitel 4 können wir verschiedene Arten von Ausgabedaten erstellen, indem wir die Algorithmen optimieren, bis wir mit ihren Ausgabedaten zufrieden sind. Wenn wir jedoch wissen, welche Eigenschaften der generierte Inhalt haben sollte, ist es bequemer, direkt anzugeben, wie der allgemeine Algorithmus Inhalte finden soll, die unseren Kriterien entsprechen.
Dieses Buch beschreibt die Programmierung von Antwortsätzen (ASP), in denen wir die Struktur unserer Arbeit beschreiben (Fliesen sind Boden und Wände, Fliesen grenzen aneinander), die Struktur der Lösungen, nach denen wir suchen (ein Dungeon ist eine Gruppe Verbundene Kacheln mit Anfang und Ende) und die Eigenschaften der Lösungen (Seitengänge sollten nicht mehr als 5 Räume enthalten, es müssen 1-2 Schleifen im Labyrinth vorhanden sein, drei Assistenten müssen besiegt werden, bevor sie den Boss erreichen). Danach erstellt das System mögliche Lösungen und ermöglicht Ihnen zu entscheiden, was mit ihnen geschehen soll.
Kürzlich wurde ein Constraint-Solver entwickelt, der aufgrund seines coolen Namens und seiner kuriosen Demo großes Interesse weckte: Wave Function Collapse.
[Es gibt einen Artikel über diesen Löser auf Habr.] Wenn Sie ihm Beispielbilder geben, um anzugeben, welche Einschränkungen benachbarten Kacheln auferlegt werden, erstellt er neue Beispiele, die den angegebenen Mustern entsprechen. Seine Arbeit ist beschrieben in
WaveFunctionCollapse ist Constraint Solving in the Wild :
WFC implementiert eine gierige Suchmethode, ohne zurück zu gehen. In diesem Artikel wird WFC als Beispiel für auf Einschränkungen basierende Entscheidungsmethoden erläutert.
Mit Hilfe von Constraint Solvern habe ich bereits viel erreicht. Wie bei der Algebra muss ich viel lernen, bevor ich lerne, wie man sie effektiv einsetzt.
Ein weiteres Beispiel: das
Raumschiff, das ich erstellt habe . Der Spieler kann Motoren überall hin ziehen, und das System bestimmt, welche Motoren aktiviert werden müssen, wenn Sie auf W, A, S, D, Q, E klicken. Beispiel: In diesem Schiff:
Wenn Sie vorwärts fliegen möchten, schließen Sie zwei hintere Motoren ein. Wenn Sie nach links abbiegen möchten, schalten Sie die rechten hinteren und linken vorderen Motoren ein. Ich habe nach einer Lösung gesucht und das System gezwungen,
viele Parameter zu
durchlaufen :
Das System hat funktioniert, aber nicht perfekt. Später wurde mir klar, dass dies ein weiteres Beispiel dafür ist, wo die Lösung in die entgegengesetzte Richtung helfen könnte. Es stellte sich heraus, dass die Bewegung von Raumfahrzeugen durch ein
lineares System von Beschränkungen beschrieben werden
kann . Wenn ich das verstehe, könnte ich eine vorgefertigte Bibliothek verwenden, die die Einschränkungen genau löst, und nicht meine Trial-and-Error-Methode, die eine Annäherung zurückgibt.
Und noch ein Beispiel: das G9.js-Projekt, in dem Sie die
Ausgabe einer bestimmten Funktion auf den Bildschirm ziehen können und das bestimmt, wie die
Eingabedaten geändert
werden , um sie an die gewünschten Ausgabedaten anzupassen.
G9.js Demos sehen toll aus! Stellen Sie sicher, dass Sie die Zeile "Kommentieren Sie die folgende Zeile aus" in der Rings-Demo auskommentieren.
Es ist manchmal nützlich, sich eine Aufgabe in umgekehrter Reihenfolge vorzustellen. Es stellt sich oft heraus, dass dies mir
bessere Lösungen gibt als mit direktem Denken.