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

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

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

SessionLockedDataStoreWrapper เป็น meta-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 ในเมตาดาต้าของกุญแจเป็น GUIDUID นี้จะถูกเก็บไว้ในหน่วยความจําบนเซิร์ฟเวอร์เพื่อให้สามารถตรวจสอบได้ในครั้งต่อไปที่เข้าถึงกุญแจหากเซิร์ฟเวอร์มีล็อคบนกุญแจนี้แล้ว มันจะไม่ทำการเปลี่ยนแปลงนอกจากนี้ยังกำหนดเวลาในการเตือนคุณหากคุณไม่ได้เข้าถึงกุญแจอีกครั้งเพื่อรักษาการล็อคภายในเวลาหมดอายุของกุญแจ
หากกุญแจต้องถูกปลดล็อก UpdateAsync ลบ LockId คีย์
ตัวจัดการลองอีกครั้งที่กําหนดเองจะถูกส่งไปยัง DataStoreWrapper พื้นฐานเพื่อให้การดําเนินการถูกลองอีกครั้งหากถูกยกเลิกในขั้นตอนที่ 1 เนื่องจากเซสชันถูกล็อค
ข้อความข้อผิดพลาดที่กําหนดเองยังถูกส่งกลับไปยังผู้บริโภคเพื่อให้ระบบข้อมูลผู้เล่นรายงานข้อผิดพลาดทางเลือกในกรณีที่การล็อคเซสชันไปยังไคลเอนต์
ข้อควรระวัง
ระบบล็อคเซสชั่นใช้เซิร์ฟเวอร์ปล่อยล็อคบนกุญแจเสมอเมื่อเสร็จสิ้นการทำงานสิ่งนี้ควรเกิดขึ้นเสมอผ่านคําสั่งเพื่อปลดล็อกกุญแจเป็นส่วนหนึ่งของการเขียนครั้งสุดท้ายใน PlayerRemoving หรือ BindToClose()
อย่างไรก็ตาม การปลดล็อกอาจล้มเหลวในบางสถานการณ์ ตัวอย่างเช่น:
- เซิร์ฟเวอร์ล้มเหลวหรือ DataStoreService คีย์
- เนื่องจากข้อผิดพลาดในเหตุผลหรือข้อบกพร่องที่คล้ายกัน, คำสั่งเพื่อปลดล็อกกุญแจไม่ได้ถูกสร้างขึ้น
เพื่อรักษาการล็อคบนกุญแจคุณต้องเข้าถึงอย่างสม่ำเสมอตราบเท่าที่มันโหลดในหน่วยความจำสิ่งนี้มักจะทำเป็นส่วนหนึ่งของวงจรบันทึกอัตโนมัติที่ทำงานในพื้นหลังในระบบข้อมูลผู้เล่นส่วนใหญ่ แต่ระบบนี้ยังเปิดเผยวิธี refreshLockAsync หากคุณต้องทำมันด้วยตนเอง
หากเวลาหมดอายุการล็อคถูกเกินโดยไม่มีการอัปเดตการล็อคแล้ว เซิร์ฟเวอร์ใดก็ได้จะสามารถยึดการล็อคได้หากเซิร์ฟเวอร์อื่นล็อคไว้ การพยายามอ่านหรือเขียนกุญแจโดยเซิร์ฟเวอร์ปัจจุบันจะล้มเหลว เว้นแต่จะสร้างกุญแจใหม่
การประมวลผลผลิตภัณฑ์นักพัฒนา
โซลเดียน: 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
ข้อมูลผู้เล่น
ซิงเลตอน: PlayerData.Server , PlayerData.Client
พื้นหลัง
โมดูลที่ให้อินเทอร์เฟซสําหรับโค้ดเกมเพื่ออ่านและเขียนข้อมูลเซสชันผู้เล่นได้อย่างสะดวกในเวลาเดียวกันเป็นที่นิยมในประสบการณ์ Robloxส่วนนี้ครอบคลุม PlayerData.Server และ PlayerData.Client .
วิธีการ
PlayerData.Server และ PlayerData.Client กำลังติดตาม:
- โหลดข้อมูลผู้เล่นลงในหน่วยความจำรวมถึงจัดการกรณีที่ไม่สามารถโหลดได้
- ให้อินเทอร์เฟซสําหรับรหัสเซิร์ฟเวอร์เพื่อสอบถามและเปลี่ยนข้อมูลผู้เล่น
- ส่งการเปลี่ยนแปลงในข้อมูลผู้เล่นไปยังไคลเอนต์เพื่อให้รหัสไคลเอนต์สามารถเข้าถึงได้
- การเลียนแบบข้อผิดพลาดในการโหลดและ/หรือบันทึกไปยังไคลเอนต์เพื่อให้สามารถแสดงข้อความข้อผิดพลาดได้
- บันทึกข้อมูลผู้เล่นเป็นระยะเมื่อผู้เล่นออกและเมื่อเซิร์ฟเวอร์ปิด
โหลดข้อมูลผู้เล่น

SessionLockedDataStoreWrapper สร้างคำขอ getAsync ไปยังร้านข้อมูล
หากคำขอนี้ล้มเหลว ข้อมูลเริ่มต้นจะถูกใช้และโปรไฟล์จะถูกทําเครื่องหมายว่า "ผิดพลาด" เพื่อให้แน่ใจว่าจะไม่ถูกเขียนลงในคลังข้อมูลในภายหลัง
ตัวเลือกทางเลือกคือการเตะผู้เล่น แต่เราแนะนำให้ให้ผู้เล่นเล่นด้วยข้อมูลเริ่มต้นและล้างการสื่อสารตามที่เกิดขึ้นแทนที่จะลบพวกเขาออกจากประสบการณ์
การจ่ายเริ่มแรกถูกส่งไปยัง PlayerDataClient ที่มีข้อมูลที่โหลดและสถานะข้อผิดพลาด (ถ้ามี)
กระทู้ใดๆ ที่ได้รับโดยใช้ waitForDataLoadAsync สำหรับผู้เล่นจะถูกดำเนินการต่อ
ให้อินเทอร์เฟซสําหรับรหัสเซิร์ฟเวอร์
- PlayerDataServer เป็นโซลเดี่ยวที่สามารถต้องการและเข้าถึงได้โดยรหัสเซิร์ฟเวอร์ใดๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน
- ข้อมูลผู้เล่นจะถูกจัดเป็นสารานุกรมของคีย์และค่าคุณสามารถควบคุมค่าเหล่านี้บนเซิร์ฟเวอร์โดยใช้วิธีการ setValue , getValue , updateValue และ removeValueวิธีเหล่านี้ทั้งหมดทำงานอย่างสอดคล้องโดยไม่ยอมแพ้
- วิธี hasLoaded และ waitForDataLoadAsync มีอยู่เพื่อให้แน่ใจว่าข้อมูลได้โหลดก่อนที่คุณจะเข้าถึงเราแนะนำให้ทำเช่นนี้หนึ่งครั้งในระหว่างหน้าจอการโหลดก่อนที่ระบบอื่นจะเริ่มต้นเพื่อหลีกเลี่ยงการตรวจสอบข้อผิดพลาดการโหลดก่อนทุกการโต้ตอบกับข้อมูลบนไคลเอนต์
- วิธี hasErrored สามารถสอบถามได้หากการโหลดเริ่มต้นของผู้เล่นล้มเหลวทำให้พวกเขาใช้ข้อมูลเริ่มต้นตรวจสอบวิธีนี้ก่อนที่จะอนุญาตให้ผู้เล่นทำการซื้อ เนื่องจากการซื้อไม่สามารถบันทึกไปยังข้อมูลได้หากไม่มีการโหลดที่ประสบความสำเร็จ
- สัญญาณ playerDataUpdated จะปิดด้วย player , key และ value เมื่อมีการเปลี่ยนแปลงข้อมูลของผู้เล่นระบบแต่ละระบบสามารถสมัครรับสิ่งนี้ได้
สร้างการเปลี่ยนแปลงให้กับไคลเอนต์
- การเปลี่ยนแปลงใดๆ ในข้อมูลผู้เล่นใน PlayerDataServer จะถูกส่งไปยัง PlayerDataClient ยกเว้นถ้ากุญแจถูกทําเครื่องหมายว่าเป็นส่วนตัวโดยใช้ setValueAsPrivate
- setValueAsPrivate ใช้เพื่อระบุกุญแจที่ไม่ควรส่งไปยังไคลเอนต์
- PlayerDataClient รวมถึงวิธีการรับค่าของกุญแจ (รับ) และสัญญาณที่จะยิงเมื่อมันได้รับการอัปเดต (อัปเดต)วิธี hasLoaded และสัญญาณ loaded ยังรวมอยู่ด้วยเพื่อให้ลูกค้าสามารถรอให้ข้อมูลโหลดและสําเนาก่อนที่จะเริ่มระบบของตน
- PlayerDataClient เป็นโซลเดี่ยวที่สามารถต้องการและเข้าถึงได้โดยรหัสไคลเอนต์ใดๆ ที่ทำงานในสภาพแวดล้อมเดียวกัน
ส่งข้อผิดพลาดซ้ำไปยังไคลเอนต์
- สถานะข้อผิดพลาดที่พบเมื่อบันทึกหรือโหลดข้อมูลผู้เล่นจะถูกส่งต่อไปยัง PlayerDataClient
- เข้าถึงข้อมูลนี้ด้วยวิธี getLoadError และ getSaveError รวมถึงสัญญาณ loaded และ saved
- มีสองประเภทของข้อผิดพลาด: DataStoreError (คำขอ DataStoreService ล้มเหลว) และ SessionLocked (ดู การล็อคเซสชัน ).
- ใช้เหตุการณ์เหล่านี้เพื่อปิดการแจ้งเตือนการซื้อของไคลเอนต์และดำเนินการใช้บทสนทนาเตือน ภาพนี้แสดงตัวอย่างบทสนทนา:

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

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