持久化
Sinespace API 提供了一系列 API 函数,使我们能够创建不同类型的持久化数据。作为脚本开发者,我们需要选择最适合当前任务的持久化类型,甚至可以混合使用。在本指南中,我们将逐一介绍所有不同的类和函数,并解释如何使用它们。
参考
以下两个表格简要说明了具有持久化功能的不同类型的类和成员之间的区别,随后我们将对此进行更详细的展开说明。
| 类 | 成员 | 持久化类型 |
|---|---|---|
| SNetwork (客户端脚本) | SetRegionProperty GetRegionProperty SetShardProperty GetShardProperty HasShardProperties | 半永久区域 半永久区域 半永久分片 半永久分片 半永久分片/区域 |
| SPersistence (客户端脚本) | UpdateInfo RetrieveValue SetValue UpdateRegionInfo RetrieveRegionValue SetRegionValue | 永久玩家 永久玩家 永久玩家 永久区域 永久区域 永久区域 |
| SShared (客户端脚本) | SetSuperGlobal GetSuperGlobal | 半永久查看器 半永久查看器 |
| SDatabase (服务器脚本) | GetPlayerValue SetPlayerValue GetRegionValue SetRegionValue | 永久玩家 永久玩家 永久区域 永久区域 |
| 持久化类型 | 描述 |
|---|---|
| 半永久区域 | 持久化数据将持续到区域关闭(所有玩家离开区域后)。 持久化应用于整个区域(如果有任何分片处于活动状态,则包括所有分片)。 |
| 半永久分片 | 持久化数据将持续到分片关闭(所有玩家离开分片后)。 持久化仅应用于特定的分片。 |
| 半永久查看器 | 持久化数据将持续到查看器关闭。 持久化应用于查看器。 |
| 永久玩家 | 持久化数据将永久保存。 持久化应用于特定玩家。 |
| 永久区域 | 持久化数据将永久保存。 持久化应用于区域。 |
什么是持久化?
发送网络消息对于一次性事件来说很酷,例如:玩家挥手打招呼,这仅在几秒钟内相关。然而,有时我们希望进行一些持续时间超过几秒钟的更改,例如:打开一个窗口。
如果我们发送一条网络消息向所有玩家广播“窗口 A 现在已打开”,当新玩家进入我们的区域时,他们会错过这条网络消息,因此在他们的查看器中窗口 A 仍然是关闭的。
这就是为什么我们需要使场景中的某些更改能够持久化,无论是永久性的,还是至少直到所有玩家离开区域为止。
SNetwork 类
SNetwork 类包含多种网络功能,其中有 4 个函数和 1 个属性与持久化相关。
- SetRegionProperty/GetRegionProperty
- SetShardProperty/GetShardProperty
- HasShardProperties
分片与区域
你会注意到有些函数适用于分片,有些适用于区域。那么“分片”到底是什么呢?
分片是 Sinespace 中的一项功能,其中一个****区域可以拥有多个****分片(实例),非常类似于平行宇宙,一个分片上的玩家无法看到另一个分片上的玩家。
出于本入门指南的目的,我们不会深入探讨多分片体验的脚本编写,但了解这些区别已经足够了。
什么是分片/区域属性?
对于每个打开的分片/区域(里面有玩家),服务器上会创建一个半永久存储,用于保存属性。所有使用这些函数的脚本都将访问此相同的存储来读写属性。此存储是键和值的集合。当分片/区域关闭(所有玩家离开后),此存储也将关闭。
你应该使用什么“键”?
你通常希望你的键是私有的,这样其他脚本就无法访问它。你也很可能希望你的键特定于某个 GameObject。
我们可以通过以下方式实现上述两点:
thisGameObject = Space.Host.ExecutingObject
KEY = 'SomethingSecret' .. thisGameObject.ID
现在,我们创建了一个既私有又特定于 GameObject 的键。稍后我们将使用它来访问属性存储并读写特定值。
HasShardProperties
当玩家加入区域时,想要立即访问属性数据库的脚本可能会在一个非常短暂的时间段内触发,此时我们尚未连接到属性存储。这就是为什么我们要使用 HasShardProperties 来检查存储数据库是否已连接并准备好进行读写。(HasShardProperties 用于分片属性和区域属性)
让我们通过添加一个协程(在并行线程中运行我们的代码)来扩展之前的代码,以处理这个问题:
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)
设置和获取属性
分片属性和区域属性的执行方式完全相同。因此,出于本指南的目的,我们将继续使用区域属性。
注意:以下两个函数的使用限制为每秒 10 次调用。(在 Breakroom 中为每秒 20 次调用)
获取区域属性:value = Space.Network.GetRegionProperty(KEY)
注意:有时区域属性可能尚未设置,因此它可能会返回 Nil。我们需要对此进行检查。
设置区域属性:Space.Network.SetRegionProperty(KEY, 'A Value')
注意:区域属性值是字符串数据类型,因此你必须将数字或表转换为字符串才能存储它们。
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)
窗口示例
让我们编写一个窗口的脚本,当点击时打开/关闭窗口并更新其区域属性,同时持续读取区域属性,以防其他玩家打开/关闭窗口。
我们将在脚本中添加两个主要部分。第一部分持续读取区域属性以监控和响应更改。第二部分响应点击并更新保存窗口状态(打开/关闭)的区域属性。
第一部分
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)
第二部分(及最终代码)
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 类
SShared 类(客户端脚本)包含多种脚本间通信功能和本地查看器存储功能,其中有 2 个函数与持久化相关。
- SetSuperGlobal
- GetSuperGlobal
什么是超级全局变量?
在 SShared 类中,“全局变量”是本地玩家查看器内的存储,所有脚本均可访问。
“超级全局变量”是相同的存储,但是半永久性的;即使更改区域,它也会继续存在,直到查看器关闭。
在此示例中,我们将创建一个可点击的对象,将我们传送到一个区域,在该区域中我们将有一个脚本,根据我们来自何处做出反应。
你应该使用什么“命名空间”和“键”?
与我们之前的情况不同,这些函数存储在特定玩家的查看器中,并且可能旨在被多个脚本访问(可能是我们的其他脚本或其他开发者的脚本)。它本质上特定于玩家,并且可能特定于某个项目或操作/目标。
这就是为什么它使用命名空间、键和值来访问,而不仅仅是键和值。命名空间可以是一种访问器,不必是私有的,同时我们仍然可以拥有一个私有的键访问器。
--SCRIPT A
NAMESPACE = 'space.sine.apidocs.persistenceguide' --this format is just a naming convention
KEY = 'Teleported From'
设置和获取超级全局变量
设置超级全局变量时,值不必是字符串。它接受类型为 DynValue 的值,这意味着它可以是字符串、数字或表等...
我们这样设置超级全局变量:
Space.Shared.SetSuperGlobal('namespace', 'key', AVariable)
我们这样获取超级全局变量:
value = Space.Shared.GetSuperGlobal('namespace','key')
传送器示例
我们将为此示例创建 2 个脚本。第一个脚本将负责传送我们并设置超级全局变量,第二个脚本将在目的地访问我们的超级全局变量并对我们来自何处做出反应。
我们将通过扩展上面的代码来开始创建第一个脚本,添加一个可点击对象,并使其设置一个超级全局变量,携带我们需要传递到第二个区域的信息。出于本指南的目的,我们将通过表而不是单个变量传递信息。
--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
现在我们有了一个对象,点击时会设置超级全局变量,然后传送我们。在目的地区域,我们现在将创建处理我们携带的信息(区域名称和 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 类
SPersistence 类有 6 个函数,全部用于创建永久持久化。
- UpdateInfo/UpdateRegionInfo
- RetrieveValue/RetrieveRegionValue
- SetValue/SetRegionValue
什么是区域值和玩家值?
此类中一半的函数创建永久区域持久化,另一半创建永久玩家持久化。
如果函数名称中包含单词 region,则表示它创建区域持久化。如果不包含,则表示它创建玩家持久化。
区域/玩家值是永久存储中的值。区域值位于与区域链接的永久存储中,而(玩家)值位于与运行脚本的玩家链接的永久存储中。
就使用而言,区域值相当于 SNetwork 类的永久版本。而玩家值相当于 SShared 类。
你应该使用什么“键”?
对于区域值,我们将以与 SNetwork 类相同的方式创建键。
对于(玩家)值,我们将以与 SShared 类相同的方式创建键。
为什么我们需要“更新区域信息”?
此类的独特功能是它是一个客户端脚本类,但使我们能够在不使用服务器脚本的情况下创建持久化。
用于实现此目的的存储是外部的,这意味着我们必须首先使用 UpdateRegionInfo() 函数请求存储发送其最新值的副本(在尝试访问这些值之前)。
我们这样做:
function WhatToDoWhenInfoIsReady()
--We access the data we need from inside here
end
Space.Persistence.UpdateRegionInfo() --or UpdateInfo() for Player version
与半永久区域属性不同,此函数调用不是即时的。因为它访问外部永久存储,我们需要挂钩一个函数,该函数将在信息到达后被调用。
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()
此功能的响应速度较慢或可变,因此不建议像我们对 SNetwork 区域属性那样快速调用它。因此,我们可能不得不将代码与其他类型的持久化混合使用。
智能灯光示例
SDatabase 类
SDatabase 类相当于 SPersistence 类,唯一的区别是它只能通过服务器脚本访问。服务器脚本无法访问 SPersistence 类。
服务器脚本本身也具有半持久性。这意味着如果你在服务器脚本中执行 aVariable = 5,此变量将持续存在,直到所有玩家离开区域。这使得 SDatabase 类的使用比 SPersistent 类更容易,因为不需要混合使用持久化方法。
服务器脚本的一个缺点是它们只能放置在家具物品中,并且需要在区域上加载才能使用(与客户端脚本不同)。
由于此类几乎是 SPersistence 类的精确副本。我们将仅概述以下差异(以避免重复)。
SDatabase 和 SPersistence 之间的差异
这两个类之间仅有的两个小差异是:
- SPersistence 中的 SetValue/GetValue 函数,在 SDatabase 中是 SetPlayerValue/GetPlayerValue
- GetRegionValue 和 GetPlayerValue 有一个额外的 onSave 参数,这是一个钩子,让我们知道值已成功设置。
--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)
谢谢
感谢完成本指南。如果你遇到任何问题或希望提出改进建议, 请在此页面上进行。