Engine, Skriptsprache und visueller Roman - in 45 Stunden

Visual Novel, Engine und Skriptsprache für 45 Stunden


Grüße Es geschah, dass ich drei Jahre hintereinander ein Spiel als Geschenk zum Neujahr für bestimmte Leute gemacht habe. Im Jahr 2018 war es ein Plattformer mit Puzzle-Elementen, über den ich auf einem Hub schrieb Im Jahr 2019 - ein Netzwerk RTS für zwei Spieler, über die ich nichts geschrieben habe. Und schließlich, in der 2020. - eine visuelle Kurzgeschichte, die später diskutiert wird, in sehr kurzer Zeit erstellt.


In diesem Artikel:


  • Design und Implementierung der Engine für visuelle Kurzgeschichten,
  • ein Spiel mit einer nichtlinearen Handlung in 8 Stunden,
  • Entfernung der Logik des Spiels in Skripten in ihrer eigenen Sprache.

Interessant? Dann willkommen bei cat.


Achtung: Es gibt viel Text und ~ 3,5 MB Bilder


Inhalt:


0. Begründung für die Entwicklung des Motors.


  1. Die Wahl der Plattform.
  2. Motorarchitektur und deren Implementierung:
    2.1. Erklärung des Problems.
    2.2. Architektur und Implementierung.
  3. Skriptsprache:
    3.1. Sprache.
    3.2. Dolmetscher
  4. Spielentwicklung:
    4.1. Die Geschichte und Entwicklung der Logik des Spiels.
    4.2. Grafik
  5. Statistiken und Ergebnisse.

Hinweis: Wenn Sie aus irgendeinem Grund nicht an den technischen Details interessiert sind, können Sie mit Schritt 4 "Spieleentwicklung" fortfahren. Der Großteil des Inhalts wird jedoch übersprungen


0. Begründung für die Entwicklung des Motors


Natürlich gibt es eine Vielzahl von vorgefertigten Engines für visuelle Kurzgeschichten, die zweifellos besser sind als die unten beschriebene Lösung. Egal was für ein Programmierer ich war, wenn ich keinen anderen geschrieben hätte. Nehmen wir daher an, dass seine Entwicklung gerechtfertigt war.


1. Plattformauswahl


Die Auswahl war in der Tat gering: entweder Java oder C ++. Ohne nachzudenken, entschied ich mich, meinen Plan in Java umzusetzen, weil für eine schnelle Entwicklung bietet es alle Möglichkeiten (nämlich automatisches Speichermanagement und größere Einfachheit im Vergleich zu C ++, das viele Details auf niedriger Ebene verbirgt und dadurch weniger Betonung auf die Sprache selbst zulässt und nur an Geschäftslogik denkt), und bietet auch Unterstützung für Windows, Grafik und Audio von Anfang an.


Swing wurde ausgewählt, um die grafische Benutzeroberfläche zu implementieren, da ich Java 13 verwendet habe, bei dem JavaFX nicht mehr Teil der Bibliothek ist und das Hinzufügen von mehreren zehn Megabyte OpenJFX, je nachdem, zu faul war. Vielleicht war dies nicht die beste Lösung, aber dennoch.


Es stellt sich wahrscheinlich die Frage: Was für eine Spiel-Engine ist das, aber ohne Hardwarebeschleunigung? Die Antwort liegt in der fehlenden Zeit, um sich mit OpenGL zu befassen, sowie in seiner absoluten Sinnlosigkeit: FPS ist für einen visuellen Roman nicht wichtig (auf jeden Fall mit so viel Animation und Grafik wie in diesem Fall).


2. Die Architektur der Engine und ihre Implementierung


2.1 Darlegung des Problems


Um zu entscheiden, wie etwas zu tun ist, müssen Sie entscheiden, warum. Hier geht es mir um die Aussage des Problems, denn Die Architektur ist nicht universell, aber eine "domänenspezifische" Engine hängt per definitionem direkt vom beabsichtigten Spiel ab.


Unter Universal Engine verstehe ich die Engine, die Konzepte auf relativ niedriger Ebene unterstützt, wie "Game Object", "Scene", "Component". Es wurde beschlossen, es nicht zu einem Universalmotor zu machen, da dies die Entwicklungszeit erheblich verkürzen würde.


Das Spiel sollte wie geplant aus folgenden Teilen bestehen:


Struktur des visuellen Romanspiels


Das heißt, für jede Szene gibt es einen Hintergrund, den Haupttext sowie ein Textfeld für Benutzereingaben (der visuelle Roman wurde genau mit willkürlichen Benutzereingaben gedacht und nicht wie so oft aus den vorgeschlagenen Optionen ausgewählt. Später werde ich Ihnen sagen, warum es schlecht war Entscheidung). Das Diagramm zeigt auch, dass es im Spiel mehrere Szenen geben kann und daher Übergänge zwischen ihnen hergestellt werden können.


Hinweis: Mit Szene meine ich den logischen Teil des Spiels. Das Kriterium für die Szene kann genau in diesem Teil derselbe Hintergrund sein.


Zu den Anforderungen an die Engine gehörte auch die Fähigkeit, Audio abzuspielen und Nachrichten anzuzeigen (mit der optionalen Benutzereingabefunktion).


Der vielleicht wichtigste Wunsch war der, die Spielelogik nicht in Java, sondern in einer einfachen deklarativen Sprache zu schreiben.


Es bestand auch der Wunsch, die Möglichkeit einer prozeduralen Animation, nämlich der elementaren Bewegung von Bildern, zu realisieren, damit auf Java-Ebene die Funktion bestimmt werden kann, anhand derer die aktuelle Bewegungsgeschwindigkeit berücksichtigt wird (zum Beispiel so, dass der Geschwindigkeitsgraph direkt oder sinusförmig oder etwas anderes ist).


Die gesamte Benutzerinteraktion sollte wie geplant über ein Dialogsystem erfolgen. In diesem Fall wurde der Dialog nicht unbedingt als Dialog mit dem NPC oder ähnlichem betrachtet, sondern im Allgemeinen als Reaktion auf Benutzereingaben, für die der entsprechende Handler registriert wurde. Unverständlich? Es wird bald klarer.


2.2. Architektur und Implementierung


Aus all diesen Gründen können Sie die Engine in drei relativ große Teile aufteilen, die denselben Java-Paketen entsprechen:


  1. display - enthält alles, was die Ausgabe von Informationen (Grafik, Text und Ton) an den Benutzer sowie den Empfang von Eingaben von ihm betrifft. Eine Art (View), wenn wir über MVC / MVP / etc. sprechen.
  2. initializer - enthält Klassen, in denen die Engine initialisiert und gestartet wird.
  3. sl - enthält Tools zum Arbeiten mit der Skriptsprache (im Folgenden - SL).

In diesem Abschnitt werde ich die ersten beiden Teile betrachten. Ich werde mit dem zweiten beginnen.


Die Initialisierungsklasse verfügt über zwei Hauptmethoden: initialize() und run() . Die Steuerung erfolgt zunächst in der Launcher-Klasse, von der aus initialize() aufgerufen wird. Nach dem Aufruf analysiert der Initialisierer die an das Programm übergebenen Parameter (den Pfad zum Verzeichnis mit den Aufgaben und den Namen der auszuführenden Aufgabe), lädt das Manifest der ausgewählten Aufgabe (etwas später), initialisiert die Anzeige und prüft, ob die für die Aufgabe erforderliche Sprachversion (SL) von den Daten unterstützt wird Der Interpreter startet schließlich einen separaten Thread für die Entwicklerkonsole.


Unmittelbar danach ruft der Launcher die run() -Methode auf, die das eigentliche Laden der Quest in Gang setzt, wenn alles reibungslos lief. Zunächst gibt es alle Skripte, die sich auf die heruntergeladene Quest beziehen (zur Struktur der Questdatei - siehe unten). Sie werden dem Analysator zugeführt, dessen Ergebnis dem Interpreter übergeben wird. Dann wird die Initialisierung aller Szenen gestartet und der Initialisierer beendet die Ausführung seines Streams und hängt schließlich den Enter-Key-Handler auf dem Display auf. Wenn der Benutzer die Eingabetaste drückt, wird die erste Szene geladen, aber dazu später mehr.


Die Dateistruktur der Quest ist wie folgt:


Dateistruktur der Quest


Es gibt einen separaten Ordner für die Quest, in dessen Stammverzeichnis sich ein Manifest befindet, sowie drei zusätzliche Ordner: audio - für Sound, graphics - für den visuellen Teil und scenes - für Skripte, die Szenen beschreiben.


Ich möchte das Manifest kurz beschreiben. Es enthält die folgenden Felder:


  • sl_version_req - SL-Version, die zum Starten der Quest erforderlich ist,
  • init_scene - der Name der Szene, ab der die Quest beginnt,
  • quest_name - ein schöner quest_name , der im Fenstertitel erscheint,
  • resolution - die Auflösung des Bildschirms, für den die Quest bestimmt ist (ein paar Worte dazu später),
  • font_size - Schriftgröße für den gesamten Text,
  • font_name ist der Schriftname für den gesamten Text.

Es ist anzumerken, dass während der Initialisierung der Anzeige unter anderem die Berechnung der Renderauflösung durchgeführt wurde: Das heißt, die erforderliche Auflösung wurde aus dem Manifest entnommen und in den für das Fenster verfügbaren Platz gedrückt, so dass:


  • das Seitenverhältnis blieb das gleiche wie in der Auflösung aus dem Manifest,
  • Der gesamte verfügbare Platz war entweder in der Breite oder in der Höhe belegt.
    Dank dessen kann der Entwickler der Quest sicher sein, dass seine Bilder, zum Beispiel 16: 9, auf jedem Bildschirm in diesem Verhältnis angezeigt werden.

Wenn die Anzeige initialisiert wird, wird der Cursor ausgeblendet, da er nicht am Gameplay beteiligt ist.


Kurz gesagt zur Entwicklerkonsole. Es wurde aus folgenden Gründen entwickelt:


  1. Zum Debuggen.
  2. Wenn während des Spiels etwas schief geht, kann dies über die Entwicklerkonsole behoben werden.
    Es wurden nur wenige Befehle implementiert, nämlich: Ausgeben von Deskriptoren eines bestimmten Typs und ihres Status, Ausgeben von Arbeitsthreads, Neustarten der Anzeige und des wichtigsten Befehls - exec , mit dem jeder SL-Code in der aktuellen Szene ausgeführt werden konnte.

Dies beendet die Beschreibung des Initialisierers und verwandter Dinge, und wir können mit der Beschreibung der Anzeige fortfahren.


Seine endgültige Struktur ist wie folgt:


Anzeigestruktur


Aus der Erklärung des Problems können wir schließen, dass alles, was getan werden muss, ist, Bilder zu zeichnen, Text zu zeichnen und Audio abzuspielen.


Wie wird Text / Bild normalerweise in Universal Engines und darüber hinaus gezeichnet? Es gibt eine Methode vom Typ update() , die bei jedem Häkchen / Schritt / Frame / Render / Frame / usw. aufgerufen wird und bei der eine Methode vom Typ drawText() / drawImage() - dies stellt das Erscheinungsbild von Text / Bild in diesem Frame sicher. Sobald der Aufruf solcher Methoden beendet wird, wird das Rendern der entsprechenden Objekte jedoch beendet.


In meinem Fall wurde beschlossen, etwas anderes zu machen. Da für Bildromane Text und Bilder relativ dauerhaft sind und auch fast alles sind, was der Benutzer sieht (das heißt, sie sind wichtig genug), wurden sie als Spielobjekte erstellt - das heißt, Dinge, die man nur erzeugen muss und die nicht verschwinden bis du sie fragst. Darüber hinaus vereinfachte diese Lösung die Implementierung.


Ein Objekt (aus Sicht von OOP), das den Text / das Bild beschreibt, wird als Deskriptor bezeichnet. Das heißt, für den Benutzer der API-Engine gibt es nur Deskriptoren, die zum Anzeigestatus hinzugefügt und daraus entfernt werden können. Daher gibt es in der endgültigen Version der Anzeige die folgenden Deskriptoren (sie entsprechen den Klassen mit dem gleichen Namen):


Name des Deskriptors:Beschreibung des Deskriptors:
ImageDescriptorBildbeschreibung: Enthält das Bild ( BufferedImage ), die Position auf dem Bildschirm und die Breite mit der Höhe. Beim Erstellen wird jedoch nur die Breite angegeben - die Höhe wird proportional zur ursprünglichen Höhe berechnet (und es gibt keine Möglichkeit, das Bild überproportional manuell zu strecken / zu komprimieren, sodass dies eine falsche Entscheidung war).
TextDescriptorTextdeskriptor: Enthält den Text, seine Position und Größe. Darüber hinaus wird der Text nicht in Breite und Höhe skaliert, sondern über die Grenzen hinaus abgeschnitten. Der Text kann durch Silben und einfach durch Leerzeichen übertragen werden und auch durch die Zeilen scrollen.
AudioDescriptorEin Audio-Deskriptor, mit dem Audio (entweder einmalig oder in einer Schleife) wiedergegeben, angehalten und entfernt werden kann.
AnimationDescriptorEin Animationshandle, das wie Audio geloopt werden kann. Enthält ein Objekt, das die Animation implementiert, und einen Bilddeskriptor, für den die Animation abgespielt wird. Die Hauptmethode ist update(long) gibt die Anzahl der Millisekunden an, die seit dem letzten update(long) Aufruf update(long) vergangen sind. Ihre Anzahl wird verwendet, um den aktuellen Status der Animation zu berechnen.
InputDescriptorEingabedeskriptor: ist ein Textfeld, in dem sich der Cursor immer am Ende des Textes befindet. Es ist auch erwähnenswert, dass das Speichern und Rendern von Text aus einem Eingabedeskriptor über einen implizit erstellten Textdeskriptor erfolgt, um die Logik nicht zu duplizieren. Das Lustige ist, dass ich die Möglichkeit, die Rücktaste zu drücken, in Betracht gezogen habe, aber Delete nicht berücksichtigt habe; und als während des Spiels noch Löschen gedrückt wurde, erschien ▯▯▯ im Feld, da für Löschen keine Verarbeitung durchgeführt wurde und das Zeichen versucht wurde, als Text anzuzeigen.
KeyAwaitDescriptorEin Handle, an das die erforderliche Taste (von KeyEvent ) übergeben wird, und ein Rückruf mit einer Logik, die beim Drücken der entsprechenden Taste gestartet wird.
PostWorkDescriptorEin Handle, das einen Rückruf akzeptiert, der nach der Verarbeitung jedes Ticks aufgerufen wird.

Die Anzeige enthält auch Felder für den aktuellen Eingabeempfänger (Eingabedeskriptor) und ein Feld, das angibt, welcher Textdeskriptor jetzt den Fokus hat und dessen Text mit den entsprechenden Aktionen des Benutzers gescrollt wird.


Der Spielzyklus sieht ungefähr so ​​aus:


  1. Audioverarbeitung - Aufrufen der update() -Methode für Audiodeskriptoren, die den aktuellen Status des Audios überprüft, Speicher freigibt (falls erforderlich) und andere technische Arbeiten ausführt.
  2. Tastatureingaben verarbeiten - überträgt eingegebene Zeichen an einen Deskriptor zum Empfangen von Eingaben, verarbeitet Tastatureingaben für Bildlauftasten (Aufwärts- und Abwärtspfeile) und die Rücktaste.
  3. Animationsverarbeitung.
  4. Löschen des Hintergrunds im Rendering-Puffer ( BufferedImage diente als Puffer).
  5. Bilder zeichnen.
  6. Textwiedergabe.
  7. Zeichnen von Eingabefeldern.
  8. Die Ausgabe des Puffers auf den Bildschirm.
  9. Umgang mit PostWorkDescriptor 's.
  10. Einige Arbeiten zum Ersetzen von Anzeigezuständen, auf die ich später eingehen werde (im Abschnitt zum SL-Interpreter).
  11. Stoppen Sie den Fluss für eine dynamisch berechnete Zeit, damit die FPS dem angegebenen Wert entspricht (standardmäßig 30).

Hinweis: Möglicherweise stellt sich die Frage: "Warum Eingabefelder rendern, wenn für sie entsprechende Textdeskriptoren erstellt wurden, die einen Schritt früher gerendert werden?" Tatsächlich findet das Rendern in Absatz 7 nicht statt - nur die Parameter des InputDescriptor werden mit den Parametern des InputDescriptor synchronisiert - wie z. B. Sichtbarkeit des Bildschirms, Position, Größe und andere. Dies geschah, wie oben angegeben, aus dem Grund, dass der Benutzer den entsprechenden Eingabedeskriptor nicht direkt mit einem Textdeskriptor steuert und im Allgemeinen nichts darüber weiß.


Es ist zu beachten, dass die Größe und Position der Elemente auf dem Bildschirm nicht in Pixeln, sondern in relativen Größen festgelegt wird - Zahlen von 0 bis 1 (Abbildung unten). Das heißt, die gesamte Breite für das Rendern ist 1 und die gesamte Höhe ist 1 (und sie sind nicht gleich, was ich einige Male vergessen und später bereut habe). Es würde sich auch lohnen, (0,0) als Mittelpunkt festzulegen, und die Breite / Höhe sollte gleich zwei sein, aber aus irgendeinem Grund habe ich es vergessen / nicht darüber nachgedacht. Jedoch hat sogar die Option mit einer Breite / Höhe von 1 das Leben des Questentwicklers vereinfacht.


relatives Koordinatensystem


Ein paar Worte zum System zur Speicherfreigabe.


Jeder Deskriptor hatte eine setDoFree(boolean) Methode, die der Benutzer aufrufen musste, um den angegebenen Deskriptor zu zerstören. Die Speicherbereinigung für Deskriptoren eines bestimmten Typs erfolgte unmittelbar nach der Verarbeitung aller Deskriptoren dieses Typs. Außerdem wurde einmal wiedergegebenes Audio nach Beendigung der Wiedergabe automatisch gelöscht. Genau das Gleiche wie eine Animation ohne Schleife.


So können Sie im Moment alles zeichnen, was Sie wollen, aber dies ist nicht das obige Bild, auf dem sich nur ein Hintergrund, der Haupttext und ein Eingabefeld befinden. Und hier kommt der Wrapper über das Display, der der Klasse DefaultDisplayToolkit .


Bei der Initialisierung werden lediglich Deskriptoren für den Hintergrund, den Text usw. zur Anzeige hinzugefügt und es wird auch die Anzeige von Nachrichten mit dem optionalen Symbol, dem Eingabefeld und dem Rückruf erläutert.


Dann tauchte ein kleiner Fehler auf, dessen vollständige Korrektur das Wiederherstellen der Hälfte des Renderingsystems erfordern würde: Wenn Sie sich die Renderreihenfolge in der Spieleschleife ansehen, sehen Sie, dass zuerst die Bilder und dann erst der Text gezeichnet werden. Wenn das Toolkit das Bild anzeigt, wird es gleichzeitig in Breite und Höhe in der Mitte des Bildschirms platziert. Wenn die Nachricht viel Text enthält, sollte dieser den Haupttext der Szene teilweise überlappen. Da der Nachrichtenhintergrund jedoch ein Bild ist (vollständig schwarz, aber dennoch) und die Bilder vor dem Text gezeichnet werden, wird ein Text einem anderen überlagert (Abbildung unten). Das Problem wurde teilweise durch vertikale Zentrierung nicht auf dem Bildschirm, sondern im Bereich über dem Haupttext gelöst. Eine vollständige Lösung würde die Einführung eines Tiefenparameters und das Wiederherstellen der Renderer von dem Wort "vollständig" umfassen.


Overlay-Demonstration

Text überlappend


Vielleicht geht es hier letztendlich um die Anzeige von allem. Sie können zu der Sprache übergehen, mit der die gesamte API arbeitet, die im sl Paket enthalten ist.


3. Skriptsprache


Hinweis: Wenn der angesehene% USERNAME% es hier liest, hat er es gut gemacht, und ich würde ihn bitten, nicht damit aufzuhören: Jetzt wird es viel interessanter als zuvor.


3.1. Sprache


Zunächst wollte ich eine deklarative Sprache erstellen, in der nur alle erforderlichen Parameter für die Szene angegeben werden müssen, und das ist alles. Der Motor würde die gesamte Logik übernehmen. Am Ende kam ich jedoch zu der prozeduralen Sprache, selbst mit OOP-Elementen (kaum unterscheidbar), und dies war eine gute Lösung, da sie im Vergleich zur deklarativen Option eine viel größere Flexibilität in der Spielelogik ermöglichte.


Die Sprachsyntax wurde so einfach wie möglich für das Parsen konzipiert, was angesichts der verfügbaren Zeit logisch ist.


Der Code wird also in Textdateien mit der Erweiterung SSF gespeichert. Jede Datei enthält eine Beschreibung einer oder mehrerer Szenen. Jede Szene enthält keine oder mehrere Aktionen. Jede Aktion enthält null oder mehr Operatoren.


Eine kleine Erklärung zu den Begriffen. Eine Aktion ist nur ein Vorgang ohne die Möglichkeit, Argumente weiterzugeben (was die Entwicklung des Spiels in keiner Weise behinderte). Der Operator ist anscheinend nicht ganz das, was dieses Wort in gewöhnlichen Sprachen bedeutet (+, -, /, *), aber die Form ist dieselbe: Der Operator ist die Gesamtheit seines Namens und aller seiner Argumente.


Vielleicht möchten Sie den Quellcode für SL endlich sehen, hier ist er:


 scene dungeon { action init { load_image "background" "dungeon/background.png" load_image "key" "dungeon/key.png" load_audio "background" "dungeon/background.wav" load_audio "got_key" "dungeon/got_key.wav" } action first_come { play "background" loop set_background "background" set_text "some text" add_dialog "(||(|) (||-))" "dial_look_around" dial_look_around on } //some comment action dial_look_around { play "got_key" once show "some text 2" "key" none tag "key" switch_dialog "dial_look_around" off } } 

Jetzt wird klar, was der Bediener ist. Es ist auch ersichtlich, dass jede Aktion ein Anweisungsblock ist (eine Anweisung kann ein Anweisungsblock sein) sowie die Tatsache, dass einzeilige Kommentare unterstützt werden (es war nicht sinnvoll, mehrzeilige Kommentare einzugeben, außerdem habe ich keine einzeiligen verwendet).


Der Einfachheit halber wurde ein solches Konzept als „Variable“ nicht in die Sprache eingeführt; Daher sind alle im Code verwendeten Werte Literale. Je nach Typ werden folgende Literale unterschieden:


Wörtliche Bezeichnung:Hinweis:
String-LiteralDie Möglichkeit, Anführungszeichen und Schrägstriche (\\) zu umgehen, ist enthalten. Sie können auch einen Bindestrich in den Text einfügen
Ganzzahliges LiteralUnterstützt negative Zahlen
Gleitkomma-LiteralUnterstützt negative Zahlen
Keine wörtlicheDer Code wird als none
Boolesches LiteralIm Code - on / off für wahr / falsch
Allgemeiner WortlautWenn ein Literal nicht in einen der oben genannten Typen fällt und aus den Buchstaben des englischen Alphabets, den Zahlen und dem Unterstrich besteht, handelt es sich um ein allgemeines Literal.

Ein paar Worte zur Sprachanalyse. Es gibt verschiedene Ebenen zum "Laden" des Codes (Abbildung unten):


  1. Ein Tokenizer ist eine modulare Klasse zum Aufteilen von Quellcode in Token (die minimalen semantischen Einheiten der Sprache). Jeder Tokentyp ist mit einer Nummer verknüpft - seinem Typ. Warum modular? Denn die Teile des Tokenizers, die prüfen, ob ein Teil des Quellcodes ein Token eines bestimmten Typs ist, werden vom Tokenizer isoliert und von außen heruntergeladen (aus dem zweiten Absatz).
  2. Das Tokenizer-Add-On ist eine Klasse, die das Erscheinungsbild jedes Tokentyps in SL definiert. Auf der unteren Ebene wird ein Tokenizer verwendet. Auch hier ist die Überprüfung von Leerzeichen und das Weglassen von einzeiligen Kommentaren. Die Ausgabe liefert einen sauberen Strom von Token, die in ...
  3. ... ein Parser (auch modular), der am Ausgang einen abstrakten Syntaxbaum erzeugt. Modular - weil es von sich aus nur Szenen und Aktionen analysieren kann, aber nicht weiß, wie Operatoren analysiert werden. Daher werden Module in den Konstruktor geladen (tatsächlich lädt er sie selbst in den Konstruktor, was nicht sehr gut ist), wodurch jeder seiner Operatoren analysiert werden kann.

Pipeline der Skriptsprachenanalyse


Nun kurz zu den Operatoren, damit eine Vorstellung von der Funktionalität der Sprache entsteht. Ursprünglich gab es 11 Betreiber, die das Spiel durchdachten, von denen einige zu einem verschmolzen, andere geändert und weitere 9 hinzugefügt wurden. Hier ist die Übersichtstabelle:


Name des Betreibers:Hinweis:
load_imageLädt ein Bild in den Speicher
load_audioLädt Audio in den Speicher
set_textLegt den Haupttext der Szene fest.
set_backgroundLegt den Hintergrund für die Szene fest.
playSpielt Audio ab (kann einmal oder in einer Schleife abgespielt werden)
showZeigt eine Nachricht mit einem Rückruf (in SL) und einem optionalen Bild an (das aufgerufen wird, nachdem der Benutzer die Nachricht geschlossen hat).
tagSetzt ein Tag (Label). Es kann als globale Variable mit dem angegebenen Namen betrachtet werden, die keinen Wert speichert. Dies ist in verschiedenen Fällen hilfreich: Sie können beispielsweise so markieren, ob der Spieler den Schlüssel zur Tür gefunden hat, ob er sich bereits an diesem Ort befunden hat usw.
if_tag / if_tag_nVerzweigungsoperatoren, mit denen Sie etwas tun können, je nachdem, ob das entsprechende Tag installiert ist. else branch wird unterstützt. if_tag wird ausgeführt, wenn das Tag gesetzt ist, if_tag_n - umgekehrt
add_dialogErmöglicht das Hinzufügen eines Dialogs zur Szene. Über sie etwas später
gotoÜbergang zu einer anderen Szene
callBenutzerdefinierter Aktionsaufruf
call_externEine Aktion aus einer anderen Szene aufrufen.
stop_allStoppen Sie die Wiedergabe aller Sounds / Musik
show_motionZeigt das Bild an und verschiebt es in der angegebenen Zeit (Dauer) von einem Punkt zu einem anderen (mit einem optionalen Rückruf, der aufgerufen wird, wenn die Bewegung endet)
animateAnimiert ein zuvor durch show_motion gezeigtes show_motion . Aus den Optionen: Sie können die Art der Animation angeben - v_motion / h_motion (vertikale / horizontale Bewegung nach Funktion) 2t33t2), die Möglichkeit der Schleife, eine Angabe der Zeit (Dauer), für die die Animation abgespielt werden soll. Es ist möglich, einen numerischen Wert (einen, da es keine Möglichkeit gibt, eine variable Anzahl von Argumenten zu übergeben, sodass dies teilweise eine Krücke ist) an eine Animation (für jede Animation bedeutet dies unterschiedliche Dinge) und einen optionalen Rückruf (der beim Abspielen der Animation aufgerufen wird) zu senden.

Operatoren für die Arbeit mit Zählern - Szenenspezifische Ganzzahlvariablen.


Name des Betreibers:Hinweis:
counter_setErstellen eines Zählers und Initialisieren mit einem bestimmten Wert
counter_addEinem Zähler einen Wert hinzufügen
if_counter <modifier>Kann die Werte von zwei Zählern / Zahlen / Zahlen und einem Zähler vergleichen. <modifier> ist ein allgemeines Literal und hat die Form eq/gr/ls[_n] , wobei eq gleich ist, gr größer ist als, ls kleiner ist als, _n Negation ist (z. B. ist gr_n nicht größer). Wie Sie sehen, wurde alles so weit wie möglich vereinfacht.

Es gab auch den Gedanken, eine return einzuführen (die entsprechende Funktionalität wurde sogar auf der Kernebene des Interpreters hinzugefügt), aber ich habe es vergessen und es war nicht nützlich.


, , : show_motion (, , 0.01) duration .


, (lookup) ( ): ///, load_audio / load_image / counter_set / add_dialog . , , , , — . . , . , : " scene_coast.dialog_1 " — dialog_1 scene_coast .


SL-, . , , , — . : (-, ), , lookup ', , , . , goto lookup ', .


- — - , , n ( ) . , , n . , .


. :


 add_dialog "regexp" "dialog_name" callback on/off 

, . , : , , , ( ).


, , ( ) ( ) , ( ). : , , , "" "".


dialogs system work


, ( , )


, "":


 (||((|||) ( )?(||)?)|(||)( )?|  ) 

***


, : — , , — .


, :


3.2.


: , — "" ( ). .


SL , - . :


  1. init — , ( , , , ).
  2. first_come — , . , , .
  3. , : come — , ( ).

: init first_come — , .


. : , , init -. , ( ) .


, n , first_come - ( - - ). . , : , , first_come come , come ( ). : , , , .


(, "", " ", " " . .). , , - - . , ( ), .


(, , ). : ? , , . provideState , ; , .


, , , , ( , ), (, , , ).


4.


. 2019- 2018-, , , .


4.1.


, , , — . , . ( ), , - , 9 (), - ( , ( , , ) .


, : , , , . , , .


, 25% (5) , : , ; ( animate ), ( call_extern ).


, - ( ), (, , — , "You won").


demo visual novel


4.2. Grafik


, :


stubs for graphics


, , - - " ". :


  • (4x2.23''), .
  • : , , — .
  • ////etc.

usage of art brushes


  • , , .

a character from the visual novel


  • , . , , , . .

5.


( 11 ) 30 40 . 9 4 55 . ( ) 7 41 . — ~4-6 ( 45 ).


: "Darkyen's Time Tracker" JetBrains ( ).
: 2 , — . 45 8 .


: 4777, ( ) — 637.


: cloc .


30 . ( ) : — ~8 , — ~24 , ( ) — ~8 . .


— 232 ( - , WAV).


WAV?

javax.sound.sampled.AudioSystem , WAV AU , WAV.


28 ( 3 ). — 17 /.


- : , . , , " ", " ". (, ), ( ""/"" - ).


?

— , . : . Langweilig. , , "" : NPC, , (, — ..).


, : , .


— . , : , , , . . , , , , , .


. ( ), :


  • . . .
  • . , .
  • , .

, , .


GitHub .


(assets) "Releases" "v1.0" .

Source: https://habr.com/ru/post/de483818/


All Articles