Utiliser Loreline avec Godot
Loreline fournit un plugin GDExtension pour Godot 4.2+. Ce guide montre comment configurer votre projet, charger un script .lor, et gérer les dialogues, les choix et la fin du script en GDScript.
Installation
Téléchargez loreline-godot.zip (v0.9.0). L'archive contient :
addons/loreline/: le plugin GDExtension avec les binaires natifs pour toutes les plateformes (macOS, Windows, Linux, Android, iOS, Web)sample/: un projet Godot complet démontrant l'intégration avec UI, animations et sortie stylisée
Pour ajouter Loreline à votre projet, copiez le dossier addons/loreline/ dans le répertoire addons/ de votre propre projet Godot.
Charger un script
Récupérez l'instance partagée de Loreline, puis utilisez await loreline.parse() avec le chemin de la ressource vers votre fichier .lor :
var loreline: Loreline = Loreline.shared()
func _ready() -> void:
var script = await loreline.parse("res://story/CoffeeShop.lor")
if script == null:
push_error("Échec du parsing de CoffeeShop.lor")
return
loreline.play(script, _on_dialogue, _on_choice, _on_finished)
parse() retourne un Signal que vous pouvez await. Le signal se déclenche une fois le parsing et tous les imports résolus, avec le script parsé (ou null en cas d'erreur de parsing).
Passer un chemin res:// ou user:// comme premier argument est un raccourci : Loreline lit le fichier pour vous. Si vous voulez passer du contenu source brut, voir « Chargement personnalisé » ci-dessous.
Chargement personnalisé
Si vous avez besoin de contrôler la façon dont les fichiers sont chargés (fichiers chiffrés, ressources réseau, etc.), chargez la source vous-même et fournissez un callback de fichier. Le callback de fichier est un Callable qui prend (path, provide) et appelle provide.call(content) avec le contenu du fichier (ou provide.call(null) si le fichier est introuvable) :
func _ready() -> void:
var file := FileAccess.open("res://story/CoffeeShop.lor", FileAccess.READ)
var source := file.get_as_text()
file.close()
var script = await loreline.parse(
source, "res://story/CoffeeShop.lor", _handle_file)
loreline.play(script, _on_dialogue, _on_choice, _on_finished)
func _handle_file(path: String, provide: Callable) -> void:
if FileAccess.file_exists(path):
var f := FileAccess.open(path, FileAccess.READ)
provide.call(f.get_as_text())
else:
provide.call(null)
Le callback peut répondre de manière synchrone comme ci-dessus, ou plus tard. Par exemple, vous pourriez récupérer un fichier via le réseau et appeler provide.call(...) une fois la réponse reçue. parse() ne se termine pas tant que chaque import n'a pas reçu une réponse, mais chaque demande doit être satisfaite exactement une fois.
Gérer les dialogues
Lancez la lecture en appelant loreline.play() avec le script parsé et vos callbacks :
loreline.play(script_data, _on_dialogue, _on_choice, _on_finished)
Le callback de dialogue reçoit l'interpréteur, l'identifiant du personnage, le texte, un tableau de tags et un callable pour faire avancer le script. Appelez advance.call() pour passer à la ligne suivante :
func _on_dialogue(interp: LorelineInterpreter, character: String, text: String, tags: Array, advance: Callable) -> void:
if character != "":
var display_name: String = interp.get_character_field(character, "name")
if display_name != "":
character = display_name
print(character + " : " + text)
else:
print(text)
advance.call()
Gérer les choix
Le callback de choix reçoit l'interpréteur, un tableau de dictionnaires d'options et un callable pour sélectionner une option. Chaque option a un champ "text" et un champ "enabled". Appelez select.call(index) avec l'index du choix sélectionné :
func _on_choice(_interp: LorelineInterpreter, options: Array, select: Callable) -> void:
var enabled_indices: Array[int] = []
for i in range(options.size()):
if options[i]["enabled"]:
enabled_indices.append(i)
print(" [" + str(enabled_indices.size()) + "] " + options[i]["text"])
# Dans un vrai projet, attendez ici l'entrée du joueur.
# Pour cet exemple, on sélectionne automatiquement le premier choix disponible :
select.call(enabled_indices[0])
Gérer la fin du script
Le callback de fin est appelé quand le script atteint sa fin :
func _on_finished(_interp: LorelineInterpreter) -> void:
print("--- Fin ---")
Démarrer depuis un beat spécifique
Par défaut, play() démarre depuis le début du script. Pour démarrer depuis un beat spécifique, passez son nom :
loreline.play(script_data, _on_dialogue, _on_choice, _on_finished, "MorningScene")
Options de l'interpréteur
Vous pouvez passer des options supplémentaires à play() pour enregistrer des fonctions personnalisées ou appliquer des traductions :
var options := LorelineOptions.new()
options.set_function("roll", func(interp, args): return randi_range(1, int(args[0])))
loreline.play(script_data, _on_dialogue, _on_choice, _on_finished, null, options)
Fonctions personnalisées asynchrones
Utilisez set_async_function() lorsque votre fonction personnalisée doit attendre quelque chose (un signal, un minuteur, une réponse HTTP) avant que le script continue. La Callable reçoit un argument supplémentaire resolve: Callable ; appelez resolve.call() pour reprendre l'interpréteur.
beat start
Récupération de votre score...
fetchScore()
Votre score est de $score.
var options := LorelineOptions.new()
options.set_async_function("fetchScore", _fetch_score)
loreline.play(script_data, _on_dialogue, _on_choice, _on_finished, "", options)
func _fetch_score(interp: LorelineInterpreter, _args: Array, resolve: Callable) -> void:
await get_tree().create_timer(2.0).timeout
interp.set_top_level_state_field("score", 42)
resolve.call()
L'interpréteur est en pause jusqu'à ce que resolve.call() soit appelé, puis reprend avec la ligne suivante.
Durée de vie. Conserver l'un de advance, select, resolve, ou votre variable interp garde l'interpréteur en vie. Si vous les relâchez tous, l'interpréteur est libéré, même en plein jeu. Dans l'exemple ci-dessus, l'await maintient la fonction (et donc resolve) en vie pendant la pause de 2 secondes, ce qui maintient l'interpréteur en vie. Si vous relâchez resolve sans l'appeler, l'appel asynchrone est annulé proprement et l'interpréteur est libéré.
Les fonctions asynchrones ne peuvent être appelées qu'en position d'instruction dans le script, pas à l'intérieur d'expressions ou d'interpolations.
Localisation
load_locale() parcourt l'arbre des imports du script, trouve le fichier .lang.lor correspondant à côté de chaque fichier importé et vous renvoie un LorelineTranslations que vous pouvez passer à LorelineOptions. Comme parse(), il retourne un Signal que vous pouvez await :
func _ready() -> void:
var script = await loreline.parse("res://story/CoffeeShop.lor")
var translations = await loreline.load_locale("fr", script)
var options := LorelineOptions.new()
options.set_translations(translations)
loreline.play(script, _on_dialogue, _on_choice, _on_finished, "", options)
Les fichiers de traduction absents pour une langue donnée sont silencieusement ignorés, vous pouvez donc livrer des traductions partielles et le texte original servira de repli. Si vous utilisez un callback de fichier personnalisé avec parse(), passez-le aussi comme dernier argument de load_locale().
Sauvegarder et restaurer l'état
Loreline propose deux modes de sauvegarde/restauration côté Godot.
Reprise fraîche. Capturer l'état courant sous forme de chaîne, le persister, puis créer plus tard un nouvel interpréteur à partir de cette chaîne :
# Sauvegarder l'état courant (par ex. dans un fichier)
var save_data: String = interp.save_state()
FileAccess.open("user://save.json", FileAccess.WRITE).store_string(save_data)
# Plus tard : reprendre depuis l'état sauvegardé
var loaded := FileAccess.open("user://save.json", FileAccess.READ).get_as_text()
var script = await loreline.parse("res://story/CoffeeShop.lor")
var resumed := loreline.resume(script, _on_dialogue, _on_choice, _on_finished, loaded)
Restauration sur place. Restaurer un instantané sur un interpréteur existant sans le recréer :
var save_data: String = interp.save_state()
# ... plus tard, sur le même interp ...
interp.restore_state(save_data)
Timing. Une sauvegarde prise pendant un callback de dialogue ou de choix capture l'état de sorte que la reprise re-délivre ce même callback. Le joueur revoit la même ligne (ou les mêmes choix), et appeler advance() depuis là continue normalement. C'est habituellement ce que vous voulez pour une sauvegarde automatique après chaque dialogue.
Exemple complet
Voici un GDScript minimal qui charge et exécute un script Loreline en affichant la sortie dans la console. Attachez ce script à n'importe quel Node :
extends Node
var loreline: Loreline = Loreline.shared()
var awaiting_choice := false
var enabled_indices: Array[int] = []
var pending_select: Callable
func _ready() -> void:
var script_data = await loreline.parse("res://story/CoffeeShop.lor")
if script_data:
loreline.play(script_data, _on_dialogue, _on_choice, _on_finished)
func _on_dialogue(interp: LorelineInterpreter, character: String, text: String, _tags: Array, advance: Callable) -> void:
if character != "":
var display_name: String = interp.get_character_field(character, "name")
if display_name != "":
character = display_name
print(character + " : " + text)
else:
print(text)
print("")
advance.call()
func _on_choice(_interp: LorelineInterpreter, options: Array, select: Callable) -> void:
enabled_indices.clear()
for i in range(options.size()):
if options[i]["enabled"]:
enabled_indices.append(i)
print(" " + str(enabled_indices.size()) + ". " + options[i]["text"])
pending_select = select
awaiting_choice = true
func _unhandled_input(event: InputEvent) -> void:
if not awaiting_choice:
return
if event is InputEventKey and event.pressed:
var num := event.keycode - KEY_1
if num >= 0 and num < enabled_indices.size():
awaiting_choice = false
pending_select.call(enabled_indices[num])
func _on_finished(_interp: LorelineInterpreter) -> void:
print("\n--- Fin ---")
Aller plus loin
Pour un projet Godot complet avec UI, animations et sortie stylisée, le dossier sample/ inclus dans loreline-godot.zip fournit un exemple fonctionnel complet.