The resource database

In our visual novel, we need to display different characters, backgrounds, and so on. To do so, we need access to related resources that store the texture and metadata to display.

So far, we used temporary code to preload our characters in the CharacterDisplayer.

In this tutorial, we will create a resource database, a singleton that will find and load all characters and backgrounds in the game, giving us access to them on-the-fly.

We code it now because it is a dependency of the scene player, which we will create in the next lesson.

First, let’s code a new resource for the backgrounds, so we have two types of resources to load. It will allow us to make our resource database a bit DRY.

DRY, which you read like the word dry, stands for Don’t Repeat Yourself. It’s a programming principle that states we should avoid code duplication. In games, it’s one we typically apply loosely because of our job’s nature: we delete a lot of code, limiting the value of some strict programming practices found in environments where you tend to pile up code instead. But DRY’s an excellent rule to follow when repetition adds a real technical debt.

We start by creating a new script in the background directory, named Background.gd, and open it. There, we only export some fields to edit them in the inspector.

## Data container for backgrounds
class_name Background
extends Resource

export var id := "background_id"
export var display_name := "Display Name"
export var texture: Texture

Be sure to save the script so it registers as a new resource type.

Then, create a new resource of type Background in the Backgrounds/ directory. You can name the file industrial_building.tres.

In the Inspector, set its Id to industrial_building, its Display Name to Industrial building, and assign industrial-building.jpg to its Texture.

Coding the resource database

With both Character and Background resources in our FileSystem, we can start coding the database.

Create a new script in the Autoloads/ directory named ResourceDB.gd (you can create the directory if necessary). The script should extend Node.

We register it as a singleton by going to Project -> Project Settings -> AutoLoad, setting the path to our script, and clicking the Add button.

Ensure that the name is ResourceDB, with the last two letters capitalized, not ResourceDb with a lowercase b. You can double-click on the Name to modify it.

Let’s get coding.

The heart of the ResourceDB is its loading function. We define one generic loading function that can load and filter a particular type of resource in a directory.

The function works like so:

  1. We open and loop over one directory using a Directory object.
  2. For every filename that ends with .tres, we load that file and store the resulting resource in a variable.
  3. We call a type-checking function on the resource to ensure it matches the type we expect. If so, we store it in a dictionary.

After looping over the entire directory, we return our dictionary.

To loop over a directory’s contents in Godot, we have to:

  1. Create a Directory object and call its open() method with a path to the directory.
  2. Call Directory.list_dir_begin() to initialize the loop.
  3. Call Directory.get_next() to get the first entry’s name.
  4. Define a while loop in which we wait to encounter an empty filename. It is a sign that we reached the end of the directory.
  5. Inside the loop, we call Directory.get_next() to get the next entry’s filename.
  6. Finally, after the loop, we call Directory.list_dir_end() to clean up the object’s state.
## Auto-loaded node that loads and gives access to all [Background] resources in the game.
extends Node


## Finds and loads resources of a given type in `directory_path`.
## As we don't have generics in GDScript, we pass a function's name to do type checks.
## We call that function on each loaded resource with `call()`.
func _load_resources(directory_path: String, check_type_function: String) -> Dictionary:
    var directory := Directory.new()
    if directory.open(directory_path) != OK:
        return {}

    var resources := {}

    directory.list_dir_begin()
    var filename = directory.get_next()
    while filename != "":
        # Note that we only loop over only one directory; we don't explore
        # subdirectories.
        if filename.ends_with(".tres"):
            # `String.plus_file()` safely concatenate paths following Godot's
            # virtual filesystem.
            var resource: Resource = load(directory_path.plus_file(filename))

            # Here's where we check the resource's type. If it doesn't match the
            # one we expect, we skip it.
            # Resources are reference-counted so as the local variable
            # `resource` clears, Godot frees the object from memory.
            if not call(check_type_function, resource):
                continue

            # If the resource passes the type check, we store it.
            resources[resource.id] = resource
        filename = directory.get_next()
    directory.list_dir_end()

    return resources

To make this function work with any resources we might want to load, like backgrounds, characters, maybe animations, and more, we define functions that we can use to check the resources’ type.

They take a resource as their input parameter and return true if the resource is of a specific type. We pass them to _load_resources() as an argument by name and use call() to call them.

func _is_character(resource: Resource) -> bool:
    return resource is Character


func _is_background(resource: Resource) -> bool:
    return resource is Background

As your project grows, you’re likely to have different kinds of resources in a given folder. You could use a filename suffix as a convention to differentiate specific types of resources, but this is error-prone.

With this approach, even if we may load resources we don’t need sometimes, that shouldn’t happen much, and the files should be tiny and quick to load, especially for a visual novel, which is typically a lightweight kind of game.

Our loaded resources end up in a dictionary with the form { resource_id: resource_object }, giving us access to any resource through its unique identifier.

To load backgrounds and characters specifically, we add two variables that store the return value of a call to _load_resources().

onready var _characters := _load_resources("res://Characters/", "_is_character")
onready var _backgrounds := _load_resources("res://Backgrounds/", "_is_background")

The advantage of this approach is that it’s easy to add new types of resources to load. For example, if you want to add music to the database, you can add a variable and a type-checking function.

Note that here, we preload everything into memory. This is perfectly fine while prototyping, but if your game has hours of content, you will likely need a different approach to only load the resources a scene needs, especially on mobile devices where memory is limited.

To get access to our characters and backgrounds, we define some functions. We don’t directly give access to the dictionaries to provide consistent behavior. Our functions will always return a resource or null. Using functions makes it easy to refactor the code moving forward, for example, if we want to change the way we store resources.

# The database only allows you to get a resource of a given type. Below, we
# define two functions to get a character by ID or the default character, called
# the narrator in this project.
func get_character(character_id: String) -> Character:
    return _characters.get(character_id)


func get_background(background_id: String) -> Background:
    return _backgrounds.get(background_id)

We also define a default character, our game’s narrator. This character is a special one that does not have any portrait. I invite you to create a new character resource in the characters directory named narrator.tres.

With that, we can add some code to get our game’s narrator in ResourceDB.

const NARRATOR_ID := "narrator"

func get_narrator() -> Character:
    return _characters.get(NARRATOR_ID)

And with that, we have resource loading in place. We will put it to use in the next lessons, where we will define our scene player.

If you’d like to test that the script works, you can define the following _ready() function in ResourceDB.gd.

func _ready() -> void:
    print(_characters)
    print(_backgrounds)

Then, run any scene to see the output. Autoloads are automatically-loaded nodes that Godot right below the root of your scene tree. As such, they have access to all built-in callbacks like any other node, like _ready().

Here, I ran the text box’s scene and switched to the Remote scene tree in the Scene dock.

The Output bottom panel should display something like the following:

{dani:[Resource:1224], sophia:[Resource:1219]}
{industrial_building:[Resource:1230]}

You can then delete that temporary _ready() function.

Notice how as with every Singleton we show you, this one does not have mutable state. It loads the resources, then provides three functions to get access to them. Singletons that only return values on demand won’t cause severe bugs in your codebase.

In the next lesson, we’ll start working on the scene player, which will sequence game events.

Code reference

Here is the complete Background.gd script.

## Data container for backgrounds
class_name Background
extends Resource

export var id := "background_id"
export var display_name := "Display Name"
export var texture: Texture

Here is ResourceDB.gd.

extends Node

const NARRATOR_ID := "narrator"

onready var _characters := _load_resources("res://Characters/", "_is_character")
onready var _backgrounds := _load_resources("res://Backgrounds/", "_is_background")


func get_character(character_id: String) -> Character:
    return _characters.get(character_id)


func get_background(background_id: String) -> Background:
    return _backgrounds.get(background_id)


func get_narrator() -> Character:
    return _characters.get(NARRATOR_ID)


func _load_resources(directory_path: String, check_type_function: String) -> Dictionary:
    var directory := Directory.new()
    if directory.open(directory_path) != OK:
        return {}

    var resources := {}

    directory.list_dir_begin()
    var filename = directory.get_next()
    while filename != "":
        if filename.ends_with(".tres"):
            var resource: Resource = load(directory_path.plus_file(filename))

            if not call(check_type_function, resource):
                continue

            resources[resource.id] = resource
        filename = directory.get_next()
    directory.list_dir_end()

    return resources


func _is_character(resource: Resource) -> bool:
    return resource is Character


func _is_background(resource: Resource) -> bool:
    return resource is Background