-->
This plugin provides a simple (relational) database implemented through the Resource
( its documentation ) system of Godot. Moreover, a dedicated editor is provided to create and edit such databases.
Now, why use Resource
as the base for the database, instead of json or any other? Well, initially I did consider those options, including SQLite .
There are several formats that are text based, such as json and XML. Those are very "friendly" to version control systems. Yet binary formats tend to offer better loading performance. Then SQLite
on the other hand is entirely binary format, making it not very friendly to version control. Moreover, this is probably completely overkill to hold and provide game data.
Godot offers something interesting, however. When we save a resource using its default extension (.tres), then the data is saved in text format. Nice for version control. Yet, there is a feature that converts text resources into binary data when the project is exported. This feature effectively brings us the convenience of being able to deal with version control during development while also taking full advantage of the faster loading times provided by binary storage.
In Godot 3.x this feature used to be off by default. But now in Godot 4.x it's on. To be more specific, it can be found in Project Settings -> Editor -> Export -> Convert Text Resource to Binary
.
All that said, the screenshot bellow show the editor without a loaded database:
To begin editing data we obviously need a database resource. There are two obvious option to create one:
Create New -> Resource
. In the window choose GDDatabase as type then Create. At this point you will need to specify the name of the new file then click Save.Note the extension of the file, tres
or res
. The first one will result in the resource storing the data in text format, while the second will force Godot to store data in binary format. As I have mentioned in the introduction, part of the reason I chose to work with Resource
for the database implementation is related to this "text vs binary" format.
Anyway, once the new database is created, if you chose option (1) to create the resource, you will need to load the file by either double clicking it or using the Open DB button in the Database editor pane. If you go with option (2) then the new file will automatically be loaded in the editor.
Once a valid database resource is loaded, the "+" button above the table list will be enabled, allowing us to create new tables associated with the loaded database:
Clicking this button brings a dialog that allows setup of the new table:
The first field of this dialog is the table name, which should be pretty self explanatory. The name of the table can be changed after its creation and I will show how shortly.
Next is the File Name. This should be self explanatory as well, but note that checkbox after the field, the Embed. If this is enabled then thew new table data will be directly stored within the database resource itself. Otherwise a new resource file exclusively for the able will be placed alongside the database. Note that currently there are only two ways to create binary data tables:
DBTable
resource by right clicking the File System dock then Create New -> Resource
. In this case the file name must have res as extension instead of tres.The ID type can be chosen between Integer or String. While this is entirely up to you, just know that it cannot be changed after the table is created. If the Editable checkbox is disabled then the ID
value will become a read only field within the table editor.
For the next screenshot I have created two tables, one embedded and the other in a separate file, mostly to show how the UI displays the information. Note that the first table (another
) contains its file name written bellow the table name. HOvering the mouse above the file name will bring a tooltip wit the ful path to the resource. The second table (something
) is embedded and this information is shown bellow its name with the <Embedded> string:
In this image the something
table is selected and this fact is shown by the highlight border around the table information. Each table's box contains two buttons. The first button's text matches the name of the table. The second contains a '-'.
Clicking the "name button" brings a dialog that allows us to edit the name of the table. In it the "OK" button will be disable if the new name is empty, corresponds to an existing table in the opened database or is equal to "id" (case insensitive). This is the dialog when attempting to set an already existing name:
As for the "-" button, it is used to request the editor to remove the table from the database. A confirmation dialog will be shown. Please note that if the table is stored in its own file then it will not be deleted, just not handled by the database resource. However, if it's embedded then the data will be completely removed and there is no way to revert it (at least not without an external data recovery tool).
Right above the table display itself there are a few buttons. More specifically for this case are "magnifier glass", add column and add row. Shortly I will talk about the "magnifier". On a fresh new table the Add Row button will be disabled because there are no columns. Wait, how about that "ID" column? Well, indeed, it's shown as a column but it's not a "real one". The "ID" is used as an alternative to row index that allows us to reach/query the desired data within the table. Here are the buttons:
Now, clicking the Add Column button brings a dialog that allows us to setup its initial state:
The Column name
field should be pretty self explanatory, right? The type field allows selecting the value type of the column. It is a drop down menu offering the following options:
As you can see, there are several value type options to be chosen from. Some of these types cannot be changed after the column is created. This fact will be shown on the dialog in case such option is selected. That said, there are a few things that are worth mentioning about some of those types (I will skip the self explanatory ones). Expand the contents for the explanation:
Unique String
, Unique Integer
External String ID
, External Integer ID
Random Weight
Texture
, Audio
Generic Resource
To continue with the example, I have added three columns into the another table. Those are of string, texture and audio types, respectively. After that a few rows just to be able to show some details. The image bellow showcases this. Things to note in there:
In the something table I have added two columns, one of external string id and the other for random weight. The add column dialog for the external id is shown bellow:
The idea here is to show how a column can point to values in a different table. After adding a few rows, I changed the weight value of the second row to 0.5 to exemplify some things that I want to explain about the Random Weight column shortly. That said, this is how the table looks like:
Note the external
column. Each cell contains a "..." button. Clicking it will bring a dropdown menu with a preview of the row values of the referenced table. This dropdown also provides a filter input box, which should help find the desired row. The text will match ID or any other cell. This is how the dropdown looks like:
When an external row is selected, the label of the button will be changed to the ID of the selected one. The trash bin button will appear allowing us to clear the cell value. Above the button will appear a preview of each column/value pair corresponding to the selected row, like in the image bellow:
Now for the random weight column. If you did read the Weighted Random Selection With Godot tutorial, then you will know that the random weight is just a relative probability between the candidates. In the case of this table, rows 1 and 3 have equal change, while row 2 has half the chance of any of the other two rows. This is important to know when you start assigning the weights. As I have mentioned, cells of this type provide some useful information. If you hover the mouse above rows 1 and 3 then you will notice that both have the same probability (shown in the tooltip). And if you hover above row 2 then the tooltip will show that it has half of that probability.
Note that in this specific case there is rounding error caused by floating point math, resulting in the probabilities shown being different from 40/20/40
, but something very very close to that.
Notice the magnifier buttons. Toggling one of those will reveal/hide the input box allowing a "filter" text to be used. In the left panel it helps find the desired table. In the right panel it will help filter out rows in the selected table. One thing to note here is that specific columns can be removed or included in the search by simply toggling on/off the corresponding titles. In the image bellow the column "name" will not be considered when searching for the entered filter.
You may have noticed the Database File button becomes enabled as soon as a database resource is loaded. Clicking this button displays information related to the database itself, like how many tables are in it along with some more information about each of the tables. This is the window following the example up to this point:
Databases can be exported to JSON. Clicking the Export button, which is at the top right corner of the database editor, brings the Export Database dialog. It provides a few settings that determine how the JSON will be formatted, as well as where the file(s) will be saved. The first thing in this dialog is a checkbox, Monolithic
. If this is on then all the data will be stuffed into a single .json file, which will match that of the database resource itself. If this is off, however, each table will be placed in its own .json file, which will have filename matching that of the table itself. Files will always be saved alongside each other, in the specified directory. Indentation can be configured. The screenshot bellow shows the Export Database dialog:
The UI provides a relatively easy way to fill the database with data. How about retrieving it within the game logic? For that the database offers a few functions meant to obtain the required values. To access theses functions we obviously need to load the database:
var db: GDDatabase = load("res://resources/sample.tres")
There are two functions that can be used to retrieve rows:
get_row_from(table_name: StringName, id: Variant, expand: bool = false)
get_row_from_by_index(table_name: StringName, index: int, expand: bool)
The only difference between the two functions is the second argument. In (1) we have to specify the row ID, while in (2) we specify the row index. Ideally we should retrieve rows by ID, as the index can potentially change, specially after sorting rows. That said, on both cases the name of the table from where the row should be taken is specified by the first argument table_name
.
The last argument, expand
, is only meaningful if the table table_name
references another one. In that case, if this parameter is true
then the IDs of the other table will be substituted by the values of the corresponding row.
That said, the return value is a Dictionary
containing a key for each column in the table, holding the associated cell's value.
To select a random row and return it:
randomly_pick_row(table_name: StringName, expand: bool, random_number_generator: RandomNumberGenerator = null)
Again, the name of the table from where the row should be taken must be specified, with table_name
. The expand
parameter has the exact same functionality of the two previously mentioned functions. Lastly, the random_number_generator
allows us to use an arbitrary random number generator. This brings more control over the seed and state of the generator itself. The returned dictionary is also the same of the two functions to retrieve rows.
Important to note is that if the table does not contain random weights, a random index will be chosen where, in theory, each row has the same probability to be chosen. Otherwise the weight system will be used.
Now notice that those functions are accessed through the database resource itself, which internally retrieves the table resource, which is a DBTable
. Depending on how many times you need to obtain data from tables, performing those "queries" may negatively impact performance. IF that's the case then maybe it's better to first manually obtain the table with get_table()
function then extract the necessary data from it.
Specifically from the DBTable
returned by get_table()
, there are three functions that somewhat match the mentioned ones to retrieve row data:
get_row(row_id: Variant)
get_row_by_index(row_index: int)
pick_random_row(random_number_generator: RandomNumberGenerator = null)
Note the fact that none of those functions offer the expand
parameter. This occurs because the "wiring" is performed by the database resource.