Godot Tactics RPG – 07. Conversations

7thSage again. Welcome back to part 7 of the Tactics tutorial. This time we’re continuing where we left off with the last lesson and building up our UI. This time we’re adding a system to display dialog.

Speaker Data

We’ll start off our lesson by creating something to hold our dialog data. This class will inherit from resource like PanelAnchor did in the last lesson, to once again allow us a custom data type in the inspector. This time I think there is a greater chance someone will want to give users access to use these files, so I think its important to mention again that there is some risk in using resource files. So if you want to let users create their own dialog, I’d suggest providing an intermediary format so save and load the data.

The “Scripts->Model” folder has been empty up until this point, but I think it’s time to change that. Inside the folder create a new script named “SpeakerData.gd”. This class is pretty simple. It just holds a couple variables to store a character portrait, the anchor points we want to use, and an array of messages to display.

extends Resource
class_name SpeakerData

@export var speaker:CompressedTexture2D
@export var anchor:Control.LayoutPreset
@export var messages:Array[String] = []

Conversation Data

Because we want to do more than just display a single person talking, we’ll also create a second data type to hold a list of Speaker Data, allowing us to have complete conversations. Again Resource, be careful. Create another script inside “Scripts->Model” named “ConversationData.gd”.

extends Resource
class_name ConversationData

@export var list:Array[SpeakerData] = []

Creating a Portrait

In the resource pack we downloaded at the beginning there was only a single portrait named “Avatar.png”. Because we want to showcase multiple speakers, we’ll probably want a second portrait to differentiate them. Start by finding the original portrait, “Avatar.png” located in “Textures->UI”. Right click the file and choose “Show in File Manager”. Now let’s open the file in your image editor of choice, such as Photoshop, Krita or GIMP.

We’ll be adjusting the hue/saturation of the image. This will selectively modify certain bands of color and shift their values in one direction or another. Such as making all blueish values into reddish counterparts. While strictly speaking this isn’t necessary for programming, it is a useful skill for a programmer to be able to do simple photo editing such as this. Right now is a good example. We only have a single portrait, but it would be nice to have one that is a different color to prototype without the need of hunting down an artist to do it for us.

Here are the values and menu locations I used to get a result similar to the original EvilAvatar.png created for the original tutorial. While the results aren’t a perfect match, they are fairly close. These are of course just guidelines and you can choose whichever colors you like.

Photoshop
Image->Adjustments->Hue/Saturation
or
Layer->New Adjustment Layer->Hue/Saturation
Hue:80, Saturation:100, Lightness:-9

Krita
Filter->Adjust->HSV Adjustment
Type:Hue/Saturation/Value
Hue:345, Saturation:100, Value:-7, Colorize:checked

GIMP
Color->Hue Chroma
Hue:80, Chroma:93, Lightness:3.5

When you are done save the image as “EvilAvatar.png” and bring it over to our “Textures->UI” folder. If you aren’t using Photoshop/Krita or GIMP, you may have to look up how to adjust Hue/Saturation in your program of choice. If you would like to download the file you can find it on Google Drive, or alternately you can find it on the Repository, or even the repository from the original tutorial. It should also be noted that both Krita and Gimp are free, so If you haven’t checked them out yet, It’s worth giving them a shot. They are both very solid programs. Krita is targeted more towards painting, and GIMP is more targeted toward photo editing and is on the more technical side.

Create Conversation Assets

Create another folder under “Data” named “Conversations”. This is where we’ll be creating and storing our conversations. Right click on the new folder and choose “Create New->Resource” and find our ConversationData type. It may be easier to type it into the search field to find. Name the file “IntroScene.tres”. Click on the file to view it in the inspector, we don’t need it in a scene to do this.

For our demo conversation we’ll need to add 3 elements to our array, set the size to 3, or alternately click Add Element until there are 3 SpeakerData objects created in the list. Click on each object where it says empty and set to “New SpeakerData”. The first dialog will have 3 lines of text, 2 for he second, and just 1 for the last.

The picture we will Load for “Speaker” will be either “Avatar.png” or “EvilAvatar.png” from the “Textures->UI” folder, and the anchor points we’ll use as options for the dialog are:

Control.PRESET_TOP_LEFT
Control.PRESET_TOP_RIGHT
Control.PRESET_BOTTOM_LEFT
Control.PRESET_BOTTOM_RIGHT

The lines of dialog we are using will be these:

Be scared. Cause I am awesome!!!
And Scary!!!
You’re scared aren’t you?
Nope. Not really.
Let’s just get this over with.
Oh… well… hmm…

Layout Anchor

Before we get to the conversation Panel, we need to make one quick fix in “LayoutAnchor.gd” from the last lesson. In the function ToAnchorPosition(), we need to add the “await” keyword in the “if animated:” branch.

func ToAnochorPosition(anchor:PanelAnchor,animated:bool):
	if animated:
		await MoveToAnchorPosition(anchor.myAnchor, anchor.parentAnchor, anchor.offset, anchor.duration, anchor.trans, anchor.anchorEase)
	else:
		SnapToAnchorPosition(anchor.myAnchor, anchor.parentAnchor, anchor.offset)

Conversation Panel

Create a new script under “Scripts->View Model Component” named “ConversationPanel.gd”. This is the script we’ll attach to our actual dialog panel that will control its position and the text displayed. We’ll extend LayoutAnchor that we created in the last lesson to keep things I think a bit simpler. The first three variables will hold a reference to the child objects that have elements that we will need to control in some way. The next, AnchorList we are bringing in from the Panel Tests. This will hold the custom named anchor points we will use to control the panel positions.

extends LayoutAnchor
class_name ConversationPanel

@export var message:Label
@export var speaker:TextureRect
@export var arrow:Node

@export var anchorList:Array[PanelAnchor] = []

Next we have create a signal that we will emit once this set of dialog is finished. The variable _parent we will use to get a different signal from the conversation controller letting us know when we can continue to the next line. In the _ready() function we set the variable to the conversation controller node.

signal finished
var _parent:ConversationController

func _ready():
	_parent = get_node("../")

To let us use the anchorList array, we also bring in the function GetAnchor() from the second Panel Test.

func GetAnchor(anchorName: String):
	for anchor in self.anchorList:
		if anchor.anchorName == anchorName:
			return anchor
	return null

We’ll update and display the dialog in the Display() function. The first two lines change the portrait image displayed, and set the size based on the texture used. The next bit though probably looks a bit weird, but the anchor point does not automatically update, so we’ll set it to it’s current value to trigger it to set the position to the correct anchor location.

The for loop goes through the message array in speaker data, changes the text and if there are more lines, sets the arrow to visible or hides it if not, then it pauses until we get a signal from the conversation controller. Once all dialog is finished and we are outside the for loop, we send a signal telling the controller that there is no more dialog.

func Display(sd:SpeakerData):
	speaker.texture = sd.speaker
	speaker.size = speaker.texture.get_size()
	
	#Resets the anchor point after resizing.
	speaker.anchors_preset = speaker.anchors_preset
	
	for i in sd.messages.size():
		message.text = sd.messages[i]
		arrow.visible = i + 1 < sd.messages.size()
		await _parent.resume
	
	finished.emit()

Conversation Controller

The bulk of our dialog system is here on the conversation controller. It will be in charge of which panel dialog goes onto, telling the panels to move onto the screen and will control the timing of when to advance our dialog.

We’ll start by creating a new script in the “Scripts->Controller” folder named “ConversationController.gd”

Fairly simple to start. Extend node, class_name, and we create two signals, “resume” that will send a signal to the panels to show the next line of dialog, and “completeEvent” that will send a signal to the Cut Scene State that we create later.

Next we have two @export variables to store the left and right panels. To give an indication of dialog going back and forth, we’ll give the option to send the data to one side of the screen or the other, depending on who is talking.

Then one last variable before we get into the functions, a bool to help us keep track of when our panels are animating into place.

extends Node
class_name ConversationController

signal completeEvent()
signal resume()

@export var leftPanel:ConversationPanel
@export var rightPanel:ConversationPanel

var inTransition:bool

As far as I can tell, Godot doesn’t really have a single way to disable a node, but instead gives you options to call and disable pieces as you want or need them. So here, for the time being I’ve created two functions to enable and disable nodes, so if we decide we want more or less of our panels to get disabled, we can add it without changing code in multiple spots. For now we set the process mode to disabled, and hide the node.

func _DisableNode(node:Node) -> void:
	node.process_mode = Node.PROCESS_MODE_DISABLED
	node.hide()

func _EnableNode(node:Node) -> void:
	node.process_mode = Node.PROCESS_MODE_INHERIT
	node.show()

In the _ready() function we set the default position of both left and right panels to “Hide Bottom” and disable them for good measure.

func _ready():
	leftPanel.ToAnochorPosition(leftPanel.GetAnchor("Hide Bottom"), false)
	_DisableNode(leftPanel)
	
	rightPanel.ToAnochorPosition(rightPanel.GetAnchor("Hide Bottom"), false)
	_DisableNode(rightPanel)

We’ll have two main functions that control the flow of our dialog that will get called from the Cut Scene State. Show() and Next().

The Show() function we enable our panels and start the Sequence(). Once the panels are in position it will pause and wait for Next() to be called. We’ll be calling next from the Cut Scene State whenever the OnFire() method is triggered, and if the bool for inTransition isn’t set, we send the resume signal.

func Show(data: ConversationData):
	_EnableNode(leftPanel)
	_EnableNode(rightPanel)
	Sequence(data)

func Next():
	if not inTransition:
		resume.emit()

There is quite a lot happening in the Sequence() function. Here we are looping through the speakers. The loop for the individual lines happens on the panel in the Display() function from earlier. I’ve added a few comments to mark a few points, but I’ll also try going into a bit more detail as well.

func Sequence(data:ConversationData):
	for sd in data.list:
		inTransition = true
		var currentPanel:ConversationPanel
		if(sd.anchor == Control.PRESET_TOP_LEFT || sd.anchor == Control.PRESET_BOTTOM_LEFT || sd.anchor == Control.PRESET_CENTER_LEFT):
			currentPanel = leftPanel
		else:
			currentPanel = rightPanel
		
		var show:PanelAnchor
		var hide:PanelAnchor
		
		if(sd.anchor == Control.PRESET_TOP_LEFT || sd.anchor == Control.PRESET_TOP_RIGHT || sd.anchor == Control.PRESET_CENTER_TOP):
			show = currentPanel.GetAnchor("Show Top")
			hide = currentPanel.GetAnchor("Hide Top")
		else:
			show = currentPanel.GetAnchor("Show Bottom")
			hide = currentPanel.GetAnchor("Hide Bottom")
			
		#make sure panel is hidden to start and set text to initial dialog
		currentPanel.ToAnochorPosition(hide, false)
		currentPanel.Display(sd)
		
		#move Panel and wait for it to finish moving
		await currentPanel.ToAnochorPosition(show, true)
		
		#once panel is done moving we can start accepting clicks to advance dialog
		inTransition = false 
		await currentPanel.finished
		
		#Hide panel and wait for it to get off screen
		inTransition = true
		await currentPanel.ToAnochorPosition(hide, true)
		
	_DisableNode(leftPanel)
	_DisableNode(rightPanel)
	completeEvent.emit()

Before moving any of our panels we are setting inTransition to true, and because each loop only deals with a single character, we only need to care about one of the two panels, so if the anchor preset is on one of the left anchors, we use the leftPanel, otherwise the right.

Each panel will have four named anchor points that we will set later, “Show Top”, “Show Bottom”, “Hide Top”, and “Hide Bottom”. But because we are not moving our panel around from top to bottom during a single characters dialog, we’ll also pick a single show and hide anchor point to use, depending on whether the anchor point is one of the Top anchors, and if not will use the bottom position.

Now we kinda get into the meat of the function. We start by snapping the panel to its hidden position and setting the text to the first line of dialog. If we wait to set the dialog, we’d see the placeholder text while moving it onto screen and we don’t want that.

Next we call the function we created in the last lesson to tween the panel’s position to the anchor point and once that is finished we set inTransition to false, which means Next() will start sending signals when we get input, and the loop waits here until Display() finishes with all its dialog, and sends the finished signal.

Then we are back in transition and hide the panel. If you have downloaded the version from the repository, the line “await currentPanel.ToAnochorPosition(hide, true)”, was incorrectly using “false” on the second value, telling it not to animate when leaving the screen. I’ll have this fixed in the repo for the next lesson.

Once all the characters have finished their dialog, we disable the panels and send the complete signal.

Before we move on, lets go to our Battle scene and create a child node under “Battle Controller” named “Conversation Controller” and attach the script we just created to it.

Battle Controller

Let’s add a quick line to our BattleController script along with the @export variables with the other individual controllers. This adds a reference to our Conversation Controller so we can access it through our _owner variable. Next assign our Conversation Controller node in the inspector. If you are unable, be sure that you’ve attached the ConversationController.gd script to its node.

@export var conversationController: ConversationController

Cut Scene State

We’ve mentioned the Cut Scene State a couple times now, and its the last big piece of code we need to finish. Under “Scripts->Controller->Battle States”, create a new script named “CutSceneState.gd”. We’ll extend BattleState like the other states, and set a variable that will hold the next state to transition to. Then we have a variable for the conversation data that we will be loading, which we set the values in the _ready() function. Don’t forget to call @super() first, the base class sets the _owner value that we also need.

extends BattleState

@export var selectUnitState: State
var data: ConversationData

func _ready():
	super()
	data = load("res://Data/Conversations/IntroScene.tres")

We have one more signal that we need to listen to inside this state so we’ll add that to our Add/RemoveListener() functions.

func AddListeners():
	super()
	_owner.conversationController.completeEvent.connect(OnCompleteConversation)

func RemoveListeners():
	super()
	_owner.conversationController.completeEvent.disconnect(OnCompleteConversation)

We start by calling Show() in the Enter() function, and in the OnFire() function is where we call Next() to advance the dialog.

The completeEvent signal is connected to the OnCompleteConversation() function, so it will be called when that signal is emitted, which will trigger the ChangeState to our Select Unit State that we created in a previous lesson.

func Enter():
	super()
	_owner.conversationController.Show(data)

func OnFire(e:int):
	super(e)
	_owner.conversationController.Next()
	
func OnCompleteConversation():
	_owner.stateMachine.ChangeState(selectUnitState)

That should finish up that script, so let’s create a node to attach it to. Create a new child node under “State Machine” named “Cut Scene State” and attach our script to it. While we’re here be sure to set the selectUnitState variable to its corresponding State.

Init Battle State

We just need to make two small changes. We’ll change the the state variable at the top to match the scene we are going to transition into, and we need to change to our CutSceneState instead of the SelectUnitState to start off.

@export var cutSceneState: State
_owner.stateMachine.ChangeState(cutSceneState)

And before we move on, set the cutSceneState variable to its State in the inspector.

Scene Setup

Now that we have all the code laid out, we need to set up the images and nodes to actually display it all. Once we’re done setting up our graphics, our panels should look something like the following picture.

Before we jump into that, lets change one quick setting in the project settings that will allow the UI to scale with the window. Go to, “Project Settings->General->Display->Window” and in the section “Size”, make sure Resizable is set to true. In the Section “Stretch”, “Mode” should be set to “canvas_items”, and “Aspect” should be set to “expand”

Lets add a few nodes. We’ll start with the right side, and then copy it over to the left. As a child node to “Conversation Controller” create a new node with the type “Panel” and name it “Right Edge Conversation Panel”. Attach the script “ConversationPanel.gd” to it.

As a child node to the “Right Edge Conversation Panel”, we need to create several more nodes. For the speech bubble create a node with the type “NinePatchRect” named “Background”. Instead of being a texture property, we create 9 slices on the node level in Godot instead. The portrait we need a node of type “TextureRect” named “Speaker”. The text in our bubble will be displayed with a node of type “Label” named “Message”. And the last image we need is an arrow that will indicate more text, which will be of type “TextureRect” just like the portrait. We’ll name the right one “Right More Arrow” to make it easier to identify later on when we set it up to move.

We should have a scene tree that looks like this now. We’ll get a few more settings fixed before copying the right panel over to the left.

In the inspector for “Right Edge Conversation Panel” Go to the layout section. Under transform set the size to x:465, y:227. Set the Anchor Preset to “Top Right”. Next to get rid of the gray tint, scroll down to Theme Overrides. Check the box next to Panel, and in the field to the right, choose “New StyleBoxEmpty”.

The first panel we’ll set up is our 9 slice, “Background”. The first thing we need is to assign our texture. In the inspector under Texture, choose the texture “Textures->UI->ConversationsPanel.png”. Next set our Region Rect w and h to (74,140), the same size as our texture. Set the Patch margin to Left:71, Top:70, Right:2, Bottom:70. Under Transform, we’ll set the Size to x:465, y:140. Next under Layout, set the Layout Mode to “Anchors”, and the Anchors Preset to “Bottom Right”.

“Speaker” is a bit simpler. Set the Texture to “Textures->UI->Avatar.png” Set the Layout Mode to “Anchors” and the Anchor Preset to “Bottom Right”.

For “Message” there are a few more settings. Under Text put some generic text in such as “Here is a lot of text to read. Use the arrow for more.” So we can see how our settings are looking. We’ll also set “Horizontal Alignment” and “Vertical Alignment” to “Center” and set “Autowrap Mode” to “Word”. A bit further down go to the section “Theme Overrides” set the check box for “Font Color” and next to it set the color to something like HEX# 682c15. Next set the check box for “Font Size” and set the size to 25px. The last thing we need to set for the Message label is its position and anchor. Layout Mode to “Anchors”, Preset to “Bottom Right”. Under Transform set the Size to x:284, y:120. Set the Position to x:45, y:97.

For the “Right More Arrow”, assign our texture. In the inspector under Texture, choose the texture “Textures->UI->MoreConversationArrow.png”. Again set anchor preset to “Bottom Right”. Under Transform->Size, click the reset icon next to the x value to set the size back to the size of the image, and set position to x:170, y:210.

Before we finish up any more settings, lets duplicate what we have over to the other side. Right click “Right Edge Conversation Panel” and select duplicate. Rename the copy to “Left Edge Conversation Panel” and rename the arrow to “Left More Arrow”.

Now that we have both conversation Panels, select the “Conversation Controller” and in the inspector Assign the Right and Left Conversation Panels to their variables.

There are a few things we need to flip and reposition to make the left side work. We’ll start with the “Left Edge Conversation Panel”. Set the Anchor Preset to “Top Left”.

For “Background” set the Anchor Preset to “Bottom Left” and under transform click the chain icon to the right of the values to where it is a broken chain. Once you have done so, set the scale to x:-1, y:1. The position won’t line up correct yet because of how the pivot is calculated. To set things right, still in the Transform section, we need to set the Pivot Offset to x:232.5, y:0, half of the layers size value that we scaled.

“Speaker” is a bit simpler. We need to select the checkbox “Flip H” and set the Anchor Preset to “Bottom Left”.

“Message” is also fairly simple. Change Anchor Preset to “Bottom Left” and set the position to x:136, y:97. “Left More Arrow” we will set Anchor Preset to “Bottom Left” and set the position to x:262, y:210

Next go to the Left and Right Edge Conversation Panels and link up all the pieces in the inspector. Assign the corresponding nodes to each of the fields, “Message”, “Speaker” and “Arrow”. Careful that you get the correct side’s nodes.

We need to add 4 Panel Anchors to our “Anchor List” array. Set the size to 4, or alternately click Add Element until there are 4 objects created in the list. Click on each object where it says empty and set to “New PanelAnchor”. The 4 anchors we created will be named “Hide Bottom”, “Hide Top”, “Show Bottom”, and “Show Top”. The left and right controllers will both have 4 Panel Anchors with the same names, but the values will be different depending on whether it is for the left or right side. The Left Panel will move the panel off the edge of the left side to hide it, and the right panel will move it off the screen on the right to hide that one. The picture below shows the values for the left and right sides.

Arrow Animation

To make things look a bit more dynamic, let’s give the arrow a little bounce animation. This time instead of using tweens, we’ll use an animation node to accomplish that. As a child of “Right More Arrow”, create a node of type “AnimationPlayer” named “Right Animation” and create another as a child of the “Left More Arrow” named “Left Animation”.

We’ll start by creating the animation for the right arrow. Once you click the “Right Animation” node, the bottom center section will switch to the “Animation” tab. Here we can set up our animation. In the top center area of the animation section, click the button that says “Animation” and select “New”. Name the animation “Right Arrow Bounce” to make it easy to identify.

In the top right corner below the pushpin is a button for “Animation Looping”, set it to “Pingpong”. The number directly to the left is the animation length, set this to “0.5” seconds. Next to the Edit button, there is a button that looks like an arrow, “Autoplay on Load” select that so our animation plays automatically. You will also see the icon show up beside the animation name indicating that it is set to autoplay. In the bottom right corner is a slider bar next to a magnifying glass. You can use this to zoom into the relevant section of our timeline.

Now to set some values for our animation, select the “Right More Arrow” node in the scene tree and in the inspector scroll down to Transform and find the Position. Set it to x:170, y:210 and click the Key icon to the right of the value. We shouldn’t need a Reset track, the animation will always be playing when the arrow is visible.

In the animation window click at the end of the gray bar, the 0.5 second mark, in the timeline. Right Click on the timeline and select “Insert Key”. Select the key and in the inspector window change the position to x:170, y:220. If you click play, you should be able to see the arrow moving up and down now.

For the left side we’ll do the same basic thing. Create New Animation named “Left Arrow Bounce”. Set to “Pingpong”, animation length to “0.5” and “Autoplay”. The first key will be x:262, y:210, and the second to x:262, y:220.

With that, everything should be working now. Test our game and see how everything is working. And with that we’re all done with the dialog system…..

Localization

Ok, so I lied, there is one more section of code. I’d like to take a moment to lay the groundwork for translating our games. While this wasn’t in the original code, it’s fairly minimal, and I do think its one of those things that is a lot easier to manage if we implement it early on in our code, instead of trying to tack it on at the end when we have little bits of text scattered throughout our codebase.

We’ll be loading our translations in csv format, which is a text file version of a spreadsheet. Start by opening up your spreadsheet editor of choice, whether that be Open Office/Libre Office, Google Sheets, or Microsoft Excel or some other choice. The first row is our labels that we’ll use to tell the engine which language a particular column represents. The first Column is our Key values. This is what we’ll use to identify which dialog we want placed, and the code will then pick the corresponding language that we have. It is possible to use the English translation itself as your key, but it can get tricky when you use the same dialog string in multiple instances. The top of each language column will use the language codes for whatever language you plan on implementing. In our case, en for English, ja for Japanese, and es for Spanish.

If you’d like to download the translation file I placed it on Google Drive along with the avatar file from earlier. I’d also like to point out before we get started that the Spanish translation is most likely utter rubbish. I don’t know Spanish and searched around the internet for things I thought sounded like they might be close. The Japanese I asked for some help with, so that one should be good. A bit of extra character is even added to make it more interesting. Gotta be careful not to fall into the trap of trying to make a translation a 1 to 1 copy. No two languages match close enough for jokes, especially puns/wordplay, or ways of speaking to work perfectly like that.

We’ll be entering the following lines into our spreadsheet. Feel free to add/remove or change any translation you would like. If a particular language does not have a translation for any specific language, it will default to the English translation. Also, one final note, you’ll sometimes see lines purposely not translated to a language. For instance menus in Japanese frequently use English for things like Start, and in fact will look odd to Japanese players if it isn’t in English.

I’ve chosen keys that help identify which character is speaking to make it easier to identify in the spreadsheet when translating.

KEYenjaes
VILLAIN_1ABe scared. Cause I am awesome!!!恐れ!俺さまは最強だぞ。Temedme. Porque soy fantástico.
VILLAIN_1BAnd Scary!!!俺さまの素晴らしさは怖いんだろ。Y aterrador
VILLAIN_1CYou’re scared aren’t you?ビビてんだろ。Tienes miedo, ¿verdad?
HERO_1ANope. Not really.いや、べつに。No, nada en especial.
HERO_1BLet’s just get this over with.さっさと決着つけようぜ。Vamos a acabar con esto.
VILLAIN_2AOh… well… hmm…あら。。。まあ。。。えーと。。。Oh… ya veo… hmm…

Once you have finished entering the values into your spreadsheet of choice, its time to save our file out to a “.csv” file. Depending on the editor of choice, you’ll probably be looking for an option that says either Save, or Export. For the file type choose .csv and save our file as “IntroScene.csv”. Several options will come up on how you want to format the data. The default comma separation is fine. Which is where the file format gets its name, Comma-Separated Values. We’ll also need to set the encoding to Unicode UTF-8, Godot is a bit picky about format. And before we import this into the engine, open the .csv file in a text editor and if there is a blank line at the end of the document, delete it. This can cause Godot to crash when it tries to import it, although I assume this should get fixed soon if it hasn’t already been.

Now back in Godot. In the FileSystem panel under Data->Conversations create a new folder named “Translations” and move our “IntroScene.csv” file into the folder. Next open Project->Project Settings and go to the “Localization” tab. Click the “Add” button. When we added the .csv file to our scene tree, the engine created several new files, one for each language in our file. These files ending in “.translation” are the one we need to add. The .csv file is used to generate these files. Once selected click “Open”. When you’ve added all the languages we can close Project Settings.

The code for this is very simple. Open “ConversationPanel.gd” and change the line:

message.text = sd.messages[i]

to

message.text = tr(sd.messages[i])

This will trigger the translation engine to look for the key value and switch out the text. If we want to use translations in something like a menu, you can in most cases skip the tr() and the engine will translate it automatically if it finds the Key. We need it here because we are dynamically loading our dialog from a string.

Next, in “InitBattleState.gd”, to test the localization, somewhere before we change state to the “CutSceneState”, add the line(s):

TranslationServer.set_locale("ja")
#TranslationServer.set_locale("en")
#TranslationServer.set_locale("es")

It should be noted that Godot will autodetect the language that our operating system is set to. These lines are added to force it during testing, or to be added as part of a language selection option. Although if you do plan on a language selection option, you’d also want to be sure to set the default language to match the user’s language.

The final piece of the puzzle is to edit the dialog itself. In the FileSystem panel select “IntroScene.tres” and replace the each line of dialog with the Key values we used in our CSV file. For the version uploaded to the repository, I’ve duplicated the file as “IntroScene2.tres” so you can load or view either file. The file is loaded inside “CutSceneState.gd” so the line inside there gets edited to match whichever file we are using.

And that should be it. The dialog should all be in Japanese now. Try changing the locale from “ja” to “en” or “es” to see the other translations in action. If you’d like to continue to add translations to menus, I found this massive template, PolyglotGamedev, with a ton of menu options and languages that will help you get started. I can’t vouch for the quality of any of the specific translations, but they look like they’re right so far as I can tell? And I’d like to also give a shout out to the folks at Crystal Hunters Manga for helping with the Japanese. It’s a great little manga series for learners. The text and grammar start out very slow and gives good reading practice that most apps in my experience are fairly poor at. I’ve been slowly trying to go through the series when I have time and have been enjoying it so far. The first book is free on their website, although the paid Kindle version has much better image quality.

Summary

Ok, we’re all done with this lesson for real this time. This was a pretty big lesson with code and a quite a few graphics to set up, but we’ve now got the ability to add conversations to our battle.

If you have any questions, feel free to ask, or check the GitHub Repository and if you need the files I used, they can be found at this link on Google Drive

Leave a Reply

Your email address will not be published. Required fields are marked *