Moteur, langage de script et roman visuel - en 45 heures

roman visuel, moteur et langage de script pendant 45 heures


Salutations. Il se trouve que pendant trois années consécutives, comme cadeau pour la nouvelle année à certaines personnes, j'ai créé un jeu. En 2018, c'était un jeu de plateforme avec des éléments de puzzle, à propos duquel j'ai écrit sur un hub En 2019 - un réseau RTS pour deux joueurs, sur lequel je n'ai rien écrit. Et enfin, dans le 2020e - une courte histoire visuelle, qui sera discutée plus tard, créée dans un temps très limité.


Dans cet article:


  • conception et implémentation du moteur de nouvelles visuelles,
  • un jeu avec un tracé non linéaire en 8 heures,
  • suppression de la logique du jeu dans des scripts dans leur propre langue.

Intéressant? Alors bienvenue au chat.


Attention: il y a beaucoup de texte et ~ 3,5 Mo d'images


Contenu:


0. Justification du développement du moteur.


  1. Le choix de la plateforme.
  2. Architecture du moteur et sa mise en œuvre:
    2.1. Énoncé du problème.
    2.2. Architecture et implémentation.
  3. Langage de script:
    3.1. Langue.
    3.2. Interprète
  4. Développement de jeux:
    4.1. L'histoire et le développement de la logique du jeu.
    4.2. Graphisme
  5. Statistiques et résultats.

Remarque: Si, pour une raison quelconque, vous n'êtes pas intéressé par les détails techniques, vous pouvez passer directement à l'étape 4 "Développement de jeu", mais vous ignorerez la majeure partie du contenu.


0. Justification du développement du moteur


Bien sûr, il existe un grand nombre de moteurs prêts à l'emploi pour les histoires visuelles, qui, sans aucun doute, sont meilleurs que la solution décrite ci-dessous. Cependant, peu importe le genre de programmeur que j'étais, si je n'en avais pas écrit un autre. Imaginons donc que son développement soit justifié.


1. Sélection de la plateforme


En fait, le choix était restreint: Java ou C ++. Sans y réfléchir à deux fois, j'ai décidé de mettre en œuvre mon plan en Java, car pour un développement rapide, il offre toutes les possibilités (à savoir, la gestion automatique de la mémoire et une plus grande simplicité par rapport au C ++, qui cache beaucoup de détails de bas niveau et, par conséquent, permet de moins mettre l'accent sur le langage lui-même et de ne penser qu'à la logique métier), et prend également en charge les fenêtres, les graphiques et l'audio prêts à l'emploi.


Swing a été choisi pour implémenter l'interface graphique, car j'ai utilisé Java 13, où JavaFX ne fait plus partie de la bibliothèque, et ajouter des dizaines de mégaoctets d'OpenJFX en fonction de cela était trop paresseux. Ce n'était peut-être pas la meilleure solution, mais néanmoins.


La question se pose probablement: de quel type de moteur de jeu s'agit-il, mais sans accélération matérielle? La réponse réside dans le manque de temps pour faire face à OpenGL, ainsi que son absence de sens absolu: le FPS n'est pas important pour un roman visuel (en tout cas, avec autant d'animation et de graphisme que dans ce cas).


2. L'architecture du moteur et sa mise en œuvre


2.1 énoncé du problème


Pour décider comment faire quelque chose, vous devez décider pourquoi. C'est moi sur l'énoncé du problème, car l'architecture n'est pas universelle, mais un moteur "spécifique au domaine", par définition, dépend directement du jeu envisagé.


Par moteur universel, je comprends le moteur qui prend en charge des concepts de relativement bas niveau, tels que "Game Object", "Scene", "Component". Il a été décidé de ne pas en faire un moteur universel, car cela réduirait considérablement le temps de développement.


Comme prévu, le jeu devrait comprendre les parties suivantes:


structure du jeu de roman visuel


Autrement dit, il y a un arrière-plan pour chaque scène, le texte principal, ainsi qu'un champ de texte pour la saisie utilisateur (le roman visuel a été pensé précisément avec une saisie utilisateur arbitraire, et pas un choix parmi les options proposées, comme c'est souvent le cas. Plus tard, je vais vous expliquer pourquoi il était mauvais décision). Le diagramme montre également qu'il peut y avoir plusieurs scènes dans le jeu et, par conséquent, des transitions peuvent être effectuées entre elles.


Remarque: Par scène, je veux dire la partie logique du jeu. Le critère de la scène peut être le même arrière-plan tout au long de cette partie.


Parmi les exigences du moteur figurait également la possibilité de lire des messages audio et d'affichage (avec la fonction d'entrée utilisateur en option).


Le désir le plus important était peut-être le désir d'écrire la logique du jeu non pas en Java, mais dans un langage déclaratif simple.


Il y avait également un désir de réaliser la possibilité d'une animation procédurale, à savoir le mouvement élémentaire des images, de sorte qu'au niveau Java, il serait possible de déterminer la fonction par laquelle la vitesse actuelle du mouvement est prise en compte (par exemple, pour que le graphique de vitesse soit droit, ou sinusoïde, ou autre).


Comme prévu, toutes les interactions avec les utilisateurs devaient se faire par le biais d'un système de dialogues. Dans ce cas, le dialogue n'était pas nécessairement considéré comme un dialogue avec le NPC ou quelque chose de similaire, mais généralement comme une réaction à toute entrée utilisateur pour laquelle le gestionnaire correspondant était enregistré. Pas clair Cela deviendra plus clair bientôt.


2.2. Architecture et implémentation


Compte tenu de tout ce qui précède, vous pouvez diviser le moteur en trois parties relativement grandes qui correspondent aux mêmes packages java:


  1. display - contient tout ce qui concerne la sortie à l'utilisateur de toute information (graphique, texte et son), ainsi que la réception de données de sa part. Une sorte de (Voir), si on parle de MVC / MVP / etc.
  2. initializer - contient les classes dans lesquelles le moteur est initialisé et lancé.
  3. sl - contient des outils pour travailler avec le langage de script (ci-après - SL).

Dans ce paragraphe, je considérerai les deux premières parties. Je vais commencer par le second.


La classe d'initialisation a deux méthodes principales: initialize() et run() . Initialement, le contrôle vient de la classe du lanceur, d'où initialize() appelé initialize() . Après l'appel, l'initialiseur analyse les paramètres passés au programme (le chemin d'accès au répertoire avec les quêtes et le nom de la quête à exécuter), charge le manifeste de la quête sélectionnée (à ce sujet un peu plus tard), initialise l'affichage, vérifie si la version linguistique (SL) requise par la quête est prise en charge par les données interprète, et enfin, lance un thread séparé pour la console du développeur.


Immédiatement après cela, si tout s'est bien passé, le lanceur appelle la méthode run() , qui déclenche le chargement réel de la quête. Tout d'abord, il y a tous les scripts liés à la quête téléchargée (à propos de la structure du fichier de quête - ci-dessous), ils sont fournis à l'analyseur, dont le résultat est donné à l'interpréteur. Ensuite, l'initialisation de toutes les scènes est lancée et l'initialiseur termine l'exécution de son flux, pour finalement suspendre le gestionnaire de touches Entrée à l'écran. Et donc, lorsque l'utilisateur appuie sur Entrée, la première scène est chargée, mais plus sur cela plus tard.


La structure des fichiers de la quête est la suivante:


structure des fichiers de quête


Il y a un dossier séparé pour la quête, à la racine duquel se trouve le manifeste, ainsi que trois dossiers supplémentaires: audio - pour le son, graphics - pour la partie visuelle et les scenes - pour les scripts qui décrivent les scènes.


Je voudrais décrire brièvement le manifeste. Il contient les champs suivants:


  • sl_version_req - Version SL nécessaire pour démarrer la quête,
  • init_scene - le nom de la scène à partir de laquelle la quête commence,
  • quest_name - un beau nom de quête qui apparaît dans le titre de la fenêtre,
  • resolution - la résolution de l'écran auquel la quête est destinée (quelques mots à ce sujet plus tard),
  • font_size - taille de police pour tout le texte,
  • font_name est le nom de la police pour tout le texte.

Il convient de noter que lors de l'initialisation de l'affichage, entre autres choses, le calcul de la résolution de rendu a été effectué: c'est-à-dire que la résolution requise a été prise du manifeste et insérée dans l'espace disponible pour la fenêtre afin que:


  • le rapport d'aspect est resté le même que dans la résolution du manifeste,
  • tout l'espace disponible était occupé en largeur ou en hauteur.
    Grâce à cela, le développeur de la quête peut être sûr que ses images, par exemple 16: 9, seront affichées sur n'importe quel écran dans ce rapport.

De plus, lorsque l'affichage est initialisé, le curseur est masqué, car il n'est pas impliqué dans le gameplay.


En bref sur la console développeur. Il a été développé pour les raisons suivantes:


  1. Pour le débogage.
  2. Si quelque chose ne va pas pendant le jeu, il peut être corrigé via la console du développeur.
    Il n'a implémenté que quelques commandes, à savoir: la sortie des descripteurs d'un type spécifique et de leur état, la sortie des threads de travail, le redémarrage de l'affichage et la commande la plus importante - exec , qui a permis d'exécuter n'importe quel code SL dans la scène actuelle.

Ceci termine la description de l'initialiseur et des choses connexes, et nous pouvons passer à la description de l'affichage.


Sa structure finale est la suivante:


structure d'affichage


De l'énoncé du problème, nous pouvons conclure que tout ce qui devra être fait est de dessiner des images, dessiner du texte, lire du son.


Comment le texte / l'image est-il généralement dessiné dans les moteurs universels et au-delà? Il existe une méthode de type update() , appelée chaque tick / step / frame / render / frame / etc et dans laquelle il y a un appel à une méthode de type drawText() / drawImage() - cela garantit l'apparence du texte / image dans ce cadre. Cependant, dès que l'appel à de telles méthodes s'arrête, le rendu des choses correspondantes s'arrête.


Dans mon cas, il a été décidé de faire un peu différent. Étant donné que pour les romans visuels, le texte et les images sont relativement permanents et sont presque tout ce que l'utilisateur voit (c'est-à-dire qu'ils sont suffisamment importants), ils ont été créés comme des objets de jeu - c'est-à-dire des choses que vous avez juste besoin d'apparaître et elles ne disparaîtront pas jusqu'à ce que vous leur demandiez. De plus, cette solution a simplifié la mise en œuvre.


Un objet (du point de vue de la POO) qui décrit le texte / l'image est appelé un descripteur. Autrement dit, pour l'utilisateur du moteur d'API, seuls des descripteurs peuvent être ajoutés à l'état d'affichage et supprimés de celui-ci. Ainsi, dans la version finale de l'affichage, il y a les descripteurs suivants (ils correspondent aux classes du même nom):


Nom du descripteur:Description du descripteur:
ImageDescriptorDescripteur d'image: contient l'image ( BufferedImage ), position sur l'écran et largeur avec hauteur. Cependant, lors de la création, seule la largeur est définie - la hauteur est calculée proportionnellement à la hauteur d'origine (et il n'y a aucun moyen d'étirer / compresser manuellement l'image de manière disproportionnée, donc c'était une mauvaise décision).
TextDescriptorDescripteur de texte: contient le texte, sa position et sa taille. De plus, le texte n'est pas mis à l'échelle en largeur et en hauteur, mais est coupé lorsque vous allez au-delà des bordures. Le texte peut être transféré par syllabes et simplement par des espaces, et défiler également par les lignes.
AudioDescriptorUn descripteur audio qui peut lire l'audio (une fois ou en boucle), le mettre en pause et le supprimer.
AnimationDescriptorUne poignée d'animation qui, comme l'audio, peut être bouclée. Contient un objet qui implémente l'animation et un descripteur d'image pour lequel l'animation est lue. La méthode principale est update(long) , qui prend le nombre de millisecondes qui se sont écoulées depuis le dernier appel update(long) . Leur nombre est utilisé pour calculer l'état actuel de l'animation.
InputDescriptorDescripteur d'entrée: est un champ de texte dans lequel le curseur est toujours à la fin du texte. Il convient également de noter que le stockage et le rendu du texte à partir du descripteur d'entrée sont effectués via un descripteur de texte créé implicitement, afin de ne pas dupliquer la logique. Le plus drôle, c'est que j'ai pris en compte la possibilité d'appuyer sur Retour arrière, mais je n'ai pas pris en compte Supprimer; et lorsque Supprimer était toujours enfoncé pendant le jeu, ▯▯▯ apparaissait dans le champ, car aucun traitement n'avait été effectué pour Supprimer et le personnage tentait de s'afficher sous forme de texte.
KeyAwaitDescriptorUn handle vers lequel la touche nécessaire est passée (depuis KeyEvent ) et un rappel avec une certaine logique qui sera lancée lorsque la touche correspondante sera enfoncée.
PostWorkDescriptorUn handle qui accepte un rappel qui sera appelé après le traitement de chaque tick.

L'affichage contient également des champs pour le récepteur d'entrée actuel (descripteur d'entrée) et un champ indiquant quel descripteur de texte a maintenant le focus et dont le texte défilera sous les actions correspondantes de la part de l'utilisateur.


Le cycle de jeu ressemble à ceci:


  1. Traitement audio - appel de la méthode update() sur les descripteurs audio, qui vérifie l'état actuel de l'audio, libère de la mémoire (si nécessaire) et effectue d'autres travaux techniques.
  2. Traitement des frappes - transférez les caractères saisis vers un descripteur pour la réception de l'entrée, le traitement des frappes pour les touches de défilement (flèches haut et bas) et Retour arrière.
  3. Traitement d'animation.
  4. Effacement de l'arrière-plan dans le tampon de rendu ( BufferedImage servi de tampon).
  5. Dessiner des images.
  6. Rendu de texte.
  7. Dessiner des champs pour la saisie.
  8. La sortie du tampon à l'écran.
  9. Gestion des PostWorkDescriptor .
  10. Certains travaux sur le remplacement des états d'affichage, dont je parlerai plus tard (dans la section sur l'interpréteur SL).
  11. Arrêtez le flux pendant une durée calculée dynamiquement afin que le FPS soit égal à la valeur spécifiée (30 par défaut).

Remarque: Peut-être que la question se pose: "Pourquoi rendre les champs de saisie si des descripteurs de texte appropriés ont été créés pour eux et seront rendus une étape plus tôt?" En fait, le rendu du paragraphe 7 ne se produit pas - seuls les InputDescriptor de InputDescriptor sont synchronisés avec les InputDescriptor de InputDescriptor - tels que la visibilité à l'écran, la position, la taille et autres. Cela a été fait, comme indiqué ci-dessus, parce que l'utilisateur ne contrôle pas directement le descripteur d'entrée correspondant avec un descripteur de texte et ne sait généralement rien à ce sujet.


Il convient de noter que la taille et la position des éléments sur l'écran sont définies non pas en pixels, mais en tailles relatives - des nombres de 0 à 1 (diagramme ci-dessous). Autrement dit, toute la largeur pour le rendu est 1, et toute la hauteur est 1 (et ils ne sont pas égaux, ce que j'ai oublié plusieurs fois et regretté plus tard). Il serait également intéressant de faire de (0,0) le centre, et la largeur / hauteur devrait être égale à deux, mais pour une raison quelconque, j'ai oublié / n'y ai pas pensé. Cependant, même l'option avec une largeur / hauteur égale à 1 a simplifié la vie du développeur de quête.


système de coordonnées relatif


Quelques mots sur le système de libération de mémoire.


Chaque descripteur avait une setDoFree(boolean) , que l'utilisateur devait appeler s'il voulait détruire le descripteur donné. La récupération de place pour les descripteurs d'un certain type s'est produite immédiatement après le traitement de tous les descripteurs de ce type. De plus, l'audio qui a été lu une fois a été automatiquement supprimé une fois la lecture terminée. Exactement la même chose que l'animation sans boucle.


Ainsi, pour le moment, vous pouvez dessiner tout ce que vous voulez, mais ce n'est pas l'image ci-dessus, sur laquelle il n'y a qu'un arrière-plan, le texte principal et un champ de saisie. Et voici le wrapper sur l'affichage, qui correspond à la classe DefaultDisplayToolkit .


Lorsqu'il est initialisé, il ajoute simplement des descripteurs pour l'arrière-plan, le texte, etc. à l'affichage. Il sait également comment afficher les messages avec l'icône facultative, le champ de saisie et le rappel.


Puis un petit bug est apparu, dont une correction complète nécessiterait de refaire la moitié du système de rendu: si vous regardez l'ordre de rendu dans la boucle de jeu, vous pouvez voir que les images sont dessinées en premier et seulement ensuite le texte. En même temps, lorsque la boîte à outils affiche l'image, elle la place au milieu de l'écran en largeur et en hauteur . Et s'il y a beaucoup de texte dans le message, il doit chevaucher partiellement le texte principal de la scène. Cependant, étant donné que l'arrière-plan du message est une image (complètement noire, mais néanmoins) et que les images sont dessinées avant le texte, un texte est superposé à un autre (capture d'écran ci-dessous). Le problème a été partiellement résolu par un centrage vertical non pas sur l'écran, mais dans la zone au-dessus du texte principal. Une solution complète consisterait à introduire un paramètre de profondeur et à refaire les rendus à partir du mot «complètement».


Démonstration de superposition

chevauchement de texte


Peut-être s'agit-il de l'affichage, enfin, de tout. Vous pouvez passer à la langue, l'intégralité de l'API pour travailler avec qui est contenue dans le package sl .


3. Langage de script


Remarque: Si le respecté% USERNAME% le lit ici, il a bien fait, et je lui demanderais de ne pas arrêter de le faire: maintenant ce sera beaucoup plus intéressant qu'auparavant.


3.1. La langue


Au départ, je voulais faire un langage déclaratif dans lequel il suffirait d’indiquer tous les paramètres nécessaires à la scène, et c’est tout. Le moteur prendrait toute la logique. Cependant, à la fin, je suis arrivé au langage procédural, même avec des éléments OOP (à peine distinguables), et c'était une bonne solution, car, par rapport à l'option déclarative, cela permettait beaucoup plus de flexibilité dans la logique du jeu.


La syntaxe du langage a été pensée de manière à être aussi simple que possible pour l'analyse, ce qui est logique, compte tenu du temps disponible.


Ainsi, le code est stocké dans des fichiers texte avec l'extension SSF; chaque fichier contient une description d'une ou plusieurs scènes; chaque scène contient zéro ou plusieurs actions; chaque action contient zéro ou plusieurs opérateurs.


Une petite explication sur les termes. Une action n'est qu'une procédure sans possibilité de passer des arguments (en aucun cas empêché le développement du jeu). L'opérateur n'est apparemment pas tout à fait ce que ce mot signifie dans les langues ordinaires (+, -, /, *), mais la forme est la même: l'opérateur est la totalité de son nom et de tous ses arguments.


Peut-être que vous avez hâte de voir enfin le code source de SL, le voici:


 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 } } 

Maintenant, il devient clair ce qu'est l'opérateur. On voit également que chaque action est un bloc d'instructions (une instruction peut être un bloc d'instructions), ainsi que le fait que les commentaires sur une seule ligne sont pris en charge (cela n'avait pas de sens de saisir des commentaires sur plusieurs lignes, d'ailleurs, je n'ai pas utilisé ceux sur une seule ligne).


Par souci de simplification, un concept de «variable» n'a pas été introduit dans le langage; par conséquent, toutes les valeurs utilisées dans le code sont des littéraux. Selon le type, les littéraux suivants sont distingués:


Nom littéral:Remarque:
Littéral de chaîneLa possibilité d'échapper les guillemets et les barres obliques (\\) est incluse, il est également possible d'insérer un trait d'union dans le texte
Entier littéralPrend en charge les nombres négatifs
Littéral en virgule flottantePrend en charge les nombres négatifs
Aucun littéralLe code est représenté comme none
Littéral booléenDans le code - on / off pour vrai / faux, respectivement
Littéral généralSi un littéral n'appartient à aucun des types ci-dessus et se compose des lettres de l'alphabet anglais, des chiffres et du trait de soulignement, il s'agit d'un littéral général.

Quelques mots sur l'analyse de la langue. Il existe plusieurs niveaux de "chargement" du code (schéma ci-dessous):


  1. Un tokenizer est une classe modulaire pour diviser le code source en jetons (les unités sémantiques minimales du langage). Chaque type de jeton est associé à un numéro - son type. Pourquoi modulaire? Parce que les parties du tokenizer qui vérifient si une partie du code source est un token d'un certain type sont isolées du tokenizer et téléchargées de l'extérieur (à partir du deuxième paragraphe).
  2. Le module complémentaire tokenizer est une classe qui définit l'apparence de chaque type de jeton dans SL; au niveau inférieur utilise un tokenizer. Voici également le filtrage des jetons spatiaux et l'omission des commentaires sur une seule ligne. La sortie donne un flux propre de jetons, qui est utilisé dans ...
  3. ... un analyseur (il est également modulaire), qui produit un arbre de syntaxe abstrait à la sortie. Modulaire - car en soi, il ne peut analyser que les scènes et les actions, mais il ne sait pas comment analyser les opérateurs. Par conséquent, des modules y sont chargés (en fait, il les charge lui-même dans le constructeur, ce qui n'est pas très bon), ce qui permet d'analyser chacun de ses opérateurs.

pipeline d'analyse du langage de script


Maintenant, brièvement sur les opérateurs, afin qu'une idée de la fonctionnalité du langage apparaisse. Au départ, il y avait 11 opérateurs, puis en train de réfléchir au jeu, certains d'entre eux ont fusionné en un seul, certains ont changé et 9 autres ont été ajoutés. Voici le tableau récapitulatif:


Nom de l'opérateur:Remarque:
load_imageCharge une image en mémoire
load_audioCharge l'audio en mémoire
set_textDéfinit le texte principal de la scène.
set_backgroundDéfinit l'arrière-plan de la scène.
playLit l'audio (peut jouer une fois ou une lecture en boucle)
showAffiche un message avec un rappel (en SL) et une image facultative (appelée après que l'utilisateur a fermé le message)
tagDéfinit une balise (étiquette). Elle peut être considérée comme une variable globale avec le nom spécifié, qui ne stocke aucune valeur. Ceci est utile dans différents cas: par exemple, vous pouvez marquer de cette manière si le joueur a trouvé la clé de la porte, s'il était déjà à cet endroit, etc.
if_tag / if_tag_nOpérateurs de branchement qui vous permettent de faire quelque chose selon que la balise correspondante est installée. else branche est prise en charge. if_tag est exécuté si la balise est définie, if_tag_n - vice versa
add_dialogVous permet d'ajouter un dialogue à la scène. À leur sujet un peu plus tard
gotoTransition vers une autre scène
callAppel d'action personnalisé
call_externInvoquer une action d'une autre scène.
stop_allArrêtez de jouer tous les sons / musique
show_motionAffiche et déplace l'image d'un point à un autre dans le temps spécifié (durée) (avec un rappel facultatif qui est appelé à la fin du mouvement)
animateAnime une image précédemment affichée via show_motion . A partir des options: vous pouvez spécifier le type d'animation - v_motion / h_motion (mouvement vertical / horizontal par fonction 2t ^ 3-3t ^ 2 $ ), la possibilité de bouclage, une indication de la durée (durée) pendant laquelle l'animation doit être jouée. Il est possible de transmettre une valeur numérique (une, car il n'y a aucun moyen de passer un nombre variable d'arguments, donc c'est en partie une béquille) à une animation (pour chaque animation cela signifie des choses différentes) et un rappel optionnel (appelé lorsque l'animation est jouée).

Opérateurs pour travailler avec des compteurs - variables entières spécifiques à la scène.


Nom de l'opérateur:Remarque:
counter_setCréer un compteur et l'initialiser avec une certaine valeur
counter_addAjout d'une valeur à un compteur
if_counter <modifier>Capable de comparer les valeurs de deux compteurs / nombres / nombres et d'un compteur. <modifier> est un littéral général et a la forme eq/gr/ls[_n] , où eq sont égaux, gr est supérieur à, ls est inférieur à, _n est la négation (par exemple, gr_n n'est pas supérieur). Comme vous pouvez le voir, tout a été simplifié autant que possible.

Il y avait aussi l'idée d'introduire une return (la fonctionnalité correspondante a même été ajoutée au niveau de base de l'interpréteur), mais j'ai oublié, et ce n'était pas utile.


, , : 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. Graphisme


, :


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 /.


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


?

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


, : , .


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


. ( ), :


  • . Pas du tout. .
  • . , .
  • , .

, , .


GitHub .


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

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


All Articles