Using SMRT-Godot - The dialog system with a fancy name
This post is long due, but I found some motivation after talking to
Nathan(from GDQuest, featured in my list of gamedev
resources).
In here I'll show a simple project to highlight some of the features of
SMRT.
Our basic scene
You can find this project in SMRT's examples folder at the project's github repository, download it and follow along.
Play the scene and interact with the characters before proceeding...
Done?
Let's just go over the basic concept of SMRT: chapters, dialogs and
texts
Texts are a single dialog box worth of text
Dialogs are a collection of texts, they are referenced through
an unique name.
Chapters are a collection of dialogs. They represent the same
concept of chapters in a book: A new point in the story. So they can say
different things, while keeping the same dialog names with just a change
in chapter
We will focus basically on the code for the following:
This is the final scene tree
- World: It will be very simple, just setting the initial conditions for the rest of the scene through the use of Globals
- Transition: A simple transition that will be played at a specific time later on.
- Player's avatar: With simple 8 directions movement.
- NPC node: That can detect if the player entered its interactive area and say things. to the player.
- A trigger: Basically an instance of the npc node with no sprite. It will be initially blocked by an NPC. When the player enters its area, a cutscene will play, and at the end of this cutscene, the chapter will change.
For SMRT to work properly, it should be a child of a CanvasLayer. SMRT also needs a language file, it is a JSON that has info on how it should display messages.
I created an editor for this process, access it by clicking on the following button:
Click on SMRT-Editor
Use the load button and navigate to res://examples/cave to
load another_example.lan
Click in a chapter to display all available dialogs in the dialog's viewer, then click in one of the texts to edit its properties. For actions, you can add, duplicate, edit and delete chapters and dialogs while you can add, duplicate, delete and change the order of the texts with the arrows bellow the text field.
Careful! There is no confirmation for deleting anything. So if you do something you didn't meant to, providing you haven't saved it yet, just load the file again.
The transition
It's setup:
It is a texture of 64x64 pixels that covers the entire viewport, with two keyframes for opacity: It starts at 0 and ends at 1.
The world
The most important line is the forth one. On it, we create a global variable called 'chapter' with the value 'intro'.
In the script for the npcs this value will be used and changed as desired.
Lines 5 and 6 are basically SMRT and a fade transition node respectively.
The player
Just simple movement, our player class will be passive, it will not have the methods to interact with objects by itself, this will be the work of the NPC node.
Take note of line 7: This is how we will identify what character is the player for our NPCs.
extends KinematicBody2D
var walk_speed = 500
var direction = Vector2(0,0)
func _ready():
set_fixed_process(true)
Globals.set("player", self) # We make the player accessible more easily through Globals.
func _fixed_process(delta):
# Simple movement for our char
if Input.is_action_pressed("player_up"):
direction.y = -1
elif Input.is_action_pressed("player_down"):
direction.y = 1
elif Input.is_action_pressed("player_left"):
direction.x = -1
elif Input.is_action_pressed("player_right"):
direction.x = 1
else:
direction = Vector2(0,0)
move(direction*walk_speed*delta)
The NPC
It is a duplicate of the player char with an Area2D for the interactive area.
extends KinematicBody2D
var direction = Vector2(0,0)
export var dialog_name = "FRIEND_TALK"
export var start_at_message = 0
var can_interact = true
func _ready():
get_node("interactive_area").connect("body_enter",self, "on_body_enter")
pass
func on_body_enter(body):
if body == Globals.get("player"): # Check if the character that entered is the player
if dialog_name != null or dialog_name != "" and can_interact: # check if the npc has something to say and we can interact with
can_interact = false # make the npc non-interactive during the talk
# using some variables to shorten the burden of writing
var smrt = Globals.get("dialog")
var chapter = Globals.get("chapter")
Globals.get("player").set_fixed_process(false) # We disable the player while the dialog happen
smrt.show_text(chapter, dialog_name, start_at_message) # Start the dialog
# we can connect to the signal 'dialog_control' and do different things based on the current state of the dialog
if not smrt.is_connected("dialog_control",self,"on_dialog"):
smrt.connect("dialog_control",self,"on_dialog")
yield(smrt,"finished") # wait for smrt to emit the finished signal so we can continue
can_interact = true # re-enable the npc interactiveness
smrt.disconnect("dialog_control",self,"on_dialog")
Globals.get("player").set_fixed_process(true) # And finally, re-enable the player
func on_dialog(info):
# info is a dictionary that sends the following:
# answer: if a question was answered, it will give the index of the button selected, otherwise it is null
# chapter: the chapter currently active
# dialog: the dialog currently active
# last_text_index: the index of the last text that was displayed
# total_text: the number of texts in the currently active dialog
var smrt = Globals.get("dialog") # Let's grab the dialog system into a 4 letter var
# Check for an answer:
if info.chapter == "intro": # it is good practice to also check what chapter and...
if info.dialog == "friend_talk": # the dialog we're in
if info.answer == 0: # There is only one question on this dialog, we check if the player answered "Of course, I am fearless!"
smrt.stop() # We kindly ask SMRT to stop
yield(get_tree(),"idle_frame") # and wait one frame for it to patch things up and quit nicelly
smrt.show_text("intro","friend_talk_positive") # to finally follow it with a new dialog
if test_move(Vector2(128,0)): # We will make the npc go out of the path
move(Vector2(-128,0))
else:
move(Vector2(128,0))
print("changed dialog name from ", get_name())
dialog_name = "friend_talk_positive" # We also change the dialog the npc will talk from now on so when the player interact with it from now on, it will show a new answer.
elif info.dialog == "great_adventure":
var transition = Globals.get("transition") # Grab the transition node
if info.last_text_index == 0: # When the first text finishes
transition.play("fade") # we play a fade-to-black animation
elif info.last_text_index == info.total_text: # when the last dialog has finished
transition.play_backwards("fade") # we play the same animation, backwards
Globals.set("chapter","after_going_there") # change the chapter
yield(transition,"finished") # wait for the transition to end
The variables from line 5 through 7 will dictate what the character will say, based on the language file loaded in SMRT. Exporting them, allow each instance to have its own string. The NPC will be responsible to see if the player entered its interactive area, for that, we need to connect the area2D to the npc.
Doing it with the editor for one or two npcs is fine, but it is better to do it through code, this way we don't need to keep connecting areas to new npcs, This is done at _ready() in line 10.
We connect the interactive_area to the function "on_body_enter" inside the npc itself. This function first checks if the body that entered is the player, then see if the npc has something to say (the dialog_name is not empty nor null) and if it can interact.
If those conditions are met, we make can_interact false so we don't fire the event again until the dialog finishes, grab SMRT and the chapter in small variables(lines 19 and 20), stop the process of the player so he won't be able to move while the dialog happens (line 21) and finally call show_text(chapter, dialog_name, start_at).
With this, we have a basic interaction between player and npc. We can pretty much make them say whatever we want. But the example is far from over...
The npc that is blocking the exit will ask a question and make things happen based on it. This happens thanks to the signal "dialog_control". It can give us important info like the answer a player gave in a question.
At line 24, we check if the signal dialog_control is already connected. If it isn't, we connect it to the npc's function *on_dialog. *
friend_talk is the first npc's dialog, the one blocking the exit of the cave
Then, through lines 44 and 45 we check if we're in the "intro" chapter and if the dialog playing is "friend_talk", checking for the answer index sent by SMRT. So we check for the first option to be clicked and make the npc move out of the way. We also change the dialog the char will say from now on to friend_talk_positive.
It may seem obvious, but you should handle what the answers will do. If you don't code anything, SMRT will simply continue the dialog until the end, regardless of the answer selected. This is also why we don't check for the negative option, as the dialog will simply continue to it.
The Trigger
The trigger is a duplicate of the npc. It behaves exactly like one with the exception that it doesn't have a sprite set. Its dialog is set to "great_adventure".
This is also handled inside the npc's script. from line 56 to line 63.
When the very first message of great_adventure finishes, the screen will fade to black while the dialog continues. At the end of it, we change the chapter global to "after_going_there" and life goes on.
The second NPC
This one just illustrates the usefulness of the chapter > dialog paradigm. This npc will start the dialog "that_guy_talk". In the language file, there exists two versions of "that_guy_talk", each in the two different chapters.
This is all you need to do to make the same chars say different things based on moments of the game.
Final thoughts
This can give you a pretty good foundation to work with SMRT in your projects.
I hope this helps you work on your game. I learned a lot thanks to people writing articles, sharing knowledgement and directly helping me in my doubts. In a way, it is me continuing the cycle.