Persistence

Guide on how to script persistence in Sinespace

The Sinespace API has a range of API functions that enable us to create different types of persistence. As scripters, we have to choose the type which is more suitable for the task at hand, or even mix and match. In this guide we will go through all the the different classes and functions and explain how to use them.

Reference

These two tables briefly explains the difference between the different types of classes and members that have persistence functionality, and we will expand even more after that.

What is persistence?

Sending Network Messages is cool for one off events, for example: a player waving Hello, which is relevant for only a few seconds. However, sometimes we want to make changes that are relevant for more than just a few seconds, for example: a window being opened. If we send a Network Message to broadcast to all players that Window A is now open, when a new player enters our region, they would have missed that Network Message, and Window A would still be closed for them in their viewer. This is why we need the ability to make certain changes to the scene Persist, either permanently or at least until everyone's left the region.

SNetwork class

The SNetwork class has a variety of networking functions but has 4 functions and 1 property which are related to persistence.

  • SetRegionProperty/GetRegionProperty

  • SetShardProperty/GetShardProperty

  • HasShardProperties

Shard Vs Region

You'll notice some functions apply to Shards and some to Regions. What are "shards" anyways?

A Shard is a feature in Sinespace where one Region can have multiple Shards (instances), very much like parallel universes, where players on one Shard cannot see players on another Shard.

For the purpose of this beginner guide, we won't go into scripting multi-Shard experiences, but it's enough to understand the differences.

What are Shard/Region properties?

For every Shard/Region that is open (there's players inside), there's a semi-permanent storage created on the server that holds properties. All scripts using these functions will access this same storage to read and write properties. This storage is a collection of keys and values. This storage will close after the Shard/Region is closed (after all players leave).

What "key" should you use?

You mostly want your key to be a private one, so other scripts cannot access it. You are also most likely to want your key to be specific to a GameObject.

We can achieve both of the above in this way:

thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID

Now we've created a key which both private and specific to a GameObject. We will use it later to access the properties storage and read/write a specific value.

HasShardProperties

When a player joins the region, scripts that want to immediately access the properties database might fire off during a very tiny period where we haven't yet connected to the properties storage. This is why use HasShardProperties to check if the storage database is connected and ready for read/write. (HasShardProperties is used for both Shard properties and Region properties)

Let's expand our previous code by adding a coroutine (run our code in a parallel thread) to take care of this:

thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID

function TheCoroutineFunction()
  while not Space.Network.HasShardProperties do --this while loop keeps looping until HasShardProperties returns true
    coroutine.yield(0) --this ensures our loop runs once per frame, otherwise it will be crashed for running too long on a single frame
  end

--At this point we have the confirmation we need to begin read/write

end

Space.Host.StartCoroutine(TheCoroutineFunction)

Setting and Getting properties

Shard properties and Region properties are executed in the exact same way. So for the purpose of this guide we will continue using Region properties only.

Note: The below to functions are usage limited to 10 calls per second. (20 calls per second on Breakroom)

To get a Region property: value = Space.Network.GetRegionProperty(KEY)

Note: Sometimes a Region property may have not been set before, so it might return Nil. We'll have to add a check for that.

To set a Region property: Space.Network.SetRegionProperty(KEY, 'A Value')

Note: Region property values are a string datatype, so you'll have to convert your numbers or tables to a string to store them.

thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID

function TheCoroutineFunction()
  while not Space.Network.HasShardProperties do --this while loop keeps looping until HasShardProperties returns true
    coroutine.yield(0) --this ensures our loop runs once per frame, otherwise it will be crashed for running too long on a single frame
  end
--below part only runs after weve got our confirmation
  value = Space.Network.GetRegionProperty(KEY) --how to read
  Space.Network.SetRegionProperty(KEY, 'A Value') --how to write
end

Space.Host.StartCoroutine(TheCoroutineFunction)

The window example

Let's write a script for a window which when clicked opens/closes and updates it's Region property, and also keeps reading the Region property incase another player opens/closes the window.

We are going to add to our script two main parts. First part is constantly reading the Region property to monitor and react to changes. Second part reacts to clicks and updates the Region property which holds the state of the window (open/close).

The First Part

thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID
refWindow = Space.Host.GetReference("window") -- this should be linked in the Scripting Runtime "references" section


function TheCoroutineForFirstPart()
  while true do
    local value = Space.Network.GetRegionProperty(KEY) --how to read

    if value = nil then
      --we're going to default the window to closed if Nil
      refWindow.Active = true
    elseif value == "Open" then
      --if it's "Open" we're going to make the window disappear
      refWindow.Active = false
    elseif value == "Closed" then
      --if it's "Closed" we're going to make the window appear
      refWindow.Active = true
    end
    coroutine.yield(0.2) --We'll make this run every 0.2 seconds. That's should be quick enough and we also have a limit to mind.  
  end
end


function TheCoroutineFunction()
  while not Space.Network.HasShardProperties do --this while loop keeps looping until HasShardProperties returns true
    coroutine.yield(0) --this ensures our loop runs once per frame, otherwise it will be crashed for running too long on a single frame
  end
--below part only runs after weve got our confirmation
  Space.Host.StartCoroutine(TheCoroutineForFirstPart) -- we can no start our coroutine which consist of the first part

  Space.Network.SetRegionProperty(KEY, 'A Value') --how to write
end


Space.Host.StartCoroutine(TheCoroutineFunction)

The Second Part (and final code)

thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID
refWindow = Space.Host.GetReference("window") -- this should be linked in the Scripting Runtime "references" section


function TheCoroutineForFirstPart()
  while true do
    local value = Space.Network.GetRegionProperty(KEY) --how to read

    if value = nil then
      --we're going to default the window to closed if Nil
      refWindow.Active = true
    elseif value == "Open" then
      --if it's "Open" we're going to make the window disappear
      refWindow.Active = false
    elseif value == "Closed" then
      --if it's "Closed" we're going to make the window appear
      refWindow.Active = true
    end
    coroutine.yield(0.2) --We'll make this run every 0.2 seconds. That's should be quick enough and we also have a limit to mind.  
  end
end

function TheOnClickFunctionForSecondPart() --Second Part code which will update region properties when object is clicked
  local checkFirst = Space.Network.GetRegionProperty(KEY)

  if checkFirst == nil then --if it's nil, this window was never used, so we update property to open 
    Space.Network.SetRegionProperty(KEY, "Open")
  elseif checkFirst == "Open" --if its "Open" we update it to "Closed"
  Space.Network.SetRegionProperty(KEY, "Closed")
elseif checkFirst == "Closed" --if its "Closed" we update it to "Open"
end 

end

function TheCoroutineFunction()
  while not Space.Network.HasShardProperties do --this while loop keeps looping until HasShardProperties returns true
    coroutine.yield(0) --this ensures our loop runs once per frame, otherwise it will be crashed for running too long on a single frame
  end
--below part only runs after weve got our confirmation
  Space.Host.StartCoroutine(TheCoroutineForFirstPart) -- we can no start our coroutine which consist of the first part

  Space.Network.SetRegionProperty(KEY, 'A Value') --how to write
end


thisGameObject.AddClickable() --we make this object a clickable
thisGameObject.Clickable.OnClick(TheOnClickFunctionForSecondPart) --we add our Second Part new code to be executed On cLick
Space.Host.StartCoroutine(TheCoroutineFunction)

SShared class

The SShared class (client scripting) has a variety of inter-script communication functions and local viewer storage functions but has 2 functions which are related to persistence.

  • SetSuperGlobal

  • GetSuperGlobal

What is a Super Global?

A "Global" in the SShared class is a storage within the local player's viewer accessible to all scripts.

A "Super Global" is the same storage but semi-permanent; it will keep existing even when changing regions, until the viewer is closed.

For this example, we will creator a clickable object that teleports us to a region, and on that region we will have a script that reacts depending where we came from.

What "namespace" and "key" should you use?

Unlike our previous case, these functions are storing on a specific player's viewer, and they could be meant to be accessed by multiple scripts (could be our other scripts or other scripters' scripts too). It's inherently specific to a player, and likely to a specific project or operation/objective.

That's why it's accessed using a namespace, key and value, vs just key and value. The namespace could be a type of accessor which doesn't have to be private, while still having a key accessor that we can make private.

--SCRIPT A
NAMESPACE = 'space.sine.apidocs.persistenceguide' --this format is just a naming convention
KEY = 'Teleported From'

Setting and Getting Super Globals

When Setting a Super Global, the value need not be a string. It accepts a value of type DynValue, which means it can be a string, number or table etc...

We set a Super Global like this:

Space.Shared.SetSuperGlobal('namespace', 'key', AVariable)

We get a Super Global like this:

value = Space.Shared.GetSuperGlobal('namespace','key')

The teleporter example

We will create 2 scripts for this example. The first script will be the one teleporting us an Setting a Super Global, and the second one will be at the destination and will access our Super Global and react to where we teleported from.

We will start creating our first script by expanding on our code above by adding a clickable and make it Set a Super Global with the information we need to carry on to the second region. For the purpose of this guide we will pass our information in a Table rather than a single variable.

--Script A
NAMESPACE = 'space.sine.apidocs.persistenceguide' --this format is just a naming convention
KEY = 'Teleported From'
thisGameObject = Space.Host.ExecutingObject

function OnClickFunction()
  
  local tableValue = {id = Space.Scene.RegionID, name = Space.Scene.Name} --we create a Table that holds both RegionID and Region Name 
  Space.Shared.SetSuperGlobal(NAMESPACE,KEY, tableValue) --we Set our Super Global
  Space.Scene.PlayerAvatar.Teleport(0000000) -- We teleport. You'll have to replace the zeros with your region ID

end

thisGameObject.AddClickable() --we make the object clickable
thisGameObject.Clickable.OnClick(OnClickFunction) --we hook our OnClickFunction to the clickable's OnClick event

Now we have an object, that when clicked, Sets a Super Global and then teleports us. On the destination region we will now create the script that processes our carried over information (regiond name and ID).

--Script B
NAMESPACE = 'space.sine.apidocs.persistenceguide' --this format is just a naming convention
KEY = 'Teleported From'

getTableValue = Space.Shared.GetSuperGlobal(NAMESPACE, KEY)

Space.Log("You have arrived from region ID: ".. getTableValue.id .. " with Name: " .. getTableValue.name)

SPersistence Class

The SPersistence class has 6 functions that are all used for creating permanent persistence.

  • UpdateInfo/UpdateRegionInfo

  • RetrieveValue/RetrieveRegionValue

  • SetValue/SetRegionValue

What are Region Values and Values?

Half the functions in this class create Permanent Region persistence, and the other half create Permanent Player persistence.

If the function has the word region in it, that means it creates Region persistence. If it does not, that means it creates Player persistence.

Region/Player values are values in a permanent storage. Region values are in a permanent storage which is linked to the region, and (Player) Values are in a permanent storage linked to the player running the script.

In terms of usage, Region Values are the equivalent to the permanent version of the SNetwork class. Whereas Player values are the equivalent to the SShared class.

What "key" should you use?

In the case of Region values we will be creating keys in the same way as we did for the SNetwork class.

In the case of (Player) values we will be creating keys in the same way as we did for the SShared class.

Why do we need to "Update Region Info"?

The unique feature of this class is that it is a client scripting class, but gives us the ability to create persistence without the use of a server script.

The storage used to achieve this is an external one, which means we have to start with using the UpdateRegionInfo() function to request the storage to send us a copy of it's most recent values (before trying to access these values).

We do it this way:

 function WhatToDoWhenInfoIsReady()
 --We access the data we need from inside here
  end
 Space.Persistence.UpdateRegionInfo() --or UpdateInfo() for Player version

Unlike semi-permanent region properties, this function call is not as instant. Because it accesses an external permanent storage, we need to hook a function which will be called once the info has arrived.

thisGameObject = Space.Host.ExecutingObject
KEY = "Secret" .. thisGameObject.GlobalID

function WhatToDoWhenInfoIsReady()
  value = Space.Persistence.RetrieveRegionValue(KEY) --Getting a region value
  Space.Log(value) --should print "Something"
end

Space.Persistence.SetRegionValue(KEY, "Something") --Setting a region value
Space.Persistence.UpdateRegionInfo() 


The slower or variable response speed of this functionality means that it's not recommended to be called rapidly like we did with SNetwork region properties. Therefore we might have to mix and match our code with other types of persistence.

Smart Lights Example

Visit our Smart Lights sample project page.

SDatabase Class

The SDatabase class is the equivalent of the SPersistence class but the only difference being that it is accessed only through Server Scripts. Server scripts do not have access to the SPersistence class.

A server script is also inherently semi-persistent. That means if you do this aVariable = 5 in a server script. This variable will be persistent until all players leave the region. This makes the usage of the SDatabase class much easier than the SPersistent class due to not having to mix and match persistent methods.

A downside to server scripts is that they can only be placed in furniture items and need to be loaded on a region to be used (unlike client scripts).

Since this class is almost an exact copy of the SPersistence class. We will only be outlining the differences below (to avoid being repetitive).

Differences between SDatabase and SPersistence

The only two small differences between these to classes are:

  1. Functions SetValue/GetValue in SPersistence, are SetPlayerValue/GetPlayerValue in SDatabase

  2. GetRegionValue and GetPlayerValue have an extra onSave parameter which is a hook that lets us know when the value as been successfully set.

--Setting a region value using SPersistence in a client script
Space.Persistence.SetRegionValue(KEY, "Something") 

--vs

--Setting a region value using SDatabase in a server script
function onSave()
  --do something when set is succesful
end
Space.Database.SetRegionValue(KEY, "Something", onSave) 

Thank You

Thanks for completing this guide. If you run into any issues or would like to recommend improvements, please do so on this page.

Last updated

Sinespace® is a registered trademark of Sine Wave Entertainment Ltd, All Rights Reserved.