การนำข้อมูลผู้เล่นและระบบการซื้อไปใช้

*เนื้อหานี้แปลโดยใช้ AI (เวอร์ชัน Beta) และอาจมีข้อผิดพลาด หากต้องการดูหน้านี้เป็นภาษาอังกฤษ ให้คลิกที่นี่

เบื้องหลัง

Roblox มีชุดของ APIs เพื่อเชื่อมต่อกับ data stores ผ่าน DataStoreService กรณีการใช้งานที่พบบ่อยที่สุดสำหรับ APIs เหล่านี้คือการบันทึก โหลด และอัพเดต ข้อมูลผู้เล่น ซึ่งเป็นข้อมูลที่เกี่ยวข้องกับความก้าวหน้า การซื้อ และลักษณะการเล่นอื่น ๆ ที่ยังคงอยู่ระหว่างเซสชันการเล่นแต่ละครั้ง

เกมส่วนใหญ่บน Roblox ใช้ APIs เหล่านี้เพื่อใช้ในการดำเนินการระบบข้อมูลผู้เล่นในรูปแบบใดรูปแบบหนึ่ง การนำไปใช้เหล่านี้แตกต่างกันในแนวทาง แต่โดยทั่วไปจะมุ่งแก้ไขปัญหาชุดเดียวกัน

ปัญหาทั่วไป

ด้านล่างนี้คือปัญหาทั่วไปที่ระบบข้อมูลผู้เล่นพยายามจะแก้ไข:

  • การเข้าถึงในหน่วยความจำ: การร้องขอ DataStoreService จะส่งคำขอเว็บที่ทำงานแบบอะซิงโครนัสและของพวกเขาจะถูกกำหนดค่าด้วยขีดจำกัดการเข้าถึง เหมาะสมสำหรับการโหลดเริ่มต้นที่เริ่มต้นเซสชัน แต่ไม่เหมาะสมสำหรับการอ่านและเขียนที่ความถี่สูงซ้ำระหว่างการเล่นเกมในปกติ ผู้พัฒนาส่วนใหญ่จะเก็บข้อมูลนี้ในหน่วยความจำบนเซิร์ฟเวอร์ Roblox จำกัด การร้องขอ DataStoreService ไว้ในสถานการณ์ต่อไปนี้:

    • อ่านเริ่มต้นที่เริ่มเซสชัน
    • เขียนสุดท้ายที่จบเซสชัน
    • เขียนเป็นระยะ ๆ ที่ช่วงเวลาเพื่อลดปัญหาที่การเขียนสุดท้ายล้มเหลว
    • การเขียนเพื่อให้แน่ใจว่าข้อมูลถูกรักษาในระหว่างการประมวลผลการซื้อ
  • การจัดเก็บที่มีประสิทธิภาพ: การเก็บข้อมูลเซสชันทั้งหมดของผู้เล่นในตารางเดียวช่วยให้คุณสามารถอัพเดตหลายค่าในคราวเดียวและจัดการข้อมูลเดียวกันในคำขอน้อยลง นอกจากนี้ยังช่วยกำจัดความเสี่ยงของการไม่ซิงค์ระหว่างค่าต่าง ๆ และทำให้การย้อนกลับทำได้ง่ายขึ้น

    นักพัฒนาบางคนยังใช้การทำซีเรียลลิซเซชันแบบกำหนดเองเพื่อบีบอัดโครงสร้างข้อมูลขนาดใหญ่ (โดยปกติจะเก็บเนื้อหาที่ผู้ใช้สร้างในเกม)

  • การทำซ้ำ: คลายเอนท์จำเป็นต้องเข้าถึงข้อมูลผู้เล่นบ่อย ๆ (เช่น เพื่อตรวจสอบ UI) แนวทางทั่วไปในการทำซ้ำข้อมูลผู้เล่นไปยังคลายเอนท์ช่วยให้คุณส่งข้อมูลนี้โดยไม่ต้องสร้างระบบการทำซ้ำแบบเฉพาะสำหรับแต่ละคอมโพเนนต์ของข้อมูล นักพัฒนามักต้องการตัวเลือกในการเลือกเกี่ยวกับสิ่งที่ถูกทำซ้ำไปยังคลายเอนท์

  • การจัดการข้อผิดพลาด: เมื่อไม่สามารถเข้าถึง DataStores ได้, โซลูชันส่วนใหญ่จะใช้กลไกการพยายามใหม่และการตกกลับไปยัง 'ข้อมูลเริ่มต้น' ความระมัดระวังพิเศษจำเป็นต้องให้แน่ใจว่าข้อมูลตกกลับไม่เขียนทับ 'ข้อมูลจริง' ในภายหลัง และสิ่งนี้จะต้องสื่อสารกับผู้เล่นอย่างเหมาะสม

  • การพยายามใหม่: เมื่อไม่สามารถเข้าถึง data stores ได้, โซลูชันส่วนใหญ่จะใช้กลไกการพยายามใหม่และการตกกลับไปยังข้อมูลเริ่มต้น ระวังให้แน่ใจว่าข้อมูลตกกลับไม่นำมาซึ่งการเขียนทับ "ข้อมูลจริง" ในภายหลัง และสื่อสารสถานการณ์ให้ผู้เล่นทราบอย่างเหมาะสม

  • การล็อคเซสชัน: หากข้อมูลของผู้เล่นคนเดียวกันถูกโหลดและอยู่ในหน่วยความจำบนหลายเซิร์ฟเวอร์ อาจเกิดปัญหาที่เซิร์ฟเวอร์หนึ่งบันทึกข้อมูลที่ไม่เป็นปัจจุบัน ซึ่งอาจส่งผลให้ข้อมูลสูญหายและแม้กระทั่งการเกิดช่องโหว่ซ้ำซ้อนทั่วไป

  • การจัดการการซื้ออย่างอะตอม: ยืนยัน มอบรางวัล และบันทึกการซื้ออย่างอะตอมเพื่อป้องกันไม่ให้ไอเทมสูญหายหรือมอบให้หลายครั้ง

ตัวอย่างโค้ด

Roblox มีโค้ดอ้างอิงเพื่อช่วยคุณในการออกแบบและสร้างระบบข้อมูลผู้เล่น ข้อความที่เหลือในหน้านี้จะตรวจสอบเบื้องหลัง รายละเอียดการนำไปใช้ และเควสทั่วไป


หลังจากที่คุณนำเข้ารูปแบบไปยัง Studio แล้ว คุณควรเห็นโครงสร้างโฟลเดอร์ดังต่อไปนี้:

หน้าต่าง Explorer แสดงโมเดลระบบการซื้อ

สถาปัตยกรรม

แผนภาพระดับสูงนี้แสดงระบบสำคัญในตัวอย่างและวิธีที่พวกเขาเชื่อมต่อกับโค้ดในเกมที่เหลือ

แผนภาพสถาปัตยกรรมสำหรับตัวอย่างโค้ด

การพยายามใหม่

Class: DataStoreWrapper

เบื้องหลัง

เนื่องจาก DataStoreService ทำคำขอเว็บให้ทำงานในเบื้องหลัง คำขอของมันไม่ได้รับประกันว่าจะสำเร็จ เมื่อเกิดเหตุการณ์นี้ วิธีการ DataStore จะโยนข้อผิดพลาด อนุญาตให้คุณจัดการกับมัน

ในการจัดการข้อผิดพลาดของ data store มี "หนี้" ที่สามารถเกิดขึ้นได้ถ้าคุณพยายามที่จะจัดการกับการล้มเหลวของ data store แบบนี้:


local function retrySetAsync(dataStore, key, value)
for _ = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
break
end
task.wait(TIME_BETWEEN_ATTEMPTS)
end
end

แม้ว่านี่จะเป็นกลไกการพยายามใหม่ที่ถูกต้องสำหรับฟังก์ชันทั่วไป แต่ไม่เหมาะสำหรับการร้องขอ DataStoreService เพราะมันไม่ได้รับประกันลำดับที่ร้องขอจะถูกทำ ในขณะที่การรักษาลำดับของคำขอเป็นสิ่งสำคัญสำหรับการร้องขอ DataStoreService เนื่องจากพวกเขาโต้ตอบกับสถานะ พิจารณาสถานการณ์ต่อไปนี้:

  1. ทำการร้องขอ A เพื่อกำหนดค่าของคีย์ K เป็น 1
  2. คำขอไม่สำเร็จ จึงมีการวางแผนการพยายามใหม่ใน 2 วินาที
  3. ก่อนที่จะมีการทำซ้ำเกิดขึ้น คำขอ B ตั้งค่าของ K เป็น 2 แต่การทำซ้ำของคำขอ A จะเขียนทับค่าดังกล่าวและตั้งค่า K เป็น 1

แม้ว่า UpdateAsync จะทำงานกับเวอร์ชันล่าสุดของค่าของคีย์ แต่การร้องขอ UpdateAsync ต้องดำเนินการตามลำดับเพื่อหลีกเลี่ยงสถานะชั่วคราวที่ไม่ถูกต้อง (เช่น การซื้อหักเหรียญก่อนที่การเพิ่มเหรียญจะถูกประมวลผล ส่งผลให้เหรียญติดลบ)

ระบบข้อมูลผู้เล่นของเราจะใช้คลาสใหม่ DataStoreWrapper ซึ่งมีให้บริการการพยายามใหม่ที่รับประกันว่าจะถูกประมวลผลตามลำดับต่อคีย์

แนวทาง

แผนภาพกระบวนการที่แสดงระบบการพยายามใหม่

DataStoreWrapper มีวิธีการที่ตรงกับวิธี DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() และ DataStore:RemoveAsync().

เมื่อเรียกใช้วิธีการเหล่านี้:

  1. เพิ่มคำขอเข้าไปในคิว แต่ละคีย์มีคิวของตนเองซึ่งคำขอจะถูกทำตามลำดับและในซีรีส์ เธรดที่ร้องขอจะหยุดจนกว่าคำขอจะเสร็จสมบูรณ์

    ฟังก์ชันนี้อิงจากคลาส ThreadQueue ซึ่งเป็นตัวจัดกำหนดงานอิงจาก coroutine และอัตราการควบคุม แทนที่จะคืนค่าคำสัญญา ThreadQueue จะหยุดเธรดปัจจุบันจนกว่าจะเสร็จสมบูรณ์และโยนข้อผิดพลาดหากล้มเหลว สิ่งนี้สอดคล้องกับรูปแบบการทำงานแบบอะซิงโครนัสของ Luau

  2. หากคำขอไม่สำเร็จจะทำการพยายามใหม่ด้วยการคืนค่าแบบกำหนดค่า โดยการพยายามใหม่เหล่านี้เป็นส่วนหนึ่งของการเรียกกลับที่ส่งไปยัง ThreadQueue ดังนั้นพวกเขาจะทำเสร็จสิ้นก่อนที่คำขอถัดไปในคิวสำหรับคีย์นี้จะเริ่ม

  3. เมื่อคำขอเสร็จสิ้น วิธีการที่เรียกจะคืนค่าด้วย success, result รูปแบบ

DataStoreWrapper ยังมีให้บริการวิธีการเพื่อรับความยาวของคิวสำหรับคีย์ที่กำหนดและล้างคำขอที่ล้าสมัย ตัวเลือกที่หลังนี้มีประโยชน์โดยเฉพาะในสถานการณ์ที่เซิร์ฟเวอร์กำลังจะปิดและไม่มีเวลาในการประมวลผลคำขอนอกจากคำขอที่ล่าสุดที่สุด

เควส

DataStoreWrapper ปฏิบัติตามหลักการที่ว่า นอกเหนือจากกรณีที่รุนแรง คำขอ data store ทุกคำขอควรได้รับอนุญาตให้เสร็จสิ้น (ไม่ว่าอย่างไรก็ตาม) แม้ว่าคำขอที่ใหม่กว่าทำให้มันเป็นเรื่องซ้ำซ้อน เมื่อมีคำขอใหม่เกิดขึ้น คำขอที่ล้าสมัยจะไม่ถูกลบออกจากคิว แต่จะได้รับอนุญาตให้เสร็จสิ้นก่อนที่คำขอใหม่จะเริ่ม สิ่งที่ก่อให้เกิดเหตุการณ์นี้มีพื้นฐานมาจากความสามารถในการใช้งานของโมดูลนี้เป็นยูทิลิตี้ data store ทั่วไปมากกว่าการเป็นเครื่องมือเฉพาะทางสำหรับข้อมูลผู้เล่น และมีดังนี้:

  1. การตัดสินใจเกี่ยวกับกฎชุดที่เข้าใจง่ายสำหรับเมื่อใดคำขอจึงจะปลอดภัยในการลบจากคิวเป็นเรื่องยาก พิจารณาคิวต่อไปนี้:

    Value=0, SetAsync(1), GetAsync(), SetAsync(2)

    พฤติกรรมที่คาดหวังคือว่า GetAsync() จะคืนค่า 1 แต่หากเราลบการร้องขอ SetAsync() ออกจากคิวเนื่องจากทำให้เป็นเรื่องซ้ำซ้อนโดยการร้องขอล่าสุด มันจะคืนค่า 0

    ความก้าวหน้าที่มีเหตุผลคือเมื่อมีการเพิ่มคำขอเขียนใหม่ จะต้องตัดคำขอที่ล้าสมัยออกเฉพาะเท่าที่เป็นที่ยอมรับล่าสุดของการอ่านคำขอ UpdateAsync() ซึ่งเป็นการดำเนินการที่พบบ่อยที่สุด (และใช้โดยระบบนี้) สามารถอ่านและเขียนได้ จึงยากที่จะส่งเรื่องนี้ในการออกแบบนี้โดยไม่เพิ่มความยุ่งยากเพิ่มเติม

    DataStoreWrapper อาจต้องกำหนดว่าการร้องขอ UpdateAsync() ได้รับอนุญาตให้อ่านและ/หรือเขียนหรือไม่ แต่จะไม่มีความเกี่ยวข้องกับระบบข้อมูลผู้เล่นของเรา ซึ่งไม่สามารถตัดสินล่วงหน้าได้เนื่องจากกลไกการล็อคเซสชัน (ซึ่งจะกล่างถึงในรายละเอียดเพิ่มเติมในภายหลัง)

  2. เมื่อถูกลบออกจากคิว การตัดสินใจเกี่ยวกับกฎที่เข้าใจง่ายสำหรับ วิธีการ นี้ควรจัดการได้ยาก เมื่อมีการร้องขอ DataStoreWrapper เธรดปัจจุบันจะหยุดจนกว่ามันจะเสร็จสิ้น หากเราลบคำขอที่ล้าสมัยออกจากคิวเราจะต้องตัดสินใจว่าจะคืนค่า false, "Removed from queue" หรือไม่ก็ไม่คืนค่าและทิ้งเธรดที่ทำงานอยู่ ทั้งสองแนวทางนี้มาพร้อมกับจุดอ่อนและส่งมอบความซับซ้อนเพิ่มเติมให้เข้ากับผู้บริโภค

ท้ายที่สุด มุมมองของเราคือวิธีการที่ง่ายกว่า (การประมวลผลทุกคำขอ) ถือว่าดีกว่าในที่นี้และสร้างสภาพแวดล้อมที่ชัดเจนในการเดินก้าวเข้าสู่ปัญหาที่ซับซ้อน เช่น การล็อคเซสชัน ข้อยกเว้นเพียงข้อเดียวคือในระหว่าง DataModel:BindToClose(), ที่ซึ่งการล้างคิวเป็นสิ่งจำเป็นเพื่อบันทึกข้อมูลผู้ใช้ทั้งหมดภายในเวลาและค่าที่แต่ละการเรียกฟังก์ชันคืนค่าไม่มีความสำคัญอีกต่อไป เพื่อคำนึงถึงเรื่องนี้เราจึงให้สร้าง skipAllQueuesToLastEnqueued วิธีการ สำหรับบริบทเพิ่มเติม ดูที่ ข้อมูลผู้เล่น.

การล็อคเซสชัน

Class: SessionLockedDataStoreWrapper

เบื้องหลัง

ข้อมูลผู้เล่นจะถูกเก็บในหน่วยความจำบนเซิร์ฟเวอร์และจะถูกอ่านและเขียนลงใน data stores ที่พื้นฐานเมื่อจำเป็นเท่านั้น คุณสามารถอ่านและอัปเดตข้อมูลผู้เล่นในหน่วยความจำได้ทันทีโดยไม่จำเป็นต้องมีคำขอเว็บและหลีกเลี่ยงการเกินขีดจำกัดของ DataStoreService.

เพื่อให้โมเดลนี้ทำงานตามที่ตั้งใจไว้ ต้องมีเซิร์ฟเวอร์เพียงหนึ่งเดียวเท่านั้นที่สามารถโหลดข้อมูลของผู้เล่นคนเดียวนั้นลงในหน่วยความจำจาก DataStore ในเวลาเดียวกัน

ตัวอย่างเช่น หากเซิร์ฟเวอร์ A โหลดข้อมูลของผู้เล่น เซิร์ฟเวอร์ B จะไม่สามารถโหลดข้อมูลนั้นได้จนกว่าเซิร์ฟเวอร์ A จะปล่อยล็อคในระหว่างการบันทึกสุดท้าย โดยไม่มีกลไกการล็อค เซิร์ฟเวอร์ B อาจโหลดข้อมูลผู้เล่นที่ไม่เป็นปัจจุบันจาก data store ก่อนที่เซิร์ฟเวอร์ A จะมีโอกาสบันทึกเวอร์ชันล่าสุดที่มีอยู่ในหน่วยความจำ จากนั้นหากเซิร์ฟเวอร์ A บันทึกข้อมูลที่ใหม่กว่าของมันหลังจากที่เซิร์ฟเวอร์ B โหลดข้อมูลที่ไม่เป็นปัจจุบัน เซิร์ฟเวอร์ B ก็จะเขียนทับข้อมูลที่ใหม่กว่านั้นในระหว่างการบันทึกครั้งถัดไป

แม้ว่า Roblox จะอนุญาตให้คลายเอนท์เชื่อมต่อกับเซิร์ฟเวอร์ได้เพียงเซิร์ฟเวอร์เดียวในเวลาเดียวกัน คุณก็ไม่สามารถสมมติได้ว่าข้อมูลจากเซสชันหนึ่งจะสามารถบันทึกได้ก่อนที่เซสชันถัดไปจะเริ่ม พิจารณาสถานการณ์ต่อไปนี้ที่สามารถเกิดขึ้นได้เมื่อผู้เล่นออกจากเซิร์ฟเวอร์ A:

  1. เซิร์ฟเวอร์ A ทำการร้องขอ DataStore เพื่อบันทึกข้อมูลของพวกเขา แต่คำขอนั้นล้มเหลวและต้องการหลายครั้งในการพยายามใหม่เพื่อให้เสร็จสมบูรณ์ ในระหว่างช่วงเวลาการพยายามใหม่ ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
  2. เซิร์ฟเวอร์ A ทำการเรียกใช้ UpdateAsync() ที่มากเกินไปกับคีย์เดียวกันและถูกจำกัด คำขอการบันทึกขั้นสุดท้ายอยู่ในคิว ขณะที่คำขอนั้นอยู่ในคิว ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
  3. บนเซิร์ฟเวอร์ A มีโค้ดที่เชื่อมโยงกับเหตุการณ์ PlayerRemoving ซึ่งหยุดก่อนที่จะบันทึกข้อมูลของผู้เล่น ก่อนที่การทำงานนี้จะเสร็จสิ้น ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
  4. ประสิทธิภาพของเซิร์ฟเวอร์ A ลดลงถึงจุดที่การบันทึกขั้นสุดท้ายล่าช้ากว่าระยะเวลาที่ผู้เล่นเข้าร่วมเซิร์ฟเวอร์ B

สถานการณ์เหล่านี้ควรเกิดขึ้นได้ยาก แต่พวกเขายังเกิดขึ้น โดยเฉพาะในสถานการณ์ที่ผู้เล่นตัดการเชื่อมต่อจากเซิร์ฟเวอร์ A และเชื่อมต่อกับเซิร์ฟเวอร์ B อย่างรวดเร็ว (ในกรณีเช่นระหว่างการเทเลพอร์ต) ผู้ใช้ที่ชั่วร้ายบางคนอาจพยายามใช้ประโยชน์จากพฤติกรรมนี้เพื่อดำเนินการโดยไม่ให้ข้อมูลถาวร สิ่งนี้อาจมีผลกระทบอย่างมากในเกมที่อนุญาตให้ผู้เล่นแลกเปลี่ยนกันและเป็นแหล่งที่มาของการใช้งานซ้ำที่พบบ่อย

การล็อคเซสชันจะจัดการกับช่องโหว่นี้โดยให้แน่ใจว่า เมื่อคีย์ DataStore ของผู้เล่นถูกอ่านครั้งแรกโดยเซิร์ฟเวอร์ เซิร์ฟเวอร์จะเขียนล็อคไปยังข้อมูลเมตาของคีย์อย่างอะตอมในคำขอ UpdateAsync() เดียวกัน หากค่าล็อคนี้มีอยู่เมื่อเซิร์ฟเวอร์อื่นพยายามอ่านหรือเขียนคีย์ เซิร์ฟเวอร์จะไม่ดำเนินการต่อ

แนวทาง

แผนภาพกระบวนการที่แสดงระบบการล็อคเซสชัน

SessionLockedDataStoreWrapper เป็นเมต้า-Wrapper รอบคลาส DataStoreWrapper DataStoreWrapper ให้บริการการเรียงลำดับและฟังก์ชันการพยายามใหม่ซึ่ง SessionLockedDataStoreWrapper เสริมด้วยการล็อคเซสชัน

SessionLockedDataStoreWrapper ผ่านคำขอ DataStore ทุกคำขอ ไม่ว่ามันจะเป็น GetAsync, SetAsync หรือ UpdateAsync ผ่าน UpdateAsync ที่เป็นสาเหตุ เพราะ UpdateAsync ช่วยให้คีย์นั้นถูกอ่านและเขียนเป็นอะตอมได้ นอกจากนี้ยังสามารถเลิกเขียนได้ตามค่าที่อ่านโดยการคืนค่า nil ในการคืนค่าการแปรรูป

ฟังก์ชันการแปรรูปที่ผ่านไปใน UpdateAsync สำหรับคำขอแต่ละข้อมีการดำเนินการต่อไปนี้:

  1. ตรวจสอบว่าคีย์นั้นปลอดภัยต่อการเข้าถึง ปล่อยการดำเนินการหากไม่ใช่ "ปลอดภัยในการเข้าถึง" หมายถึง:

    • ข้อมูลเมตาของคีย์ไม่มีค่า LockId ที่ไม่รู้จักซึ่งได้รับการอัปเดตล่าสุดน้อยกว่าระยะเวลาหมดอายุของล็อต ป็นการพิจารณาในการเคารพล็อคที่วางโดยเซิร์ฟเวอร์อื่น และละเว้นล็อคหากมันหมดอายุ

    • หากเซิร์ฟเวอร์นี้ได้วางค่า LockId เองไว้ในข้อมูลเมตาของคีย์ก่อนหน้านี้ แล้วค่านั้นยังอยู่ในข้อมูลเมตาของคีย์อยู่ แสดงถึงสถานการณ์ที่เซิร์ฟเวอร์อื่นได้เข้ายึดล็อคของเซิร์ฟเวอร์นี้ (โดยหมดอายุหรือตามคำสั่ง) และในภายหลังได้ปล่อยมันไป หรือกล่าวอีกนัยหนึ่งแม้ว่าค่า LockId จะเป็น nil เซิร์ฟเวอร์อื่นอาจได้แทนที่และลบล็อคในระยะเวลาที่คุณล็อคคีย์

  2. UpdateAsync จะดำเนินการตาม DataStore ของที่ผู้บริโภค SessionLockedDataStoreWrapper ได้ร้องขอ เช่น GetAsync() แปลไปยัง function(value) return value end.

  3. ขึ้นอยู่กับพารามิเตอร์ที่ถูกส่งไปในคำขอ UpdateAsync จะทำการล็อคหรือยกเลิกล็อคคีย์:

    1. หากคีย์จะถูกล็อค UpdateAsync จะตั้งค่า LockId ในข้อมูลเมตาของคีย์เป็น GUID GUID นี้จะถูกเก็บในหน่วยความจำบนเซิร์ฟเวอร์เพื่อให้สามารถตรวจสอบค่าเมื่อเข้าถึงคีย์ในครั้งถัดไป หากเซิร์ฟเวอร์มีล็อคอยู่แล้วบนคีย์นี้จะไม่มีการเปลี่ยนแปลงเกิดขึ้น มันสามารถจัดตารางงานเพื่อเตือนคุณหากคุณไม่เข้าถึงคีย์อีกครั้งภายในระยะเวลาหมดอายุของล็อค

    2. หากคีย์จะได้รับการปลดล็อค UpdateAsync จะลบค่า LockId ในข้อมูลเมตาของคีย์

การจัดการความพยายามใหม่แบบกำหนดเองจะถูกส่งต่อไปยัง DataStoreWrapper ที่อยู่เบื้องหลัง ดังนั้นการดำเนินการจึงถูกพยายามใหม่หากถูกยกเลิกในขั้นตอนที่ 1 เนื่องจากเซสชันถูกล็อค

ข้อความข้อผิดพลาดที่กำหนดเองจะถูกส่งกลับไปยังผู้บริโภคด้วย ซึ่งอนุญาตให้ระบบข้อมูลผู้เล่นรายงานข้อผิดพลาดทางเลือกในกรณีของการล็อคเซสชันให้กับคลายเอนท์

เควส

ระเบียบการล็อคเซสชันขึ้นอยู่กับเซิร์ฟเวอร์ที่จะปล่อยล็อคในคีย์ทุกครั้งที่เสร็จสิ้นกับมัน ควรทำเช่นนี้เสมอผ่านคำสั่งเพื่อปลดล็อคคีย์ในระหว่างการ writes สุดท้ายใน PlayerRemoving หรือ BindToClose().

อย่างไรก็ตาม การปลดล็อคอาจล้มเหลวในสถานการณ์บางอย่าง ตัวอย่างเช่น:

  • เซิร์ฟเวอร์หยุดทำงานหรือ DataStoreService ไม่สามารถใช้งานได้ตลอดการเข้าถึงคีย์
  • เนื่องจากข้อผิดพลาดในตรรกะหรือข้อบกพร่องในลักษณะเดียวกัน คำสั่งในการปลดล็อคคีย์ไม่ได้ทำ

เพื่อรักษาล็อคในคีย์ คุณต้องเข้าถึงมันเป็นประจำตลอดระยะเวลาที่มันถูกโหลดในหน่วยความจำ โดยปกติจะทำได้เป็นส่วนหนึ่งของลูปการบันทึกอัตโนมัติที่ทำงานอยู่เบื้องหลังในระบบข้อมูลผู้เล่นส่วนใหญ่ แต่ระบบนี้ยังให้บริการวิธี refreshLockAsync หากคุณจำเป็นต้องทำด้วยตนเอง

หากระยะเวลาหมดอายุล็อคถูกเกินล่วงหน้าโดยที่ล็อคไม่ถูกอัปเดตใด ๆ เซิร์ฟเวอร์ใด ๆ ก็สามารถเข้ามาได้ตามอิสระ หากเซิร์ฟเวอร์อื่นเข้ามาล็อค คำพยายามของเซิร์ฟเวอร์ปัจจุบันในการอ่านหรือเขียนคีย์จะล้มเหลว เว้นแต่มันจะได้กำหนดล็อคใหม่อีกครั้ง

การประมวลผลผลิตภัณฑ์นักพัฒนา

Singleton: ReceiptHandler

เบื้องหลัง

ฟังก์ชันคืนค่าของ ProcessReceipt ทำหน้าที่ที่สำคัญอย่างยิ่งในการกำหนดว่าเมื่อใดที่จะทำการซื้อให้เสร็จสิ้น ProcessReceipt จะถูกเรียกในสถานการณ์ที่เฉพาะเจาะจง สำหรับชุดของการรับประกันโปรดดูที่ MarketplaceService.ProcessReceipt.

แม้ว่าความหมายของ "การจัดการ" การซื้อจะแตกต่างกันไปตามแต่ละเกม แต่เราจะใช้เกณฑ์ต่อไปนี้

  1. การซื้อยังไม่ได้รับการจัดการมาก่อน

  2. การซื้อสะท้อนอยู่ในเซสชันปัจจุบัน

  3. การซื้อได้รับการบันทึกลงใน DataStore.

    การซื้อทุกครั้ง แม้แต่การซื้อที่ใช้ครั้งเดียว ก็ต้องสะท้อนใน DataStore เพื่อให้บันทึกการซื้อของผู้ใช้รวมอยู่ในข้อมูลเซสชันของพวกเขา

นี้ต้องทำการดำเนินการต่อไปนี้ก่อนที่จะคืนค่า PurchaseGranted:

  1. ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการจัดการ
  2. มอบรางวัลการซื้อในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
  3. บันทึก PurchaseId ให้เป็นที่จัดการในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
  4. เขียนข้อมูลผู้เล่นในหน่วยความจำของผู้เล่นลงใน DataStore.

การล็อคเซสชันทำให้การเปลี่ยนแปลงนี้ง่ายขึ้น เพราะคุณไม่ต้องกังวลเกี่ยวกับสถานการณ์ต่อไปนี้:

  • ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์ปัจจุบันอาจจะไม่เป็นปัจจุบัน ต้องการให้คุณดึงค่าล่าสุดจาก DataStore ก่อนที่จะตรวจสอบประวัติ PurchaseId
  • การเรียกกลับสำหรับการซื้อเดียวกันจะทำงานในเซิร์ฟเวอร์อื่น ทำให้คุณต้องทั้งอ่านและเขียนประวัติ PurchaseId และบันทึกข้อมูลผู้เล่นที่อัปเดตด้วยการซื้ออย่างอะตอมเพื่อป้องกันการแข่งขัน

การล็อคเซสชันรับประกันว่า หากการพยายามเขียนใน DataStore ของผู้เล่นสำเร็จ จะไม่มีเซิร์ฟเวอร์อื่นใดที่ได้อ่านหรือเขียน DataStore ของผู้เล่นสำเร็จในระหว่างการโหลดข้อมูลและการบันทึกในเซิร์ฟเวอร์นี้ ในสั้น ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เป็นเวอร์ชันล่าสุดที่มีอยู่ อาจมีข้อควรระวังบางประการ แต่จะไม่มีผลกระทบต่อพฤติกรรมนี้

แนวทาง

ความคิดเห็นใน ReceiptProcessor สรุปแนวทาง:

  1. ตรวจสอบว่าข้อมูลของผู้เล่นถูกโหลดในเซิร์ฟเวอร์นี้และโหลดโดยไม่มีข้อผิดพลาดใดๆ

    เนื่องจากระบบนี้ใช้การล็อคเซสชัน การตรวจสอบนี้ยังช่วยยืนยันว่าข้อมูลในหน่วยความจำเป็นเวอร์ชันล่าสุดที่มีอยู่

    หากข้อมูลของผู้เล่นยังไม่ได้โหลด (ซึ่งคาดหวังเมื่อผู้เล่นเข้าร่วมเกม) ให้รอจนกว่าข้อมูลของผู้เล่นจะโหลด ระบบยังฟังเมื่อผู้เล่นออกจากเกมก่อนที่ข้อมูลของพวกเขาจะโหลด เพื่อไม่ให้มันหยุดถาวรและบล็อกให้การเรียกกลับนี้ถูกเรียกอีกครั้งในเซิร์ฟเวอร์นี้สำหรับการซื้อหากผู้เล่นเข้าใหม่

  2. ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการประมวลผลในข้อมูลผู้เล่น

    เนื่องจากการล็อคเซสชัน อาเรย์ของ PurchaseIds ที่ระบบมีในหน่วยความจำเป็นเวอร์ชันล่าสุด หาก PurchaseId ถูกบันทึกว่าได้รับการจัดการและสะท้อนอยู่ในค่าที่ถูกโหลดหรือบันทึกไปยัง DataStore ให้คืนค่า PurchaseGranted. หากมีการบันทึกว่าได้ถูกจัดการ แต่ ไม่ได้ สะท้อนอยู่ใน DataStore ให้คืนค่า NotProcessedYet.

  3. อัปเดตข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เพื่อ "มอบรางวัล" การซื้อ.

    ReceiptProcessor ใช้แนวทางการเรียกกลับทั่วไปและกำหนดการเรียกกลับที่แตกต่างกันสำหรับแต่ละ DeveloperProductId.

  4. อัปเดตข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เพื่อบันทึก PurchaseId.

  5. ส่งคำขอเพื่อบันทึกข้อมูลในหน่วยความจำลงใน DataStore คืนค่ากลับเป็น PurchaseGranted หากคำขอนั้นสำเร็จ หากไม่คืนค่า NotProcessedYet.

    หากคำขอการบันทึกนี้ไม่สำเร็จ คำขอในอนาคตเพื่อบันทึกข้อมูลเซสชันในหน่วยความจำของผู้เล่นยังคงสำเร็จ ในช่วงการเรียก ProcessReceipt ถัดไป ขั้นตอนที่ 2 จะจัดการกับสถานการณ์นี้และคืนค่า PurchaseGranted.

ข้อมูลผู้เล่น

Singletons: PlayerData.Server, PlayerData.Client

เบื้องหลัง

โมดูลที่ให้บริการอินเทอร์เฟซสำหรับให้โค้ดอ่านและเขียนข้อมูลเซสชันของผู้เล่นแบบซิงโครนัสนั้นเป็นที่นิยมในเกม Roblox หมวดนี้จะกล่าวถึง PlayerData.Server และ PlayerData.Client.

แนวทาง

PlayerData.Server และ PlayerData.Client ดำเนินการตาม:

  1. โหลดข้อมูลของผู้เล่นลงในหน่วยความจำรวมถึงการจัดการกรณีที่ล้มเหลวในการโหลด
  2. ให้บริการอินเทอร์เฟซสำหรับโค้ดเซิร์ฟเวอร์เพื่อสอบถามและเปลี่ยนแปลงข้อมูลผู้เล่น
  3. ถ่ายทอดการเปลี่ยนแปลงในข้อมูลผู้เล่นไปยังคลายเอนท์เพื่อให้โค้ดของคลายเอนท์สามารถเข้าถึงได้
  4. ถ่ายทอดความผิดพลาดในการโหลดและ/หรือบันทึกให้กับคลายเอนท์เพื่อให้แสดงกล่องโต้ตอบข้อผิดพลาด
  5. บันทึกข้อมูลผู้เล่นอย่างสม่ำเสมอ เมื่อผู้เล่นออกจากเกม และเมื่อเซิร์ฟเวอร์ปิด

โหลดข้อมูลผู้เล่น

แผนภาพกระบวนการที่แสดงระบบการโหลด
  1. SessionLockedDataStoreWrapper ทำคำขอ getAsync ไปยัง data store.

    หากคำขอนี้ล้มเหลว ข้อมูลเริ่มต้นจะถูกใช้และโปรไฟล์จะถูกตีตราว่า "ผิดพลาด" เพื่อให้แน่ใจว่าจะไม่ถูกเขียนไปยัง data store ในภายหลัง

    ทางเลือกอื่นคือการเตะผู้เล่นออก แต่เราขอแนะนำให้ผู้เล่นเล่นด้วยข้อมูลเริ่มต้นและสื่อสารอย่างชัดเจนเกี่ยวกับสิ่งที่เกิดขึ้นแทนที่จะลบพวกเขาออกจากเกม

  2. ส่งข้อมูลเริ่มต้นไปยัง PlayerDataClient ที่ประกอบด้วยข้อมูลที่โหลดและสถานะข้อผิดพลาด (ถ้ามี)

  3. เธรดใด ๆ ที่หยุดด้วย waitForDataLoadAsync สำหรับผู้เล่นจะถูกต่ออายุขึ้นใหม่

ให้บริการอินเทอร์เฟซสำหรับโค้ดเซิร์ฟเวอร์

  • PlayerDataServer เป็น singleton ที่สามารถเรียกใช้และเข้าถึงได้โดยโค้ดเซิร์ฟเวอร์ใด ๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน
  • ข้อมูลผู้เล่นถูกจัดระเบียบในรูปแบบดิกชันนารีของคีย์และค่า คุณสามารถจัดการค่าต่าง ๆ เหล่านี้บนเซิร์ฟเวอร์ด้วยวิธีการ setValue, getValue, updateValue และ removeValue วิธีการเหล่านี้ทั้งหมดทำงานแบบซิงโครนัสโดยไม่จำเป็นต้องหยุด
  • วิธีการ hasLoaded และ waitForDataLoadAsync ถูกกำหนดขึ้นเพื่อให้แน่ใจว่าข้อมูลได้โหลดแล้วก่อนที่คุณจะเข้าถึงมัน เราขอแนะนำให้ทำเช่นนี้ในระหว่างหน้าจอโหลดครั้งแรกก่อนที่จะเริ่มระบบอื่น ๆ เพื่อหลีกเลี่ยงการตรวจสอบความผิดพลาดในการโหลดก่อนทุกการมีปฏิสัมพันธ์กับข้อมูลในคลายเอนท์
  • วิธีการ hasErrored สามารถสอบถามว่าการโหลดเริ่มต้นของผู้เล่นล้มเหลวทำให้พวกเขาใช้ข้อมูลเริ่มต้นหรือไม่ ตรวจสอบวิธีนี้ก่อนที่จะอนุญาตให้ผู้เล่นทำการซื้อ เนื่องจากไม่สามารถบันทึกการซื้อได้หากไม่มีการโหลดที่สำเร็จ
  • สัญญาณ playerDataUpdated จะเกิดขึ้นพร้อมกับ player, key, และ value ทุกครั้งที่ข้อมูลของผู้เล่นเปลี่ยนแปลง ระบบต่าง ๆ สามารถสมัครสมาชิกไปยังสิ่งนี้ได้

ถ่ายทอดการเปลี่ยนแปลงไปยังคลายเอนท์

  • การเปลี่ยนแปลงใด ๆ ในข้อมูลผู้เล่นใน PlayerDataServer จะถูกถ่ายทอดไปยัง PlayerDataClient เว้นแต่คีย์นั้นจะถูกทำเครื่องหมายว่าเป็นส่วนตัวโดยใช้ setValueAsPrivate
    • setValueAsPrivate ใช้เพื่อกำหนดคีย์ที่ไม่ควรส่งไปยังคลายเอนท์
  • PlayerDataClient มีวิธีในการดึงค่าของคีย์ (get) และสัญญาณที่เกิดขึ้นเมื่อมีการอัปเดต (updated) นอกจากนี้ยังมีวิธี hasLoaded และสัญญาณ loaded เพื่อให้คลายเอนท์สามารถรอให้ข้อมูลโหลดและส่งสำเนาก่อนที่จะเริ่มระบบของตนได้
  • PlayerDataClient เป็น singleton ที่สามารถเรียกใช้และเข้าถึงได้โดยโค้ดเซิร์ฟเวอร์ใด ๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน

ถ่ายทอดข้อผิดพลาดให้กับคลายเอนท์

  • สถานะข้อผิดพลาดที่พบเมื่อบันทึกหรือโหลดข้อมูลผู้เล่นจะถูกถ่ายทอดไปยัง PlayerDataClient.
  • เข้าถึงข้อมูลนี้ด้วยวิธี getLoadError และ getSaveError รวมถึงสัญญาณ loaded และ saved.
  • ข้อผิดพลาดมีอยู่สองประเภทคือ DataStoreError (คำขอ DataStoreService ล้มเหลว) และ SessionLocked (ดูที่ การล็อคเซสชัน).
  • ใช้เหตุการณ์เหล่านี้ในการปิดคำชวนการซื้อของคลายเอนท์และดำเนินการสร้างกล่องโต้ตอบเตือน สิ่งนี้แสดงถึงกล่องโต้ตอบเตือนที่สามารถแสดงเมื่อข้อมูลผู้เล่นไม่สามารถโหลดได้:
ภาพหน้าจอของกล่องโต้ตอบเตือนตัวอย่างที่สามารถแสดงเมื่อข้อมูลผู้เล่นไม่สามารถโหลดได้

บันทึกข้อมูลผู้เล่น

แผนภาพกระบวนการที่แสดงระบบการบันทึก
  1. เมื่อผู้เล่นออกจากเกม ระบบจะดำเนินการตามขั้นตอนต่อไปนี้:

    1. ตรวจสอบว่าปลอดภัยในการเขียนข้อมูลของผู้เล่นลงใน data store เท่าที่เกี่ยวข้องกับกรณีที่ไม่ปลอดภัย รวมถึงข้อมูลของผู้เล่นที่ไม่สามารถโหลดหรือยังอยู่ในระหว่างการโหลด
    2. ทำคำขอผ่าน SessionLockedDataStoreWrapper เพื่อเขียนค่าข้อมูลในหน่วยความจำในปัจจุบันไปยัง data store และลบล็อคเซสชันเมื่อเสร็จสิ้น
    3. ล้างข้อมูลของผู้เล่น (และตัวแปรอื่น ๆ เช่น เมตาดาต้าและสถานะข้อผิดพลาด) ออกจากหน่วยความจำของเซิร์ฟเวอร์
  2. ในลูปที่เป็นระยะ ระบบจะเขียนข้อมูลของผู้เล่นแต่ละคนไปยัง data store (โดยให้ปลอดภัยในการบันทึก) ความซ้ำซ้อนนี้ช่วยลดการสูญเสียในกรณีที่เกิดเซิร์ฟเวอร์หยุดทำงาน และจำเป็นในการดูแลล็อคเซสชัน

  3. เมื่อได้รับคำขอปรับปิดเซิร์ฟเวอร์ จะเกิดขึ้นใน BindToClose callback ต่อไปนี้:

    1. ขอให้บันทึกข้อมูลของผู้เล่นแต่ละคนในเซิร์ฟเวอร์โดยทำตามกระบวนการตามปกติในการที่เมื่อผู้เล่นออกจากเซิร์ฟเวอร์ คำขอเหล่านี้ทำในลำดับขนาน เนื่องจาก BindToClose callbacks มีเวลาเพียง 30 วินาทีในการทำให้เสร็จสิ้น
    2. เพื่อเร่งความเร็วการบันทึก คำขอทั้งหมดในคิวสำหรับคีย์แต่ละคีย์จะถูกลบออกจาก DataStoreWrapper ที่อยู่เบื้องหลัง (ดูที่ Retries).
    3. การเรียกกลับจะไม่ส่งคืนจนกว่าคำขอทั้งหมดจะเสร็จสิ้น.
©2026 Roblox Corporation. Roblox, โลโก้ Roblox และ Powering Imagination เป็นส่วนหนึ่งของเครื่องหมายการค้าที่จดทะเบียน และไม่ได้จดทะเบียนของเราในสหรัฐฯ และประเทศอื่นๆ