User Save Data

This isn’t much of a package, but it’s something I end up writing every time I get going on a game, so I figured I’d bundle it up. It’s just a wrapper around the save data api to make it isomorphic.

User Save Data provides an isomorphic way to persist data on a user. A lot of the time, I want an item that a user is interacting with to save something to the user - like the time last interacted, or something like that. Something that really only matters to the world entity.

This is my solution.

Installation

Search for User Save Data in community packages.
Drag the User Save Data template onto the user User.

Usage

This package makes 2 methods available to you.

SaveData(key, value)

SaveData accepts a string key as the first argument, followed by a string or number as the second argument.

This method works on both the server and the client.

GetData(key)

GetData accepts a single key, and will return the data for this key.

This method works on both the server and the client.

onSaveDataReady

The saveDataScript exposes an event called onSaveDataReady that will fire on the server and local client when the data has been retrieved from the server.

This is useful if you have something in your game that depends on save data, and you need to wait for it to be loaded before initializing it. For example, take a widget:

-- During initializing of the widget, we want to show how much money the user has,
-- but if we do this before the data is loaded, we'll get the incorrect value
function MyWidget:LocalInit()
  self.widget = self:GetEntity().myWidget

  -- We listen to the onSaveDataReady event here, allowing us to only initialize the frontend
  -- when the data is available
  self:GetEntity().saveDataScript.properties.onSaveDataReady:Listen(self, "UpdateWidget")
end

function MyWidget:UpdateWidget()
  self.widget.js.data.money = self:GetEntity().saveDataScript:GetData("money")
end

Example

This is an example pulled out of one of my script. It’s an interactable object that a user can interact with after some amount of time. I’ve smooshed it together for readability reasons.

Consider this method:

function TriggerPaymentScript:TimeLeft(user)
    -- Fetch the time since last update off the user's save data
    local lastTime = user.saveDataScript:GetData("trigger-last-used")

    local cooldownSeconds = self.properties.cooldownTime * 60
    local timePassed = GetWorld():GetUTCTime() - lastTime

	return cooldownSeconds - timePassed
end

This method interacts with the user’s save data. The cool thing about the User Save Data package is we can actually use this on the client and the server.

So this code runs on the server when a user interacts with an entity in the world.

-- User interacts with something, it runs this
function TriggerPaymentScript:PerformActivity(player)
	local user = player:GetUser()

	local diff = self:TimeLeft(user)

	if diff < 0 then
		-- Do Stuff
	else
		-- Don't do stuff 
	end
end

Now, the important part - this entity also has a world widget.

function TriggerPaymentScript:UpdateTimer(user)
  -- This is important, we're running this function on the client
  if IsServer() then
    self:SendToAllClients("UpdateTimer", user)
    return
  end

  if user ~- GetWorld():GetLocalUser() then return end 
  
  -- We're using the _same_ function we used in PerformActivity to get the time left,
  -- even though save data is only accessible on the server, User Save Data lets us access it
  self.widget.js.data.timeLeft = self:TimeLeft(user)
end

So we get to share the same code to calculate the time left on both the server and the client, and get to keep all of the related logic all nicely encapsulated on the concerned world entity, rather than mixing and matching between the world and the user

2 Likes

Added an event to the script:

The saveDataScript exposes an event called onSaveDataReady that will fire on the server and local client when the data has been retrieved from the server.

This is useful if you have something in your game that depends on save data, and you need to wait for it to be loaded before initializing it. For example, take a widget:

-- During initializing of the widget, we want to show how much money the user has,
-- but if we do this before the data is loaded, we'll get the incorrect value
function MyWidget:LocalInit()
  self.widget = self:GetEntity().myWidget

  -- We listen to the onSaveDataReady event here, allowing us to only initialize the frontend
  -- when the data is available
  self:GetEntity().saveDataScript.properties.onSaveDataReady:Listen(self, "UpdateWidget")
end

function MyWidget:UpdateWidget()
  self.widget.js.data.money = self:GetEntity().saveDataScript:GetData("money")
end