-
-
Notifications
You must be signed in to change notification settings - Fork 3
Getting Started
Welcome to the Getting Started guide for the Locker Framework. Here, you will learn how to use the basic functionality of saving and loading data using this framework in a Godot project.
The Locker plugin was developed in a way that makes it easy to import it to existing projects without requiring significant modifications in their structure.
With that in mind, for this tutorial, let's assume we have an existing project in which we want to be able to save and load the data of a player character. For that, we have to decide what data specifically we want to persist. In this example, let's say we want to store the player's position, health, modulate color and nickname.
Now, let's imagine how the player's scene tree is organized:
for this example, the Player
is a node that extends the CharacterBody2D
and is the root of its scene.
This node has a Sprite
child which extends the Sprite2D
.
This Sprite
stores the player's texture and its modulate
color.
Screenshot of the Player scene tree
Now for the health and nickname, let's say they are kept inside a
Stats
resource, which is defined as below:
class_name Stats
extends Resource
@export var nickname: String = ""
@export var health: float = 0.0
An instance of that Stats
is stored by the Player
node in a stats
property, using the script below:
class_name Player
extends CharacterBody2D
const SPEED: float = 256.0
@export var stats: Stats
func _process(_delta: float) -> void:
velocity = Input.get_vector("left", "right", "up", "down") * SPEED
move_and_slide()
The main way of accessing the data saved with this plugin is implemented
in a new Node, the LokStorageAccessor
,
so that it can simply be added to scenes that need to modify or access that data,
without requiring other big modifications to the structure of the scene.
Let's add this node to the Player
's scene:
Screenshot of the Player scene tree with the LokStorageAccessor
When we add a LokStorageAccessor
to the scene, a few warnings will appear.
Those warnings tell us that we need to set a valid id and version to our
new LokStorageAccessor
.
That id refers to a property of the LokStorageAccessor
class, which we can
see when we click on its node, and the version refers to a
LokStorageAccessorVersion
which we must set in the versions
Array
.
Screenshot of the LokStorageAccessor's properties
For the id
, we should set it to a unique String
that we won't use for
any other LokStorageAccessor
in the game, so that we avoid mixing data
from different LokStorageAccessors
when we save the game.
In this project, just setting it to "player"
should be okay, since we don't
have any other Players
in the game.
Before procceding to the version issue, let's also set the
dependency_paths
property with NodePaths
to important nodes that our
LokStorageAccessor
will need in order to save and load data.
In our case, it's important that it knows the Player
node and the Sprite
node, since it will need to access the Player
's global_position
, health
and nickname
, and also the Sprite
's modulate
color.
After defining that, the LokStorageAccessor
's properties should look like the following:
Screenshot of the LokStorageAccessor's properties with dependency_paths
Now, to address the version warning, we must set a
LokStorageAccessorVersion
in the versions
property of the
LokStorageAccessor
.
The LokStorageAccessorVersion
is a Resource
that describes how to gather
the data of a LokStorageAccessor
, so that it can be saved, and how to
use that data when it is loaded.
That definition should be implemented via the override of the
_retrieve_data
and _consume_data
methods of the
LokStorageAccessorVersions
.
This means we will have to write a script that extends that class in order
to override those methods.
To start that script, we need to define its class_name
and its superclass.
Since this is the first version of this LokStorageAccessor
, I recommend
setting its name to something like PlayerAccessorV1
.
class_name PlayerAccessorV1
extends LokStorageAccessorVersion
After that, we can start setting up the _retrieve_data
method.
This method receives a dependencies
Dictionary
and should return another
Dictionary
with the data that is supposed to be saved.
It's through that deps
parameter (short for dependencies
) that the
dependency_paths
we set before in the LokStorageAccessor
are passed,
so that we can access them and recover their data to save.
Despite having set those dependency_paths
as NodePaths
, in the deps
parameter they already come as Nodes
, so that we can reference them directly.
func _retrieve_data(deps: Dictionary) -> Dictionary:
var player := deps.get(&"player") as Player
var sprite := deps.get(&"sprite") as Sprite2D
var data: Dictionary = {}
var nickname: Variant = player.get_indexed(^"stats:nickname")
var health: Variant = player.get_indexed(^"stats:health")
data["position"] = var_to_str(player.global_position)
if nickname != null:
data["nickname"] = nickname
if health != null:
data["health"] = health
data["color"] = var_to_str(sprite.modulate)
return data
In the above script, we get a reference to the Player
and its Sprite
through the deps
parameter.
After that, we define a data
Dictionary
that will store the data that should
be saved and get the values of the player.stats.nickname
and the
player.stats.health
.
Finally, we save the player.global_position
, nickname
, health
and
sprite.modulate
in the data
Dictionary
and return it to be saved.
Note that we just set the nickname
and health
if they exist,
since they might be null
.
It's also important to note that when saving complex values like Vector2
and
Color
, we're using the
var_to_str
built-in Godot method, in order to make sure they are parsed to a
text friendly format.
This is necessary if we're using the LokJSONAccessStrategy
or the LokEncryptedAccessStrategy
,
which we are, by default.
Now, in the inverse path, we have to define the _consume_data
method,
which serves to specify how the game should handle the data loaded.
Similarly to the _retrieve_data
method, the _consume_data
also receives a
deps
parameter with those same dependencies.
Different from the _retrieve_data
, though, that's only the
second parameter of this method, with its first one being a res
Dictionary
(short for result
).
Also different from its saving counterpart,
this method should not return anything.
The res
parameter brings with it two keys:
the "status"
and the "data"
.
The "status"
key stores an Error
code value describing how went the
loading.
If it's Error.OK
, it means everything went fine.
The "data"
key is what we're interested in right now,
since that's where a Dictionary
with the loaded data itself is
located.
func _consume_data(res: Dictionary, deps: Dictionary) -> void:
var player := deps.get(&"player") as Player
var sprite := deps.get(&"sprite") as Sprite2D
var data: Dictionary = res.get("data")
var position: Variant = data.get("position")
var nickname: Variant = data.get("nickname")
var health: Variant = data.get("health")
var color: Variant = data.get("color")
if position != null:
player.global_position = str_to_var(position)
if nickname != null:
player.set_indexed(^"stats:nickname", nickname)
if health != null:
player.set_indexed(^"stats:health", health)
if color != null:
sprite.modulate = str_to_var(color)
In the script above, we start by getting references to the Player
and its Sprite
through the deps
parameter.
Following that, we get the loaded data
from the res
parameter.
After that, we procced to set the given data to their respective spots:
the "position"
goes to the player.global_position
, the "nickname"
goes to the player.stats.nickname
, the "health"
goes to the
player.stats.health
and the the "color"
goes to the sprite.modulate
.
Each one is only set if it exists, though,
that's why we check for their nullity.
It should be noted that the keys of the loaded properties should be the same
keys used when saving ("position"
, "nickname"
, "health"
and "color"
).
Also, it's important to note that, just like in the saving process,
we need a way to convert our data from String
back to variables
if we're using the LokJSONAccessStrategy
or LokEncryptedAccessStrategy
.
That's why we use the
str_to_var
method in the variables that we saved before using var_to_str
.
Now that we finished our LokStorageAccessorVersion
script, we should be able
to set a new instance of it in the LokStorageAccessor
versions
property.
When we do that, the LokStorageAccessor
's warnings should disappear and its
properties should look like the following.
Screenshot of the LokStorageAccessor's properties with a version set
With this, we're all set to start saving and loading data with the
Locker Plugin. All we need to do is call the save_data
and load_data
methods of the LokStorageAccessor
when we wish to execute those actions.
To test that, we can, for example, create a SaveButton and a LoadButton
in the scene and connect their pressed
signals to the methods mentioned.
After doing that, our scene tree should have a structure similar to the following:
Screenshot of the scene tree with the save and load buttons
Like mentioned before, the SaveButton
and LoadButton
nodes have to be
connected, respectively, to thesave_data
and load_data
methods of the
StorageAccessor
, similarly as it's shown below:
Screenshot of the connection of the SaveButton
In order to be able to see the effects of the saving and loading
operations with the color, nickname, and health properties,
we'll need to have some way of setting them in the game.
For that, I'm using use some of the Godot's built-in Control
nodes
that make that task easy with their signals.
I'm going to leave that part out of this tutorial since it doesn't really concern the Locker Plugin, but if you want to see how I implemented that, you can look in the repository.
When done with that, we should be able to successfully see the saving and loading in action.
At this point, we are now able to save and load our game using one
LokStorageAccessor
node.
That's not the most common way of realizing those operations using the
Locker framework, though.
Using the save_data
and load_data
methods directly from the
LokStorageAccessor
is fine if we only want to save and load THAT
accessor's data.
That's useful in multiplayer games, for example, when we want to
load only the data of a player that just joined the game.
In most cases, though, we'll want to save and load the entire game,
which we should do using the save_data
and load_data
methods of the
LokGlobalStorageManager
autoload.
This autoload is automatically created when the Locker plugin is
active.
Using the methods from that autoload, the data from all
LokStorageAccessors
currently active in the game can be involved
in the operations.
Since the LokGlobalStorageManager
is an autoload, we can't connect the
SaveButton
and LoadButton
's signals to it directly
through the editor, though.
In order to be able to do that, we can use the LokSceneStorageManager
,
which works like an interface of access to the LokGlobalStorageManager
.
To use this node, we have to add it to the scene tree, which will make it look something like the following:
Screenshot of the scene tree with the LokSceneStorageManager
After adding it, we should change the SaveButton
and LoadButton
's
signals to connect to the save_data
and load_data
methods of that new
LokSceneStorageManager
node.
When done with that, our basic saving and loading system should be working! 😀 Congratulations for setting up your first project with the Locker Framework!
Now, when clicking on the SaveButton
, the position
, nickname
, health
and color
of the player should be saved in the storage and when
clicking on the LoadButton
, that data should be recovered and sent back
to the player.
If you want to download an example project with the implementation of this tutorial, you can get it from this repository.