Roblox Guidebookقاعدة المعرفة
العربية ⌄

التطوير / ROBLOX

حفظ التقدم لأول مرة في Roblox: القراءة والتعديل والدخول من جديد

نجهز تمرينًا صغيرًا حول عداد واحد محفوظ. نميز بيانات اللاعب عن ملف المشروع، ونستخدم تجربة اختبار منفصلة، ونتجنب اعتبار فشل التحميل ملفًا جديدًا فارغًا. هذا درس تمهيدي وليس نظامًا مكتملًا لاقتصاد لعبة منشورة.

آخر تحديث:

حدد ما يجب أن يبقى بعد الخروج #

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

ابدأ بعدد صحيح واحد. لا تضف العملات والأغراض والمهام والتجارة في الوقت نفسه، لأن تحديد سبب أول نتيجة غير متوقعة سيصبح أصعب. يمكنك تسمية العداد التعليمي TestCompletedOrders. يجب أن تعرض الواجهة القيمة التي قرأها الخادم فعلًا، وليس رقمًا وضعته مسبقًا. اكتب النتيجة المتوقعة عند العودة قبل إضافة أول زر.

جهز تجربة اختبار مستقلة #

أنشئ تجربة مستقلة لهذا التمرين، وانشرها، واضبط الوصول إليها بما يناسب الاختبار. إضافة place آخر داخل تجربة قائمة لا توفر العزل نفسه: يمكن للأماكن داخل التجربة الواحدة الوصول إلى مخازن بياناتها. تأكد من هوية التجربة المفتوحة في Creator Hub قبل تغيير الإعدادات.

تشرح وثائق Roblox وصول Studio إلى واجهات API ضمن Security في Experience Settings. فعّل هذا الوصول في تجربة الاختبار المستقلة فقط. قد يتغير موضع عناصر القوائم؛ راجع الوثائق الحالية عندما تختلف الواجهة. لا تسمح لـStudio بالوصول إلى سجلات اللاعبين الفعلية لمجرد تسريع التجربة. السجل الذي لا تعرف محتواه لا يجوز أن يصبح هدفًا عرضيًا للتمرين.

ضع منطق الحفظ على الخادم #

تُنفذ عمليات DataStore في Script يعمل على الخادم. يمكن وضع سكربت التمرين داخل ServerScriptService. يستطيع LocalScript إظهار حالة التحميل والنتيجة، لكنه لا يقرر بمفرده قيمة العداد المحفوظ ولا يعلن نجاح الكتابة.

افصل ثلاث مسؤوليات: قراءة القيمة عند دخول اللاعب، وتطبيق تعديل يسمح به الخادم، ثم حفظ التعديل. جمعها كلها داخل معالج نقرة زر يشجع على اعتبار كل نقرة نجاحًا. في مثال الورشة، تعني عبارة «اكتمل الطلب» أن الخادم تحقق من المهمة. ظهور نقرة على الشاشة لا يثبت وحده أن اللاعب استحق تقدمًا.

اختر اسم المخزن ومفتاح اللاعب #

استخدم اسمًا ثابتًا لمخزن الاختبار ومفتاحًا مبنيًا على Player.UserId. نقترح اسم GuidebookOrdersTest_v1 للمخزن، ومفتاحًا يتكون من User_ ثم معرّف المستخدم. هذه أسماء اخترناها للتمرين وليست صيغة تفرضها Roblox.

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

غياب البيانات يختلف عن فشل القراءة #

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

استخدم حالات صريحة مثل Loading وReady وLoadFailed. أثناء Loading لا تسمح بالأفعال التي تغير التقدم. في LoadFailed اعرض رسالة واضحة، ولا تكتب صفرًا فوق بيانات لا تعرف محتواها. يكفي في المرحلة الأولى إيقاف التعديلات وإنهاء الاختبار. تصميم إعادة المحاولة قرار منفصل، ولا يبرر استبدال الملف القديم بصمت بملف جديد.

نتائج القراءة والفعل المسموحافتح الصورة بالحجم الكامل ↗
رسم أصلي: فشل القراءة لا يعني غياب السجل.
الحالةالسلوك المتوقع
Loadingتعديل التقدم غير متاح
Readyالأفعال التي يسمح بها الخادم متاحة
LoadFailedلا تكتب ملفًا أوليًا

تعامل مع الفشل دون إعلان نجاح زائف #

قد تفشل استدعاءات مخزن البيانات. تعرض وثائق Roblox معالجة باستخدام pcall، لكن كتابة pcall لا تجعل الحفظ موثوقًا تلقائيًا. يجب فحص النتيجة وتحديد سلوك التطبيق عندما تفشل العملية.

«أُرسلت المحاولة» و«تأكد الحفظ» حالتان مختلفتان. استخدم رسائل منفصلة في Output للقراءة والتعديل والكتابة. لا تعرض الواجهة «تم الحفظ» قبل وصول نتيجة ناجحة إلى الخادم. لا تضع الملف الكامل أو المعلومات الشخصية داخل خطأ يراه اللاعب. يكفي هنا شرح مختصر والمرحلة التي فشلت؛ أما التفاصيل التشخيصية فتبقى في بيئة الاختبار المنضبطة لمقارنتها بتسلسل الخطوات المتوقع.

افهم الفرق بين طرق التحديث #

قارن SetAsync وUpdateAsync قبل اختيار أسلوب الكتابة. يكتب SetAsync القيمة المقدمة، بينما يطبق UpdateAsync تحويلًا على القيمة المخزنة الحالية. عند الكتابة من عدة خوادم، قد تؤدي إعادة قيمة محلية قديمة إلى إلغاء تعديل آخر.

لا يجوز لدالة callback الخاصة بـUpdateAsync تعليق التنفيذ، مثل استخدام task.wait. لا تمنح مكافأة خارجية داخل التحويل على افتراض أن الدالة ستعمل دائمًا مرة واحدة فقط. حدد قاعدة تغيير البيانات أولًا، ثم أبلغ اللاعب بالنتيجة المؤكدة بشكل منفصل. معالجة ملفات اللاعبين بين خوادم متعددة تحتاج قواعد إضافية؛ هذا الدرس لا يقدم نظام ملكية الجلسة أو معالجة المشتريات.

ضع الكود التعليمي التالي في ModuleScript باسم StoreExperiment داخل ServerScriptService. يحصل Script الخادم على مخزن الاختبار عبر DataStoreService:GetDataStore("GuidebookOrdersTest_v1")، ويحمل الوحدة بواسطة require، وينشئ المحول باستخدام StoreExperiment.new(store). اقرأ عبر adapter:Load(player.UserId). بعد تحقق الخادم من فعل تعليمي فعلي، استدعِ adapter:CompleteTestOrder(player.UserId). هذا ليس معالج زر ولا مكافأة تلقائية للدخول. يعيد النجاح true وعددًا؛ ويعيد الفشل false مع LoadFailed أو SaveFailed. قارن النتائج بالجدول. تقبل الممارسة الأعداد الصحيحة من 0 إلى 1,000,000 وتتوقف عند هذا الحد المختار للتمرين. الاستدعاء المنفصل المكرر يزيد العداد مجددًا؛ لا توجد حماية من تكرار المكافأة. فُحص الكود محليًا في Luau بمخزن محاكى، دون Studio أو استدعاءات API فعلية.

-- ModuleScript: StoreExperiment (ServerScriptService).
-- Learning adapter only; no sessions, receipts, retries or production economy.
local StoreExperiment = {}

local function keyFor(userId)
    assert(type(userId) == "number" and userId > 0
        and userId < math.huge and userId == math.floor(userId), "Invalid UserId")
    return "User_" .. tostring(userId)
end

local function counter(value)
    if value == nil then return 0 end
    assert(type(value) == "number" and value >= 0 and value <= 1000000
        and value == math.floor(value), "Unexpected counter format")
    return value
end

function StoreExperiment.new(store)
    local adapter = {}

    function adapter:Load(userId)
        local key = keyFor(userId)
        local ok, result = pcall(function()
            return counter(store:GetAsync(key))
        end)
        if not ok then return false, "LoadFailed" end
        return true, result
    end

    function adapter:CompleteTestOrder(userId)
        -- Call only after the server verifies the exercise action.
        -- A failed read never authorizes a write of initial data.
        local loaded = self:Load(userId)
        if not loaded then return false, "LoadFailed" end
        local key = keyFor(userId)
        local ok, result = pcall(function()
            return store:UpdateAsync(key, function(current)
                local value = counter(current)
                assert(value < 1000000, "Exercise counter limit reached")
                return value + 1
            end)
        end)
        if not ok then return false, "SaveFailed" end
        return true, result
    end

    return adapter
end

return StoreExperiment

وضح حدود التمرين #

العداد التعليمي ليس محفظة جاهزة للإنتاج. قبل استخدام اقتصاد فعلي، حدد الخادم المسؤول عن الملف، وطريقة التعرف على العملية المكررة، والتعامل مع فشل الشبكة، ومصير الكتابات المعلقة عند إغلاق الخادم.

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

اختبر جلستين متتاليتين #

جهز سجل الاختبار قبل التشغيل. في الجلسة الأولى، انتظر نجاح التحميل، ونفذ فعلًا تجريبيًا واحدًا، وتلقَّ تأكيد الكتابة، ثم سجل قيمة العداد الظاهرة. أنهِ الجلسة. ادخل بعد ذلك التجربة نفسها باستخدام الحساب والمخزن والمفتاح أنفسهم.

قارن القيمة التي حُمّلت من جديد بالقيمة المتوقعة. إعادة فتح واجهة داخل الجلسة القديمة ليست بديلًا لاختبار الاستمرار بين الجلسات. عند التحقيق الإضافي، تذكر أن GetAsync قد يستخدم ذاكرة مؤقتة: قراءتان متتاليتان لا تثبتان بالضرورة استعلامين مستقلين عن أحدث حالة في الخدمة. صف الملاحظة بدقة بدل تسمية كل قراءة مكررة تحققًا جديدًا.

مسار فحص الحفظافتح الصورة بالحجم الكامل ↗
رسم أصلي: قارن البيانات بين الجلسات.

افحص الأخطاء وعزل بيانات اللاعبين #

نجاح الدخول مرتين يغطي جزءًا من التمرين فقط. تحقق من أن مستخدمًا آخر يحصل على مفتاح مستقل، وأن تغيير العداد الأول لا يغير الثاني. فشل القراءة يجب ألا يبدأ كتابة بيانات أولية، وفشل الكتابة يجب ألا ينتج رسالة نجاح.

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

الفحصشرط النجاح
الدخول مجددًاتحميل العداد المؤكد
لاعب آخرمفتاح وقيمة مستقلان
فشل القراءةلا تكتب بيانات فارغة
فشل الكتابةلا تظهر رسالة نجاح الحفظ

إذا ظهر صفر بعد العودة #

تتبع السلسلة: هل هذه التجربة الصحيحة؟ هل يتطابق اسم المخزن؟ هل دخل UserId المتوقع؟ هل نجحت الكتابة السابقة؟ هل أنشأ التطبيق بيانات أولية بعد فشل القراءة؟ اقرأ رسائل Output بالترتيب، ولا تكتفِ بآخر سطر أحمر.

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

متى تنتهي المرحلة الأولى؟ #

ينجح التمرين عندما يبقى العداد بين جلسات مؤكدة، وتظل بيانات اللاعبين منفصلة، ولا يتحول فشل القراءة إلى كتابة ملف فارغ، ولا يظهر فشل الكتابة كنجاح. سجل النتائج الفعلية: الحسابات والبيئة والأفعال والقيم التي شاهدتها، ولا تكتفِ بعبارة «يعمل».

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

المصادر الأصلية

Roblox Creator Hub — Data stores
Roblox Creator Hub — Best practices for data stores