انتقل إلى المحتوى الرئيسي

الاستمرارية

توفّر Sinespace API مجموعة من دوال API التي تمكّننا من إنشاء أنواع مختلفة من الاستمرارية. بصفتنا كتّاب سكربتات، علينا اختيار النوع الأنسب للمهمة المطلوبة، أو حتى المزج بين أكثر من نوع. في هذا الدليل سنستعرض جميع الأصناف والدوال المختلفة ونشرح كيفية استخدامها.

المرجع

يوضح هذان الجدولان باختصار الفرق بين الأنواع المختلفة من الأصناف والأعضاء التي تتضمن وظائف استمرارية، ثم سنوسّع الشرح بعد ذلك.

الصنفالعضونوع الاستمرارية
SNetwork
(برمجة نصية للعميل)
SetRegionProperty
GetRegionProperty
SetShardProperty
GetShardProperty
HasShardProperties
استمرارية شبه دائمة على مستوى Region
استمرارية شبه دائمة على مستوى Region
استمرارية شبه دائمة على مستوى Shard
استمرارية شبه دائمة على مستوى Shard
استمرارية شبه دائمة على مستوى Shard/Region
SPersistence
(برمجة نصية للعميل)
UpdateInfo
RetrieveValue
SetValue
UpdateRegionInfo
RetrieveRegionValue
SetRegionValue
استمرارية دائمة للاعب
استمرارية دائمة للاعب
استمرارية دائمة للاعب
استمرارية دائمة على مستوى Region
استمرارية دائمة على مستوى Region
استمرارية دائمة على مستوى Region
SShared
(برمجة نصية للعميل)
SetSuperGlobal
GetSuperGlobal
استمرارية شبه دائمة في Viewer
استمرارية شبه دائمة في Viewer
SDatabase (برمجة نصية للخادم)GetPlayerValue
SetPlayerValue
GetRegionValue
SetRegionValue
استمرارية دائمة للاعب
استمرارية دائمة للاعب
استمرارية دائمة على مستوى Region
استمرارية دائمة على مستوى Region
أنواع الاستمراريةالوصف
استمرارية شبه دائمة على مستوى Region

تستمر الاستمرارية إلى أن تُغلق Region (بعد مغادرة جميع اللاعبين لـ Region).

تُطبّق الاستمرارية على مستوى Region بالكامل (بما في ذلك كل Shards، إن كان أي منها نشطاً).

استمرارية شبه دائمة على مستوى Shard

تستمر الاستمرارية إلى أن يُغلق Shard (بعد مغادرة جميع اللاعبين لـ Shard).

تُطبّق الاستمرارية على Shard محدد فقط.

استمرارية شبه دائمة في Viewerتستمر الاستمرارية إلى أن يُغلق Viewer.
تُطبّق الاستمرارية على Viewer.
استمرارية دائمة للاعبتستمر الاستمرارية بصورة دائمة.
تُطبّق الاستمرارية على لاعب محدد.
استمرارية دائمة على مستوى Regionتستمر الاستمرارية بصورة دائمة.
تُطبّق الاستمرارية على Region.

ما المقصود بالاستمرارية؟

إرسال رسائل الشبكة مفيد للأحداث العابرة، مثل تلويح لاعب قائلاً مرحباً، وهو أمر يظل ذا صلة لبضع ثوان فقط. لكننا نريد أحياناً إجراء تغييرات تبقى مهمة لأكثر من بضع ثوان، مثل فتح نافذة.

إذا أرسلنا رسالة شبكة إلى جميع اللاعبين تفيد بأن النافذة A مفتوحة الآن، فعندما يدخل لاعب جديد إلى منطقتنا، سيكون قد فاتته رسالة الشبكة تلك، وستظل النافذة A مغلقة لديه في Viewer.

لهذا نحتاج إلى القدرة على جعل تغييرات معينة تستمر، إما بشكل دائم أو على الأقل إلى أن يغادر الجميع المنطقة.

صنف SNetwork

يحتوي صنف SNetwork على مجموعة متنوعة من دوال الشبكات، لكنه يحتوي على 4 دوال وخاصية واحدة مرتبطة بالاستمرارية.

  • SetRegionProperty/GetRegionProperty
  • SetShardProperty/GetShardProperty
  • HasShardProperties

Shard مقابل Region

ستلاحظ أن بعض الدوال تنطبق على Shards وبعضها على Regions. ما المقصود بـ "shards" أساساً؟

Shard هي ميزة في Sinespace حيث يمكن لـ Region واحدة أن تحتوي على عدة Shards (نسخ)، على نحو يشبه الأكوان المتوازية، بحيث لا يستطيع اللاعبون في Shard رؤية اللاعبين في Shard أخرى.

لأغراض هذا الدليل للمبتدئين، لن نخوض في برمجة تجارب متعددة Shards، لكن يكفي فهم الفروق.

ما خصائص Shard/Region؟

لكل Shard/Region مفتوحة (يوجد لاعبون بداخلها)، يتم إنشاء تخزين شبه دائم على الخادم يحتفظ بـ خصائص. ستصل جميع السكربتات التي تستخدم هذه الدوال إلى التخزين نفسه لقراءة الخصائص وكتابتها. هذا التخزين عبارة عن مجموعة من المفاتيح والقيم. سيُغلق هذا التخزين بعد إغلاق Shard/Region (بعد مغادرة جميع اللاعبين).

أي "مفتاح" ينبغي أن تستخدم؟

غالباً ما تريد أن يكون المفتاح خاصاً بك، حتى لا تتمكن السكربتات الأخرى من الوصول إليه. ومن المرجح أيضاً أن تريد أن يكون المفتاح خاصاً بـ GameObject محدد.

يمكننا تحقيق الأمرين بهذه الطريقة:

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

لقد أنشأنا الآن مفتاحاً يكون خاصاً ومحدداً لـ GameObject في الوقت نفسه. سنستخدمه لاحقاً للوصول إلى تخزين الخصائص وقراءة/كتابة قيمة محددة.

HasShardProperties

عندما ينضم لاعب إلى المنطقة، قد تنطلق السكربتات التي تريد الوصول فوراً إلى قاعدة بيانات الخصائص خلال فترة صغيرة جداً لم نكن قد اتصلنا فيها بعد بتخزين الخصائص. لهذا نستخدم HasShardProperties للتحقق مما إذا كانت قاعدة بيانات التخزين متصلة وجاهزة للقراءة/الكتابة. (تُستخدم HasShardProperties لكل من خصائص Shard وخصائص Region)

لنوسّع الكود السابق بإضافة coroutine (تشغيل الكود في خيط متوازٍ) للاهتمام بهذا الأمر:

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)

ضبط الخصائص والحصول عليها

تُنفّذ خصائص Shard وخصائص Region بالطريقة نفسها تماماً. لذلك سنواصل في هذا الدليل باستخدام خصائص Region فقط.

ملاحظة: الدالتان أدناه محدودتا الاستخدام إلى 10 استدعاءات في الثانية. (20 استدعاءً في الثانية على Breakroom)

للحصول على خاصية Region: value = Space.Network.GetRegionProperty(KEY)

ملاحظة: قد لا تكون خاصية Region قد ضُبطت من قبل أحياناً، لذلك قد تُرجع Nil. سنحتاج إلى إضافة تحقق من ذلك.

لضبط خاصية Region: Space.Network.SetRegionProperty(KEY, 'A Value')

ملاحظة: قيم خصائص Region من نوع بيانات نصي، لذلك عليك تحويل الأرقام أو الجداول إلى نص لتخزينها.

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)

مثال النافذة

لنكتب سكربتاً لنافذة تُفتح/تُغلق عند النقر عليها وتحدّث خاصية Region الخاصة بها، مع استمرار قراءة خاصية Region في حال فتح لاعب آخر النافذة أو أغلقها.

سنضيف إلى السكربت جزأين رئيسيين. الجزء الأول يقرأ خاصية Region باستمرار لمراقبة التغييرات والتفاعل معها. والجزء الثاني يستجيب للنقرات ويحدّث خاصية Region التي تحتفظ بحالة النافذة (مفتوحة/مغلقة).

الجزء الأول

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 (برمجة نصية للعميل) على مجموعة متنوعة من دوال التواصل بين السكربتات ودوال التخزين المحلي في Viewer، لكنه يحتوي على دالتين مرتبطتين بالاستمرارية.

  • SetSuperGlobal
  • GetSuperGlobal

ما هو Super Global؟

"Global" في صنف SShared هو تخزين داخل Viewer الخاص باللاعب المحلي ويمكن لجميع السكربتات الوصول إليه.

"Super Global" هو التخزين نفسه لكنه شبه دائم؛ سيظل موجوداً حتى عند تغيير المناطق، إلى أن يُغلق Viewer.

في هذا المثال، سننشئ كائناً قابلاً للنقر ينقلنا إلى منطقة، وفي تلك المنطقة سيكون لدينا سكربت يتفاعل بناءً على المكان الذي أتينا منه.

أي "namespace" و"key" ينبغي أن تستخدم؟

بخلاف حالتنا السابقة، تخزّن هذه الدوال على Viewer الخاص بلاعب محدد، وقد يُقصد بها أن تكون متاحة لعدة سكربتات (قد تكون سكربتاتنا الأخرى أو سكربتات كتّاب آخرين أيضاً). هي بطبيعتها خاصة بلاعب، وغالباً بمشروع أو عملية/هدف محدد.

لهذا يتم الوصول إليها باستخدام namespace وkey وvalue، بدلاً من key وvalue فقط. يمكن أن تكون namespace نوعاً من محدد الوصول الذي لا يلزم أن يكون خاصاً، مع بقاء key محدد وصول يمكننا جعله خاصاً.

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

ضبط Super Globals والحصول عليها

عند ضبط Super Global، لا يلزم أن تكون القيمة نصاً. فهي تقبل قيمة من نوع DynValue، ما يعني أنها قد تكون نصاً أو رقماً أو جدولاً وما إلى ذلك...

نضبط Super Global هكذا:

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

ونحصل على Super Global هكذا:

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

مثال الناقل

سننشئ سكربتين لهذا المثال. سيكون السكربت الأول هو الذي ينقلنا ويضبط Super Global، وسيكون السكربت الثاني في الوجهة وسيصل إلى Super Global ويتفاعل مع المكان الذي انتقلنا منه.


سنبدأ بإنشاء السكربت الأول عبر توسيع الكود أعلاه بإضافة قابلية النقر وجعله يضبط Super Global بالمعلومات التي نحتاج إلى حملها إلى المنطقة الثانية. لأغراض هذا الدليل، سنمرر معلوماتنا في Table بدلاً من متغير واحد.

--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

لدينا الآن كائن يقوم، عند النقر عليه، بضبط Super Global ثم ينقلنا. في منطقة الوجهة سننشئ الآن السكربت الذي يعالج المعلومات المحمولة معنا (اسم region ومعرّفها).

--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 وValues؟

ينشئ نصف الدوال في هذا الصنف استمرارية دائمة على مستوى Region، وينشئ النصف الآخر استمرارية دائمة على مستوى Player.

إذا احتوت الدالة على كلمة region، فهذا يعني أنها تنشئ استمرارية Region. وإذا لم تحتوِ عليها، فهذا يعني أنها تنشئ استمرارية Player.

قيم Region/Player هي قيم في تخزين دائم. قيم Region موجودة في تخزين دائم مرتبط بـ region، أما قيم (Player) فهي موجودة في تخزين دائم مرتبط باللاعب الذي يشغّل السكربت.

من حيث الاستخدام، تُعد قيم Region المكافئ للنسخة الدائمة من صنف SNetwork. أما قيم Player فهي المكافئ لـ صنف SShared.

أي "مفتاح" ينبغي أن تستخدم؟

في حالة قيم Region، سننشئ المفاتيح بالطريقة نفسها التي استخدمناها مع صنف SNetwork.

في حالة قيم (Player)، سننشئ المفاتيح بالطريقة نفسها التي استخدمناها مع صنف SShared.

لماذا نحتاج إلى "Update Region Info"؟

الميزة الفريدة لهذا الصنف هي أنه صنف برمجة نصية للعميل، لكنه يمنحنا القدرة على إنشاء استمرارية من دون استخدام سكربت خادم.

التخزين المستخدم لتحقيق ذلك خارجي، ما يعني أنه علينا البدء باستخدام دالة UpdateRegionInfo() لطلب نسخة من أحدث قيم التخزين قبل محاولة الوصول إلى هذه القيم.

نفعل ذلك بهذه الطريقة:

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

بخلاف خصائص region شبه الدائمة، لا يكون استدعاء هذه الدالة فورياً بالقدر نفسه. لأنها تصل إلى تخزين دائم خارجي، نحتاج إلى ربط دالة سيتم استدعاؤها بمجرد وصول المعلومات.

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 region. لذلك قد نحتاج إلى المزج بين الكود وأنواع أخرى من الاستمرارية.

مثال Smart Lights

تفضل بزيارة صفحة مشروع Smart Lights النموذجي.

صنف SDatabase

صنف SDatabase هو المكافئ لصنف SPersistence، لكن الفرق الوحيد أنه لا يُستخدم إلا عبر Server Scripts. لا تملك سكربتات الخادم إمكانية الوصول إلى صنف SPersistence.

سكربت الخادم شبه مستمر بطبيعته أيضاً. وهذا يعني أنه إذا فعلت هذا aVariable = 5 في سكربت خادم، فسيظل هذا المتغير مستمراً إلى أن يغادر جميع اللاعبين المنطقة. يجعل هذا استخدام صنف SDatabase أسهل بكثير من صنف SPersistent لأنه لا يتطلب المزج بين طرق استمرارية متعددة.

أحد عيوب سكربتات الخادم أنها لا يمكن وضعها إلا في عناصر الأثاث ويجب تحميلها في region حتى تُستخدم (بخلاف سكربتات العميل).

بما أن هذا الصنف يكاد يكون نسخة مطابقة من صنف SPersistence، سنكتفي بتوضيح الفروق أدناه (لتجنب التكرار).

الفروق بين SDatabase وSPersistence

الفروقان الصغيران الوحيدان بين هذين الصنفين هما:

  1. الدالتان SetValue/GetValue في SPersistence هما SetPlayerValue/GetPlayerValue في SDatabase
  2. تحتوي GetRegionValue وGetPlayerValue على وسيط onSave إضافي، وهو hook يخبرنا عندما يتم ضبط القيمة بنجاح.
--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)

شكراً لك

شكراً لإكمال هذا الدليل. إذا واجهت أي مشكلات أو أردت اقتراح تحسينات، فيرجى فعل ذلك في هذه الصفحة.