เบื้องหลัง
Roblox มีชุดของ APIs เพื่อเชื่อมต่อกับ data stores ผ่าน DataStoreService กรณีการใช้งานที่พบบ่อยที่สุดสำหรับ APIs เหล่านี้คือการบันทึก โหลด และอัพเดต ข้อมูลผู้เล่น ซึ่งเป็นข้อมูลที่เกี่ยวข้องกับความก้าวหน้า การซื้อ และลักษณะการเล่นอื่น ๆ ที่ยังคงอยู่ระหว่างเซสชันการเล่นแต่ละครั้ง
เกมส่วนใหญ่บน Roblox ใช้ APIs เหล่านี้เพื่อใช้ในการดำเนินการระบบข้อมูลผู้เล่นในรูปแบบใดรูปแบบหนึ่ง การนำไปใช้เหล่านี้แตกต่างกันในแนวทาง แต่โดยทั่วไปจะมุ่งแก้ไขปัญหาชุดเดียวกัน
ปัญหาทั่วไป
ด้านล่างนี้คือปัญหาทั่วไปที่ระบบข้อมูลผู้เล่นพยายามจะแก้ไข:
การเข้าถึงในหน่วยความจำ: การร้องขอ DataStoreService จะส่งคำขอเว็บที่ทำงานแบบอะซิงโครนัสและของพวกเขาจะถูกกำหนดค่าด้วยขีดจำกัดการเข้าถึง เหมาะสมสำหรับการโหลดเริ่มต้นที่เริ่มต้นเซสชัน แต่ไม่เหมาะสมสำหรับการอ่านและเขียนที่ความถี่สูงซ้ำระหว่างการเล่นเกมในปกติ ผู้พัฒนาส่วนใหญ่จะเก็บข้อมูลนี้ในหน่วยความจำบนเซิร์ฟเวอร์ Roblox จำกัด การร้องขอ DataStoreService ไว้ในสถานการณ์ต่อไปนี้:
- อ่านเริ่มต้นที่เริ่มเซสชัน
- เขียนสุดท้ายที่จบเซสชัน
- เขียนเป็นระยะ ๆ ที่ช่วงเวลาเพื่อลดปัญหาที่การเขียนสุดท้ายล้มเหลว
- การเขียนเพื่อให้แน่ใจว่าข้อมูลถูกรักษาในระหว่างการประมวลผลการซื้อ
การจัดเก็บที่มีประสิทธิภาพ: การเก็บข้อมูลเซสชันทั้งหมดของผู้เล่นในตารางเดียวช่วยให้คุณสามารถอัพเดตหลายค่าในคราวเดียวและจัดการข้อมูลเดียวกันในคำขอน้อยลง นอกจากนี้ยังช่วยกำจัดความเสี่ยงของการไม่ซิงค์ระหว่างค่าต่าง ๆ และทำให้การย้อนกลับทำได้ง่ายขึ้น
นักพัฒนาบางคนยังใช้การทำซีเรียลลิซเซชันแบบกำหนดเองเพื่อบีบอัดโครงสร้างข้อมูลขนาดใหญ่ (โดยปกติจะเก็บเนื้อหาที่ผู้ใช้สร้างในเกม)
การทำซ้ำ: คลายเอนท์จำเป็นต้องเข้าถึงข้อมูลผู้เล่นบ่อย ๆ (เช่น เพื่อตรวจสอบ UI) แนวทางทั่วไปในการทำซ้ำข้อมูลผู้เล่นไปยังคลายเอนท์ช่วยให้คุณส่งข้อมูลนี้โดยไม่ต้องสร้างระบบการทำซ้ำแบบเฉพาะสำหรับแต่ละคอมโพเนนต์ของข้อมูล นักพัฒนามักต้องการตัวเลือกในการเลือกเกี่ยวกับสิ่งที่ถูกทำซ้ำไปยังคลายเอนท์
การจัดการข้อผิดพลาด: เมื่อไม่สามารถเข้าถึง DataStores ได้, โซลูชันส่วนใหญ่จะใช้กลไกการพยายามใหม่และการตกกลับไปยัง 'ข้อมูลเริ่มต้น' ความระมัดระวังพิเศษจำเป็นต้องให้แน่ใจว่าข้อมูลตกกลับไม่เขียนทับ 'ข้อมูลจริง' ในภายหลัง และสิ่งนี้จะต้องสื่อสารกับผู้เล่นอย่างเหมาะสม
การพยายามใหม่: เมื่อไม่สามารถเข้าถึง data stores ได้, โซลูชันส่วนใหญ่จะใช้กลไกการพยายามใหม่และการตกกลับไปยังข้อมูลเริ่มต้น ระวังให้แน่ใจว่าข้อมูลตกกลับไม่นำมาซึ่งการเขียนทับ "ข้อมูลจริง" ในภายหลัง และสื่อสารสถานการณ์ให้ผู้เล่นทราบอย่างเหมาะสม
การล็อคเซสชัน: หากข้อมูลของผู้เล่นคนเดียวกันถูกโหลดและอยู่ในหน่วยความจำบนหลายเซิร์ฟเวอร์ อาจเกิดปัญหาที่เซิร์ฟเวอร์หนึ่งบันทึกข้อมูลที่ไม่เป็นปัจจุบัน ซึ่งอาจส่งผลให้ข้อมูลสูญหายและแม้กระทั่งการเกิดช่องโหว่ซ้ำซ้อนทั่วไป
การจัดการการซื้ออย่างอะตอม: ยืนยัน มอบรางวัล และบันทึกการซื้ออย่างอะตอมเพื่อป้องกันไม่ให้ไอเทมสูญหายหรือมอบให้หลายครั้ง
ตัวอย่างโค้ด
Roblox มีโค้ดอ้างอิงเพื่อช่วยคุณในการออกแบบและสร้างระบบข้อมูลผู้เล่น ข้อความที่เหลือในหน้านี้จะตรวจสอบเบื้องหลัง รายละเอียดการนำไปใช้ และเควสทั่วไป
หลังจากที่คุณนำเข้ารูปแบบไปยัง Studio แล้ว คุณควรเห็นโครงสร้างโฟลเดอร์ดังต่อไปนี้:

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

การพยายามใหม่
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 เนื่องจากพวกเขาโต้ตอบกับสถานะ พิจารณาสถานการณ์ต่อไปนี้:
- ทำการร้องขอ A เพื่อกำหนดค่าของคีย์ K เป็น 1
- คำขอไม่สำเร็จ จึงมีการวางแผนการพยายามใหม่ใน 2 วินาที
- ก่อนที่จะมีการทำซ้ำเกิดขึ้น คำขอ B ตั้งค่าของ K เป็น 2 แต่การทำซ้ำของคำขอ A จะเขียนทับค่าดังกล่าวและตั้งค่า K เป็น 1
แม้ว่า UpdateAsync จะทำงานกับเวอร์ชันล่าสุดของค่าของคีย์ แต่การร้องขอ UpdateAsync ต้องดำเนินการตามลำดับเพื่อหลีกเลี่ยงสถานะชั่วคราวที่ไม่ถูกต้อง (เช่น การซื้อหักเหรียญก่อนที่การเพิ่มเหรียญจะถูกประมวลผล ส่งผลให้เหรียญติดลบ)
ระบบข้อมูลผู้เล่นของเราจะใช้คลาสใหม่ DataStoreWrapper ซึ่งมีให้บริการการพยายามใหม่ที่รับประกันว่าจะถูกประมวลผลตามลำดับต่อคีย์
แนวทาง

DataStoreWrapper มีวิธีการที่ตรงกับวิธี DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() และ DataStore:RemoveAsync().
เมื่อเรียกใช้วิธีการเหล่านี้:
เพิ่มคำขอเข้าไปในคิว แต่ละคีย์มีคิวของตนเองซึ่งคำขอจะถูกทำตามลำดับและในซีรีส์ เธรดที่ร้องขอจะหยุดจนกว่าคำขอจะเสร็จสมบูรณ์
ฟังก์ชันนี้อิงจากคลาส ThreadQueue ซึ่งเป็นตัวจัดกำหนดงานอิงจาก coroutine และอัตราการควบคุม แทนที่จะคืนค่าคำสัญญา ThreadQueue จะหยุดเธรดปัจจุบันจนกว่าจะเสร็จสมบูรณ์และโยนข้อผิดพลาดหากล้มเหลว สิ่งนี้สอดคล้องกับรูปแบบการทำงานแบบอะซิงโครนัสของ Luau
หากคำขอไม่สำเร็จจะทำการพยายามใหม่ด้วยการคืนค่าแบบกำหนดค่า โดยการพยายามใหม่เหล่านี้เป็นส่วนหนึ่งของการเรียกกลับที่ส่งไปยัง ThreadQueue ดังนั้นพวกเขาจะทำเสร็จสิ้นก่อนที่คำขอถัดไปในคิวสำหรับคีย์นี้จะเริ่ม
เมื่อคำขอเสร็จสิ้น วิธีการที่เรียกจะคืนค่าด้วย success, result รูปแบบ
DataStoreWrapper ยังมีให้บริการวิธีการเพื่อรับความยาวของคิวสำหรับคีย์ที่กำหนดและล้างคำขอที่ล้าสมัย ตัวเลือกที่หลังนี้มีประโยชน์โดยเฉพาะในสถานการณ์ที่เซิร์ฟเวอร์กำลังจะปิดและไม่มีเวลาในการประมวลผลคำขอนอกจากคำขอที่ล่าสุดที่สุด
เควส
DataStoreWrapper ปฏิบัติตามหลักการที่ว่า นอกเหนือจากกรณีที่รุนแรง คำขอ data store ทุกคำขอควรได้รับอนุญาตให้เสร็จสิ้น (ไม่ว่าอย่างไรก็ตาม) แม้ว่าคำขอที่ใหม่กว่าทำให้มันเป็นเรื่องซ้ำซ้อน เมื่อมีคำขอใหม่เกิดขึ้น คำขอที่ล้าสมัยจะไม่ถูกลบออกจากคิว แต่จะได้รับอนุญาตให้เสร็จสิ้นก่อนที่คำขอใหม่จะเริ่ม สิ่งที่ก่อให้เกิดเหตุการณ์นี้มีพื้นฐานมาจากความสามารถในการใช้งานของโมดูลนี้เป็นยูทิลิตี้ data store ทั่วไปมากกว่าการเป็นเครื่องมือเฉพาะทางสำหรับข้อมูลผู้เล่น และมีดังนี้:
การตัดสินใจเกี่ยวกับกฎชุดที่เข้าใจง่ายสำหรับเมื่อใดคำขอจึงจะปลอดภัยในการลบจากคิวเป็นเรื่องยาก พิจารณาคิวต่อไปนี้:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
พฤติกรรมที่คาดหวังคือว่า GetAsync() จะคืนค่า 1 แต่หากเราลบการร้องขอ SetAsync() ออกจากคิวเนื่องจากทำให้เป็นเรื่องซ้ำซ้อนโดยการร้องขอล่าสุด มันจะคืนค่า 0
ความก้าวหน้าที่มีเหตุผลคือเมื่อมีการเพิ่มคำขอเขียนใหม่ จะต้องตัดคำขอที่ล้าสมัยออกเฉพาะเท่าที่เป็นที่ยอมรับล่าสุดของการอ่านคำขอ UpdateAsync() ซึ่งเป็นการดำเนินการที่พบบ่อยที่สุด (และใช้โดยระบบนี้) สามารถอ่านและเขียนได้ จึงยากที่จะส่งเรื่องนี้ในการออกแบบนี้โดยไม่เพิ่มความยุ่งยากเพิ่มเติม
DataStoreWrapper อาจต้องกำหนดว่าการร้องขอ UpdateAsync() ได้รับอนุญาตให้อ่านและ/หรือเขียนหรือไม่ แต่จะไม่มีความเกี่ยวข้องกับระบบข้อมูลผู้เล่นของเรา ซึ่งไม่สามารถตัดสินล่วงหน้าได้เนื่องจากกลไกการล็อคเซสชัน (ซึ่งจะกล่างถึงในรายละเอียดเพิ่มเติมในภายหลัง)
เมื่อถูกลบออกจากคิว การตัดสินใจเกี่ยวกับกฎที่เข้าใจง่ายสำหรับ วิธีการ นี้ควรจัดการได้ยาก เมื่อมีการร้องขอ 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:
- เซิร์ฟเวอร์ A ทำการร้องขอ DataStore เพื่อบันทึกข้อมูลของพวกเขา แต่คำขอนั้นล้มเหลวและต้องการหลายครั้งในการพยายามใหม่เพื่อให้เสร็จสมบูรณ์ ในระหว่างช่วงเวลาการพยายามใหม่ ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
- เซิร์ฟเวอร์ A ทำการเรียกใช้ UpdateAsync() ที่มากเกินไปกับคีย์เดียวกันและถูกจำกัด คำขอการบันทึกขั้นสุดท้ายอยู่ในคิว ขณะที่คำขอนั้นอยู่ในคิว ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
- บนเซิร์ฟเวอร์ A มีโค้ดที่เชื่อมโยงกับเหตุการณ์ PlayerRemoving ซึ่งหยุดก่อนที่จะบันทึกข้อมูลของผู้เล่น ก่อนที่การทำงานนี้จะเสร็จสิ้น ผู้เล่นได้เข้าร่วมเซิร์ฟเวอร์ B
- ประสิทธิภาพของเซิร์ฟเวอร์ A ลดลงถึงจุดที่การบันทึกขั้นสุดท้ายล่าช้ากว่าระยะเวลาที่ผู้เล่นเข้าร่วมเซิร์ฟเวอร์ B
สถานการณ์เหล่านี้ควรเกิดขึ้นได้ยาก แต่พวกเขายังเกิดขึ้น โดยเฉพาะในสถานการณ์ที่ผู้เล่นตัดการเชื่อมต่อจากเซิร์ฟเวอร์ A และเชื่อมต่อกับเซิร์ฟเวอร์ B อย่างรวดเร็ว (ในกรณีเช่นระหว่างการเทเลพอร์ต) ผู้ใช้ที่ชั่วร้ายบางคนอาจพยายามใช้ประโยชน์จากพฤติกรรมนี้เพื่อดำเนินการโดยไม่ให้ข้อมูลถาวร สิ่งนี้อาจมีผลกระทบอย่างมากในเกมที่อนุญาตให้ผู้เล่นแลกเปลี่ยนกันและเป็นแหล่งที่มาของการใช้งานซ้ำที่พบบ่อย
การล็อคเซสชันจะจัดการกับช่องโหว่นี้โดยให้แน่ใจว่า เมื่อคีย์ DataStore ของผู้เล่นถูกอ่านครั้งแรกโดยเซิร์ฟเวอร์ เซิร์ฟเวอร์จะเขียนล็อคไปยังข้อมูลเมตาของคีย์อย่างอะตอมในคำขอ UpdateAsync() เดียวกัน หากค่าล็อคนี้มีอยู่เมื่อเซิร์ฟเวอร์อื่นพยายามอ่านหรือเขียนคีย์ เซิร์ฟเวอร์จะไม่ดำเนินการต่อ
แนวทาง

SessionLockedDataStoreWrapper เป็นเมต้า-Wrapper รอบคลาส DataStoreWrapper DataStoreWrapper ให้บริการการเรียงลำดับและฟังก์ชันการพยายามใหม่ซึ่ง SessionLockedDataStoreWrapper เสริมด้วยการล็อคเซสชัน
SessionLockedDataStoreWrapper ผ่านคำขอ DataStore ทุกคำขอ ไม่ว่ามันจะเป็น GetAsync, SetAsync หรือ UpdateAsync ผ่าน UpdateAsync ที่เป็นสาเหตุ เพราะ UpdateAsync ช่วยให้คีย์นั้นถูกอ่านและเขียนเป็นอะตอมได้ นอกจากนี้ยังสามารถเลิกเขียนได้ตามค่าที่อ่านโดยการคืนค่า nil ในการคืนค่าการแปรรูป
ฟังก์ชันการแปรรูปที่ผ่านไปใน UpdateAsync สำหรับคำขอแต่ละข้อมีการดำเนินการต่อไปนี้:
ตรวจสอบว่าคีย์นั้นปลอดภัยต่อการเข้าถึง ปล่อยการดำเนินการหากไม่ใช่ "ปลอดภัยในการเข้าถึง" หมายถึง:
ข้อมูลเมตาของคีย์ไม่มีค่า LockId ที่ไม่รู้จักซึ่งได้รับการอัปเดตล่าสุดน้อยกว่าระยะเวลาหมดอายุของล็อต ป็นการพิจารณาในการเคารพล็อคที่วางโดยเซิร์ฟเวอร์อื่น และละเว้นล็อคหากมันหมดอายุ
หากเซิร์ฟเวอร์นี้ได้วางค่า LockId เองไว้ในข้อมูลเมตาของคีย์ก่อนหน้านี้ แล้วค่านั้นยังอยู่ในข้อมูลเมตาของคีย์อยู่ แสดงถึงสถานการณ์ที่เซิร์ฟเวอร์อื่นได้เข้ายึดล็อคของเซิร์ฟเวอร์นี้ (โดยหมดอายุหรือตามคำสั่ง) และในภายหลังได้ปล่อยมันไป หรือกล่าวอีกนัยหนึ่งแม้ว่าค่า LockId จะเป็น nil เซิร์ฟเวอร์อื่นอาจได้แทนที่และลบล็อคในระยะเวลาที่คุณล็อคคีย์
UpdateAsync จะดำเนินการตาม DataStore ของที่ผู้บริโภค SessionLockedDataStoreWrapper ได้ร้องขอ เช่น GetAsync() แปลไปยัง function(value) return value end.
ขึ้นอยู่กับพารามิเตอร์ที่ถูกส่งไปในคำขอ UpdateAsync จะทำการล็อคหรือยกเลิกล็อคคีย์:
หากคีย์จะถูกล็อค UpdateAsync จะตั้งค่า LockId ในข้อมูลเมตาของคีย์เป็น GUID GUID นี้จะถูกเก็บในหน่วยความจำบนเซิร์ฟเวอร์เพื่อให้สามารถตรวจสอบค่าเมื่อเข้าถึงคีย์ในครั้งถัดไป หากเซิร์ฟเวอร์มีล็อคอยู่แล้วบนคีย์นี้จะไม่มีการเปลี่ยนแปลงเกิดขึ้น มันสามารถจัดตารางงานเพื่อเตือนคุณหากคุณไม่เข้าถึงคีย์อีกครั้งภายในระยะเวลาหมดอายุของล็อค
หากคีย์จะได้รับการปลดล็อค UpdateAsync จะลบค่า LockId ในข้อมูลเมตาของคีย์
การจัดการความพยายามใหม่แบบกำหนดเองจะถูกส่งต่อไปยัง DataStoreWrapper ที่อยู่เบื้องหลัง ดังนั้นการดำเนินการจึงถูกพยายามใหม่หากถูกยกเลิกในขั้นตอนที่ 1 เนื่องจากเซสชันถูกล็อค
ข้อความข้อผิดพลาดที่กำหนดเองจะถูกส่งกลับไปยังผู้บริโภคด้วย ซึ่งอนุญาตให้ระบบข้อมูลผู้เล่นรายงานข้อผิดพลาดทางเลือกในกรณีของการล็อคเซสชันให้กับคลายเอนท์
เควส
ระเบียบการล็อคเซสชันขึ้นอยู่กับเซิร์ฟเวอร์ที่จะปล่อยล็อคในคีย์ทุกครั้งที่เสร็จสิ้นกับมัน ควรทำเช่นนี้เสมอผ่านคำสั่งเพื่อปลดล็อคคีย์ในระหว่างการ writes สุดท้ายใน PlayerRemoving หรือ BindToClose().
อย่างไรก็ตาม การปลดล็อคอาจล้มเหลวในสถานการณ์บางอย่าง ตัวอย่างเช่น:
- เซิร์ฟเวอร์หยุดทำงานหรือ DataStoreService ไม่สามารถใช้งานได้ตลอดการเข้าถึงคีย์
- เนื่องจากข้อผิดพลาดในตรรกะหรือข้อบกพร่องในลักษณะเดียวกัน คำสั่งในการปลดล็อคคีย์ไม่ได้ทำ
เพื่อรักษาล็อคในคีย์ คุณต้องเข้าถึงมันเป็นประจำตลอดระยะเวลาที่มันถูกโหลดในหน่วยความจำ โดยปกติจะทำได้เป็นส่วนหนึ่งของลูปการบันทึกอัตโนมัติที่ทำงานอยู่เบื้องหลังในระบบข้อมูลผู้เล่นส่วนใหญ่ แต่ระบบนี้ยังให้บริการวิธี refreshLockAsync หากคุณจำเป็นต้องทำด้วยตนเอง
หากระยะเวลาหมดอายุล็อคถูกเกินล่วงหน้าโดยที่ล็อคไม่ถูกอัปเดตใด ๆ เซิร์ฟเวอร์ใด ๆ ก็สามารถเข้ามาได้ตามอิสระ หากเซิร์ฟเวอร์อื่นเข้ามาล็อค คำพยายามของเซิร์ฟเวอร์ปัจจุบันในการอ่านหรือเขียนคีย์จะล้มเหลว เว้นแต่มันจะได้กำหนดล็อคใหม่อีกครั้ง
การประมวลผลผลิตภัณฑ์นักพัฒนา
Singleton: ReceiptHandler
เบื้องหลัง
ฟังก์ชันคืนค่าของ ProcessReceipt ทำหน้าที่ที่สำคัญอย่างยิ่งในการกำหนดว่าเมื่อใดที่จะทำการซื้อให้เสร็จสิ้น ProcessReceipt จะถูกเรียกในสถานการณ์ที่เฉพาะเจาะจง สำหรับชุดของการรับประกันโปรดดูที่ MarketplaceService.ProcessReceipt.
แม้ว่าความหมายของ "การจัดการ" การซื้อจะแตกต่างกันไปตามแต่ละเกม แต่เราจะใช้เกณฑ์ต่อไปนี้
การซื้อยังไม่ได้รับการจัดการมาก่อน
การซื้อสะท้อนอยู่ในเซสชันปัจจุบัน
นี้ต้องทำการดำเนินการต่อไปนี้ก่อนที่จะคืนค่า PurchaseGranted:
- ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการจัดการ
- มอบรางวัลการซื้อในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
- บันทึก PurchaseId ให้เป็นที่จัดการในข้อมูลผู้เล่นในหน่วยความจำของผู้เล่น
- เขียนข้อมูลผู้เล่นในหน่วยความจำของผู้เล่นลงใน DataStore.
การล็อคเซสชันทำให้การเปลี่ยนแปลงนี้ง่ายขึ้น เพราะคุณไม่ต้องกังวลเกี่ยวกับสถานการณ์ต่อไปนี้:
- ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์ปัจจุบันอาจจะไม่เป็นปัจจุบัน ต้องการให้คุณดึงค่าล่าสุดจาก DataStore ก่อนที่จะตรวจสอบประวัติ PurchaseId
- การเรียกกลับสำหรับการซื้อเดียวกันจะทำงานในเซิร์ฟเวอร์อื่น ทำให้คุณต้องทั้งอ่านและเขียนประวัติ PurchaseId และบันทึกข้อมูลผู้เล่นที่อัปเดตด้วยการซื้ออย่างอะตอมเพื่อป้องกันการแข่งขัน
การล็อคเซสชันรับประกันว่า หากการพยายามเขียนใน DataStore ของผู้เล่นสำเร็จ จะไม่มีเซิร์ฟเวอร์อื่นใดที่ได้อ่านหรือเขียน DataStore ของผู้เล่นสำเร็จในระหว่างการโหลดข้อมูลและการบันทึกในเซิร์ฟเวอร์นี้ ในสั้น ข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เป็นเวอร์ชันล่าสุดที่มีอยู่ อาจมีข้อควรระวังบางประการ แต่จะไม่มีผลกระทบต่อพฤติกรรมนี้
แนวทาง
ความคิดเห็นใน ReceiptProcessor สรุปแนวทาง:
ตรวจสอบว่าข้อมูลของผู้เล่นถูกโหลดในเซิร์ฟเวอร์นี้และโหลดโดยไม่มีข้อผิดพลาดใดๆ
เนื่องจากระบบนี้ใช้การล็อคเซสชัน การตรวจสอบนี้ยังช่วยยืนยันว่าข้อมูลในหน่วยความจำเป็นเวอร์ชันล่าสุดที่มีอยู่
หากข้อมูลของผู้เล่นยังไม่ได้โหลด (ซึ่งคาดหวังเมื่อผู้เล่นเข้าร่วมเกม) ให้รอจนกว่าข้อมูลของผู้เล่นจะโหลด ระบบยังฟังเมื่อผู้เล่นออกจากเกมก่อนที่ข้อมูลของพวกเขาจะโหลด เพื่อไม่ให้มันหยุดถาวรและบล็อกให้การเรียกกลับนี้ถูกเรียกอีกครั้งในเซิร์ฟเวอร์นี้สำหรับการซื้อหากผู้เล่นเข้าใหม่
ตรวจสอบว่า PurchaseId ยังไม่ได้ถูกบันทึกว่าได้รับการประมวลผลในข้อมูลผู้เล่น
เนื่องจากการล็อคเซสชัน อาเรย์ของ PurchaseIds ที่ระบบมีในหน่วยความจำเป็นเวอร์ชันล่าสุด หาก PurchaseId ถูกบันทึกว่าได้รับการจัดการและสะท้อนอยู่ในค่าที่ถูกโหลดหรือบันทึกไปยัง DataStore ให้คืนค่า PurchaseGranted. หากมีการบันทึกว่าได้ถูกจัดการ แต่ ไม่ได้ สะท้อนอยู่ใน DataStore ให้คืนค่า NotProcessedYet.
อัปเดตข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เพื่อ "มอบรางวัล" การซื้อ.
ReceiptProcessor ใช้แนวทางการเรียกกลับทั่วไปและกำหนดการเรียกกลับที่แตกต่างกันสำหรับแต่ละ DeveloperProductId.
อัปเดตข้อมูลผู้เล่นในหน่วยความจำในเซิร์ฟเวอร์นี้เพื่อบันทึก PurchaseId.
ส่งคำขอเพื่อบันทึกข้อมูลในหน่วยความจำลงใน DataStore คืนค่ากลับเป็น PurchaseGranted หากคำขอนั้นสำเร็จ หากไม่คืนค่า NotProcessedYet.
หากคำขอการบันทึกนี้ไม่สำเร็จ คำขอในอนาคตเพื่อบันทึกข้อมูลเซสชันในหน่วยความจำของผู้เล่นยังคงสำเร็จ ในช่วงการเรียก ProcessReceipt ถัดไป ขั้นตอนที่ 2 จะจัดการกับสถานการณ์นี้และคืนค่า PurchaseGranted.
ข้อมูลผู้เล่น
Singletons: PlayerData.Server, PlayerData.Client
เบื้องหลัง
โมดูลที่ให้บริการอินเทอร์เฟซสำหรับให้โค้ดอ่านและเขียนข้อมูลเซสชันของผู้เล่นแบบซิงโครนัสนั้นเป็นที่นิยมในเกม Roblox หมวดนี้จะกล่าวถึง PlayerData.Server และ PlayerData.Client.
แนวทาง
PlayerData.Server และ PlayerData.Client ดำเนินการตาม:
- โหลดข้อมูลของผู้เล่นลงในหน่วยความจำรวมถึงการจัดการกรณีที่ล้มเหลวในการโหลด
- ให้บริการอินเทอร์เฟซสำหรับโค้ดเซิร์ฟเวอร์เพื่อสอบถามและเปลี่ยนแปลงข้อมูลผู้เล่น
- ถ่ายทอดการเปลี่ยนแปลงในข้อมูลผู้เล่นไปยังคลายเอนท์เพื่อให้โค้ดของคลายเอนท์สามารถเข้าถึงได้
- ถ่ายทอดความผิดพลาดในการโหลดและ/หรือบันทึกให้กับคลายเอนท์เพื่อให้แสดงกล่องโต้ตอบข้อผิดพลาด
- บันทึกข้อมูลผู้เล่นอย่างสม่ำเสมอ เมื่อผู้เล่นออกจากเกม และเมื่อเซิร์ฟเวอร์ปิด
โหลดข้อมูลผู้เล่น

SessionLockedDataStoreWrapper ทำคำขอ getAsync ไปยัง data store.
หากคำขอนี้ล้มเหลว ข้อมูลเริ่มต้นจะถูกใช้และโปรไฟล์จะถูกตีตราว่า "ผิดพลาด" เพื่อให้แน่ใจว่าจะไม่ถูกเขียนไปยัง data store ในภายหลัง
ทางเลือกอื่นคือการเตะผู้เล่นออก แต่เราขอแนะนำให้ผู้เล่นเล่นด้วยข้อมูลเริ่มต้นและสื่อสารอย่างชัดเจนเกี่ยวกับสิ่งที่เกิดขึ้นแทนที่จะลบพวกเขาออกจากเกม
ส่งข้อมูลเริ่มต้นไปยัง PlayerDataClient ที่ประกอบด้วยข้อมูลที่โหลดและสถานะข้อผิดพลาด (ถ้ามี)
เธรดใด ๆ ที่หยุดด้วย 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 (ดูที่ การล็อคเซสชัน).
- ใช้เหตุการณ์เหล่านี้ในการปิดคำชวนการซื้อของคลายเอนท์และดำเนินการสร้างกล่องโต้ตอบเตือน สิ่งนี้แสดงถึงกล่องโต้ตอบเตือนที่สามารถแสดงเมื่อข้อมูลผู้เล่นไม่สามารถโหลดได้:

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

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