We have come a long way from where we began, learning about Godot and building a simple RSS reader using a few simple components. In Part 1 we got a simple scene to display some RSS from a remote URL. In Part 2, we took advantage of the XMLParser to build up a series of lists which allowed us to parse the RSS feed and create several views, as well as a clickable link to view the article.

Along the way, Godot itself went from being a series of betas to a final release! I am very excited about the future of the Godot Engine, and I hope you find these tutorials interesting and motivates you to build something cool with Godot!

Let’s get started.

First off, I have created a repo for this code, in case you get stuck and want to have a reference to look at. I will keep it up to date with the latest tutorial and for future tutorials I’ll do something similar.

what we have so far

Let’s hide that debugging field now by just clicking the little “eye” next to the node.

Hidden from view

If you have ever used Photoshop, you’ll be familiar with this concept. There aren’t any “layers” per se, but you can take advantage of the tree structure to hide and show components as you like.

Our goal for this final part is to add the ability to set a “preference” that persists across app loads. We will learn now to load and save information from a save game file.

Create a new Button. Change the text within it to “Settings”.

Our settings button

Be sure to rename the node as well. I’m calling my SettingsButton.

Rename your default button to something useful

Next, let’s create a WindowDialog control. This will contain the configuration information we want to set for this application.

Our config window

You’ll notice it appears in the node tree, but the “eye” defaults to close. This is what we want to have by default when the application runs, but we need to unhide it in order to build the content we want inside it. Rename it to “SettingsDialog” so we can access from code.

Our new Settings Dialog. We can’t see it!

Unhide it by clicking the eye. You’ll see a little yellow triangle warning us that this window will be hidden by default. Resize it and make sure it’s the topmost layer.

Let’s make a window!

Now while this node is selected, add a Label node and put the text “The RSS Feed URL”. Then create a LineEdit field and position it below the label. Lastly, create another button — call it “ClearButton” in the node tree, and give it a text value of “Clear”.

Here’s what we have so far:

Our preference window is shaping up quite well!

Now if you saved this work, launched your app preview, you’ll see … nothing. That’s because even though you “un hid” the dialog in the editor, it remains hidden when you launch the app. Clicking the “Settings” button is what we want to do to trigger this window, so let’s bind the click event on our SettingsButton.

Click that button either in the Scene or the node tree and click the Node tab in the upper right. Choose the “Signals” mode if it’s not already set, and choose “pressed()” as our event to listen to.

Listening to the pressed event of our SettingsButton

Hit connect, then connect again in the dialog, and you should then be seeing our old familiar code window.

Scroll to the bottom and you’ll find our newly added function:

Replace the “pass” default text with:

$SettingsDialog.popup()

now test this out by running this scene ( remember it’s the little movie clicker icon in the upper right).

If all goes well, you should see something like this:

Our new dialog

Notice the “x” is already wired to close the dialog. Let’s wire up our “Clear” button while we are here.

You should be familiar with wiring up a node by now. Choose the ClearButton, click the Node panel and find the pressed() event to connect, etc.

I renamed my LineEdit field to RSSURLText, since in the future we might have more of these around, and I want to access it from code, once again.

In the code, we can access subcomponents of a node with a URL like syntax. To get to our RSSURLText element — to clear it out — you can do something like this $SettingsDialog/RSSURLText . Once we have the right element, we can set its text value to nothing.

Clearing the text

Try out the scene again — launching the dialog — and type in some text in the field. Then click the Clear button. It should zero out the text value.

Try out the clear button

Ok, now let’s do something interesting. What we want to do is to be able to paste a url in this field, go back to the main app and have it access that URL as the primary source of our RSS information. Let’s figure out how to save a value in this box to come back to later.

Add a new button to our dialog. Call it SaveButton and give it a label of “Save”. Put it next to the Clear button.

Now, in our code we are going to grab this text value, create a File object, write to it, and save it. Let’s create a new function called “save_data”

func save_data():
 var save_config = File.new()
 var save_data = {
   "url": $SettingsDialog/RSSURLText.text
  } 

 save_config.open("user://save_config.save", File.WRITE)
 save_config.store_line(to_json(save_data))
 save_config.close()

With only a few lines of code, a lot is going on. First, we create a new File object using File.new() then we create a new dictionary object with the curly braces and access the text via $SettingsDialg/RSSURLText.text like before. Then, we call the open()method on our File object and give it a path to save to. We used a special url user://save_config.save . The only part that is special is the user:// that’s a special path that Godot uses to store data in the user’s local directory. The save_config.save part could have been anything, as long as it makes sense to you. The File.WRITE is a special ENUM that Godot knows matches the parameter value for write for the open method.

We then convert our save_data into JSON and store it as a line in our save_config file.

Let’s add a bit of debugging to see where our file was saved.

func save_data():
 print('saving data')
 print(OS.get_user_data_dir())
 var save_config = File.new()
 var save_data = {
   "url": $SettingsDialog/RSSURLText.text
  } 

 save_config.open("user://save_config.save", File.WRITE)
 save_config.store_line(to_json(save_data))
 save_config.close()

We can check where our file is saved to by using the OS.get_user_data_dir() .

Run your code again and see what happens.

Here’s what I got:

Where did it save?

If you navigate to the location on your own machine, you should not only see the save file, but the contents should be written to as well. What I see is this:

It worked!

Ok! Are you still with me? We are almost done!

We’ve created a settings window, and saved the setting where we can get it again. We just now need to be able to fetch it again when the app opens, and use that setting when the app loads. We also need to refresh the UI if we change the setting, so we don’t have to reload the app.

Let’s write a new function called load_data() that will read the data from the config and populate the SettingsDialog .

func load_data():
 print('loading data')
 var save_config = File.new()
 if not save_config.file_exists("user://save_config.save"):
  return #error no save game!
 save_config.open("user://save_config.save", File.READ)
 var text = save_config.get_as_text()
 var url = parse_json(text)['url']
 print('Loading JSON: ' + text)
 print('URL: ' + url)

$SettingsDialog/RSSURLText.text = url
 save_config.close()

We create a file, check to see if the config file exists, open it for reading, then get the text. We next parse that text as JSON (nice for having multiple values and parameters) and set the url value to the settings window field.

We call this in the _ready function. This function gets called when all the nodes and scripts have been loaded into memory, and the code is ready to execute.

func _ready():
 load_data()

We also need to update our populateEdit function to load the RSS url from the config field:

func populateEdit():
 #pass
 var url = $SettingsDialog/RSSURLText.text
 $HTTPRequest.request(url)

We also should clear our lists and fields between requests, in case the RSS url has changed:

func _on_OpenButton_pressed():
 print("Button pressed!")
 clearFields()
 populateEdit()

func populateEdit():
 #pass
 var url = $SettingsDialog/RSSURLText.text
 $HTTPRequest.request(url)

func clearFields():
 title_arr.clear()
 desc_arr.clear()
 link_arr.clear()
 $ItemList.clear()
 $DescriptionField.text = ""
 $LinkButton.text = ""

If all has gone well, you can now load your app, open the config, paste an RSS url, save it, close the dialog, and then hit Open to view all the feeds from that URL. You can then close the app, reopen it, and just go straight to Open to see that the RSS url was saved across launches.

Here’s a full listing of the code, in case I’ve missed a step:

extends Control

# Declare member variables here.

var title_arr =  []
var desc_arr  =  []
var link_arr  =  []

# Called when the node enters the scene tree for the first time.
func _ready():
 load_data()

# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass

func _on_OpenButton_pressed():
 print("Button pressed!")
 clearFields()
 populateEdit()

func populateEdit():
 #pass
 var url = $SettingsDialog/RSSURLText.text
 $HTTPRequest.request(url)

func clearFields():
 title_arr.clear()
 desc_arr.clear()
 link_arr.clear()
 $ItemList.clear()
 $DescriptionField.text = ""
 $LinkButton.text = ""

func _on_HTTPRequest_request_completed(result, response_code, headers, body):
 $TextEdit.set_text(body.get_string_from_utf8())

 #lets parse this body content
 var p = XMLParser.new()
 var in_item_node = false
 var in_title_node = false
 var in_description_node = false
 var in_link_node = false

 p.open_buffer(body)

 while p.read() == OK:
  var node_name = p.get_node_name()
  var node_data = p.get_node_data()
  var node_type = p.get_node_type()

  # print("node_name: " + node_name)
  # print("node_data: " + node_data)
  # print("node_type: " + node_data)

  if(node_name == "item"):
   in_item_node = !in_item_node #toggle item mode

if (node_name == "title") and (in_item_node == true):
   in_title_node = !in_title_node
   continue

  if(node_name == "description") and (in_item_node == true):
   in_description_node = !in_description_node
   continue

  if(node_name == "link") and (in_item_node == true):
   in_link_node = !in_link_node
   continue

  if(in_description_node == true):
   # print("description-data" + node_data)
   if(node_data != ""):
    desc_arr.append(node_data)
   else:
    # print("description:" + node_name)
    desc_arr.append(node_name)

  if(in_title_node == true):
   # print("Title-data:"+ node_data)
   if(node_data !=""):
    title_arr.append(node_data)
   else:
    # print("Title:" + node_name)
    title_arr.append(node_name)

if(in_link_node == true):
   # print("link-desc" + node_data)
   if(node_data != ""):
    link_arr.append(node_data)
   else:
    # print("link" + node_name)
    link_arr.append(node_name)

 # print("Titles:")   
 for i in title_arr:
  # print("TITLE: " + i)
  $ItemList.add_item(i,null,true)

func _on_ItemList_item_selected(index):
 $DescriptionField.text = desc_arr[index]
 $LinkButton.text = link_arr[index]

func _on_LinkButton_pressed():
 OS.shell_open($LinkButton.text)

func _on_SettingsButton_pressed():
 $SettingsDialog.popup()

func _on_ClearButton_pressed():
 $SettingsDialog/RSSURLText.text = ""

func save_data():
 print('saving data')
 print(OS.get_user_data_dir())
 var save_config = File.new()
 var save_data = {
   "url": $SettingsDialog/RSSURLText.text
  } 

 save_config.open("user://save_config.save", File.WRITE)
 save_config.store_line(to_json(save_data))
 save_config.close()

func _on_SaveButton_pressed():
 save_data() 

func load_data():
 print('loading data')
 var save_config = File.new()
 if not save_config.file_exists("user://save_config.save"):
  return #error no save game!
 save_config.open("user://save_config.save", File.READ)
 var text = save_config.get_as_text()
 var url = parse_json(text)['url']
 print('Loading JSON: ' + text)
 print('URL: ' + url)

$SettingsDialog/RSSURLText.text = url
 save_config.close()

The whole shebang.

With this, we have completed all the parts of a really basic RSS reader. And for a really basic application that can read and write to files, access data from the internet, and display that content in a meaningful way.

If you would like to see a “diff” of the changes between the previous part and this one, you can visit this link https://github.com/triptych/godot_reader_tutorial/commit/20fbd21e11cdb5178623c794a8cfe71c27496300 .

The last thing to do is to export the project. For me, I will do it for MacOS. Your steps may vary depending on your platform.

Go to Project > Export.

You’ll probably see a dialog like this:

Get it out there.

Under Presets, hit Add… I chose Mac OSX (Runnable).

Hit Export Project.

Choose a path and a filename.

Let’s save it!

Now you have a .dmg file you can distribute to others. Let’s try it out!

Our new app!

We didn’t tweak any settings for the icon, etc. but that’s ok because this is just a tutorial.

Your app!

Double click it, and Boom! Your own app!

Here it is!

I hope you have learned something from this tutorial. Please share, like, and all the other social fun things. If you have any comments, please let me know!