Latar Belakang
Roblox menyediakan serangkaian API untuk berinteraksi dengan data store melalui DataStoreService. Kasus penggunaan yang paling umum untuk API ini adalah untuk menyimpan, memuat, dan mereplikasi data pemain. Yaitu, data yang terkait dengan kemajuan pemain, pembelian, dan karakteristik sesi lainnya yang bertahan antara sesi permainan individu.
Sebagian besar permainan di Roblox menggunakan API ini untuk mengimplementasikan beberapa bentuk sistem data pemain. Implementasi ini berbeda dalam pendekatannya, tetapi umumnya berusaha menyelesaikan masalah yang sama.
Masalah Umum
Berikut adalah beberapa masalah umum yang coba diselesaikan oleh sistem data pemain:
Akses Dalam Memori: Permintaan DataStoreService membuat permintaan web yang beroperasi secara asinkron dan tunduk pada batasan laju. Ini sesuai untuk pemuatan awal di awal sesi, tetapi tidak untuk operasi baca dan tulis frekuensi tinggi selama jalannya permainan. Sebagian besar sistem data pemain pengembang menyimpan data ini dalam memori di server Roblox, membatasi permintaan DataStoreService ke skenario berikut:
- Pembacaan awal di awal sesi
- Penulisan akhir di akhir sesi
- Penulisan berkala pada interval untuk mengurangi skenario di mana penulisan akhir gagal
- Penulisan untuk memastikan data disimpan saat memproses pembelian
Penyimpanan Efisien: Menyimpan semua data sesi pemain dalam satu tabel memungkinkan Anda memperbarui beberapa nilai secara atomik dan menangani jumlah data yang sama dalam lebih sedikit permintaan. Ini juga menghilangkan risiko desinkronisasi antar nilai dan membuat rollback lebih mudah untuk dipahami.
Beberapa pengembang juga menerapkan serialisasi kustom untuk mengompresi struktur data besar (biasanya untuk menyimpan konten yang dihasilkan pengguna dalam permainan).
Replikasi: Klien memerlukan akses reguler ke data pemain (misalnya, untuk memperbarui UI). Pendekatan umum untuk mereplikasi data pemain ke klien memungkinkan Anda mentransmisikan informasi ini tanpa harus membuat sistem replikasi khusus untuk setiap komponen data. Pengembang sering ingin memiliki opsi untuk selektif tentang apa yang direplikasi ke klien dan apa yang tidak.
Penanganan Kesalahan: Ketika DataStores tidak dapat diakses, sebagian besar solusi akan menerapkan mekanisme pengulangan dan fallback ke data 'default'. Perhatian khusus diperlukan untuk memastikan data fallback tidak kemudian menimpa data 'nyata', dan bahwa ini dikomunikasikan kepada pemain dengan tepat.
Pengulangan: Ketika data store tidak dapat diakses, sebagian besar solusi menerapkan mekanisme pengulangan dan fallback ke data default. Ambil perhatian khusus untuk memastikan bahwa data fallback tidak kemudian menimpa data "nyata", dan komunikasikan situasi tersebut kepada pemain dengan tepat.
Penguncian Sesi: Jika data pemain tunggal dimuat dan ada dalam memori di beberapa server, masalah dapat terjadi di mana satu server menyimpan informasi yang sudah ketinggalan zaman. Ini dapat menyebabkan kehilangan data dan celah duplikasi item yang umum.
Penanganan Pembelian Atomik: Verifikasi, berikan, dan catat pembelian secara atomik untuk mencegah item hilang atau diberikan beberapa kali.
Contoh Kode
Roblox memiliki kode referensi untuk membantu Anda merancang dan membangun sistem data pemain. Sisa halaman ini membahas latar belakang, detail implementasi, dan caveat umum.
Setelah Anda mengimpor model ke Studio, Anda harus melihat struktur folder berikut:

Arsitektur
Diagram tingkat tinggi ini menggambarkan sistem kunci dalam contoh dan bagaimana mereka berinteraksi dengan kode di sisa permainan.

Pengulangan
Class: DataStoreWrapper
Latar Belakang
Karena DataStoreService membuat permintaan web di belakang layar, permintaannya tidak dijamin untuk berhasil. Ketika ini terjadi, metode DataStore melempar kesalahan, memungkinkan Anda untuk menanganinya.
Sebuah "gotcha" umum dapat terjadi jika Anda mencoba menangani kegagalan data store seperti ini:
local MAX_ATTEMPTS = 5
local BASE_DELAY = 2
local MAX_DELAY = 32
local function retrySetAsync(dataStore, key, value)
for attempt = 1, MAX_ATTEMPTS do
local success, result = pcall(dataStore.SetAsync, dataStore, key, value)
if success then
return result
end
if attempt < MAX_ATTEMPTS then
local backoff = math.min(MAX_DELAY, BASE_DELAY * (2 ^ (attempt - 1)))
local jitter = math.random() * backoff
task.wait(math.min(MAX_DELAY, backoff + jitter))
end
end
endUlangi kegagalan sementara dengan backoff eksponensial dan jitter acak sehingga server tidak mencoba ulang secara bersamaan. Batasi penundaan dan jumlah percobaan.
Bahkan dengan pola penundaan itu, mekanisme pengulangan ini tidak cocok untuk permintaan DataStoreService karena tidak menjamin urutan di mana permintaan dilakukan. Mempertahankan urutan permintaan sangat penting untuk permintaan DataStoreService karena mereka berinteraksi dengan status. Pertimbangkan skenario berikut:
- Permintaan A dibuat untuk mengatur nilai kunci K menjadi 1.
- Permintaan gagal, jadi pengulangan dijadwalkan untuk dijalankan setelah penundaan backoff.
- Sebelum pengulangan terjadi, permintaan B mengatur nilai K menjadi 2, tetapi pengulangan permintaan A segera menimpa nilai ini dan mengatur K menjadi 1.
Meskipun UpdateAsync beroperasi pada versi terbaru dari nilai kunci, permintaan UpdateAsync masih harus diproses dalam urutan untuk menghindari keadaan sementara yang tidak valid (misalnya, pembelian mengurangi koin sebelum penambahan koin diproses, menghasilkan koin negatif).
Sistem data pemain kami menggunakan kelas baru, DataStoreWrapper, yang menyediakan pengulangan yang dijamin diproses dalam urutan per kunci.
Pendekatan

DataStoreWrapper menyediakan metode yang sesuai dengan metode DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync() dan DataStore:RemoveAsync().
Metode ini, ketika dipanggil:
Menambahkan permintaan ke antrean. Setiap kunci memiliki antreannya sendiri, di mana permintaan diproses dalam urutan dan secara seri. Thread yang meminta menunggu hingga permintaan selesai.
Fungsionalitas ini didasarkan pada kelas ThreadQueue, yang merupakan penjadwal tugas berbasis coroutine dan pembatas laju. Alih-alih mengembalikan janji, ThreadQueue menunggu thread saat ini hingga operasi selesai dan melempar kesalahan jika gagal. Ini lebih konsisten dengan pola asinkron Luau yang idiomatik.
Jika permintaan gagal, ia mencoba ulang dengan backoff eksponensial yang dapat dikonfigurasi. Pengulangan ini merupakan bagian dari callback yang diajukan ke ThreadQueue, sehingga mereka dijamin selesai sebelum permintaan berikutnya dalam antrean untuk kunci ini dimulai.
Ketika permintaan selesai, metode permintaan mengembalikan pola success, result
DataStoreWrapper juga mengekspos metode untuk mendapatkan panjang antrean untuk kunci tertentu dan menghapus permintaan yang sudah usang. Opsi terakhir ini sangat berguna dalam skenario ketika server sedang dimatikan dan tidak ada waktu untuk memproses permintaan selain yang paling baru.
Caveat
DataStoreWrapper mengikuti prinsip bahwa, di luar skenario ekstrem, setiap permintaan data store harus diizinkan untuk diselesaikan (berhasil atau tidak), bahkan jika permintaan yang lebih baru membuatnya menjadi tidak relevan. Ketika permintaan baru terjadi, permintaan yang sudah usang tidak dihapus dari antrean, tetapi diizinkan untuk diselesaikan sebelum permintaan baru dimulai. Alasan untuk ini berakar pada penerapan modul ini sebagai utilitas data store umum daripada alat khusus untuk data pemain, dan adalah sebagai berikut:
Sulit untuk memutuskan seperangkat aturan intuitif untuk kapan permintaan aman untuk dihapus dari antrean. Pertimbangkan antrean berikut:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Perilaku yang diharapkan adalah bahwa GetAsync() akan mengembalikan 1, tetapi jika kita menghapus permintaan SetAsync() dari antrean karena dianggap tidak relevan oleh yang terbaru, itu akan mengembalikan 0.
Progres logis adalah bahwa ketika permintaan tulis baru ditambahkan, hanya memangkas permintaan yang sudah usang sejauh permintaan baca terbaru. UpdateAsync(), yang merupakan operasi paling umum (dan satu-satunya yang digunakan oleh sistem ini), dapat membaca dan menulis, jadi akan sulit untuk mendamaikan ini dalam desain ini tanpa menambah kompleksitas ekstra.
DataStoreWrapper dapat mengharuskan Anda untuk menentukan apakah permintaan UpdateAsync() diizinkan untuk membaca dan/atau menulis, tetapi itu tidak akan berlaku untuk sistem data pemain kami, di mana ini tidak dapat ditentukan sebelumnya karena mekanisme penguncian sesi (dibahas lebih detail nanti).
Setelah dihapus dari antrean, sulit untuk memutuskan aturan intuitif untuk bagaimana ini harus ditangani. Ketika permintaan DataStoreWrapper dibuat, thread saat ini ditunggu hingga selesai. Jika kita menghapus permintaan yang sudah usang dari antrean, kita harus memutuskan apakah akan mengembalikan false, "Dihapus dari antrean" atau tidak pernah mengembalikan dan membuang thread aktif. Kedua pendekatan ini memiliki kekurangan masing-masing dan membebankan kompleksitas tambahan kepada konsumen.
Akhirnya, pandangan kami adalah bahwa pendekatan sederhana (memproses setiap permintaan) lebih disukai di sini dan menciptakan lingkungan yang lebih jelas untuk dinavigasi ketika menghadapi masalah kompleks seperti penguncian sesi. Satu-satunya pengecualian untuk ini adalah selama DataModel:BindToClose(), di mana menghapus antrean menjadi perlu untuk menyimpan data semua pengguna tepat waktu dan nilai yang dikembalikan oleh panggilan fungsi individu tidak lagi menjadi perhatian yang sedang berlangsung. Untuk konteks lebih lanjut, lihat Data Pemain.
Penguncian Sesi
Class: SessionLockedDataStoreWrapper
Latar Belakang
Data pemain disimpan dalam memori di server dan hanya dibaca dan ditulis ke data store yang mendasarinya saat diperlukan. Anda dapat membaca dan memperbarui data pemain dalam memori secara instan tanpa memerlukan permintaan web dan menghindari melebihi batas DataStoreService.
Agar model ini berfungsi seperti yang diinginkan, sangat penting bahwa tidak lebih dari satu server dapat memuat data pemain ke dalam memori dari DataStore pada saat yang sama.
Sebagai contoh, jika server A memuat data pemain, server B tidak dapat memuat data tersebut hingga server A melepaskan kuncinya selama penyimpanan akhir. Tanpa mekanisme penguncian, server B dapat memuat data pemain yang sudah ketinggalan zaman dari data store sebelum server A memiliki kesempatan untuk menyimpan versi terbaru yang ada dalam memori. Kemudian jika server A menyimpan datanya yang lebih baru setelah server B memuat data yang sudah ketinggalan zaman, server B akan menimpa data yang lebih baru tersebut selama penyimpanan berikutnya.
Meskipun Roblox hanya mengizinkan klien terhubung ke satu server pada satu waktu, Anda tidak dapat mengasumsikan bahwa data dari satu sesi selalu disimpan sebelum sesi berikutnya dimulai. Pertimbangkan skenario berikut yang dapat terjadi ketika seorang pemain meninggalkan server A:
- Server A membuat permintaan DataStore untuk menyimpan datanya, tetapi permintaan gagal dan memerlukan beberapa pengulangan untuk berhasil diselesaikan. Selama periode pengulangan, pemain bergabung dengan server B.
- Server A membuat terlalu banyak panggilan UpdateAsync() ke kunci yang sama dan dibatasi. Permintaan penyimpanan akhir ditempatkan dalam antrean. Sementara permintaan berada dalam antrean, pemain bergabung dengan server B.
- Di server A, beberapa kode yang terhubung ke acara PlayerRemoving menunggu sebelum data pemain disimpan. Sebelum operasi ini selesai, pemain bergabung dengan server B.
- Kinerja server A telah menurun hingga titik di mana penyimpanan akhir tertunda hingga setelah pemain bergabung dengan server B.
Skenario ini seharusnya jarang terjadi, tetapi memang terjadi, terutama dalam situasi di mana seorang pemain terputus dari satu server dan terhubung ke server lain dengan cepat (misalnya, saat teleportasi). Beberapa pengguna jahat bahkan mungkin mencoba menyalahgunakan perilaku ini untuk menyelesaikan tindakan tanpa mereka bertahan. Ini bisa sangat berdampak dalam permainan yang memungkinkan pemain untuk berdagang dan merupakan sumber umum eksploitasi duplikasi item.
Penguncian sesi mengatasi kerentanan ini dengan memastikan bahwa ketika kunci DataStore pemain pertama kali dibaca oleh server, server secara atomik menulis kunci ke metadata kunci dalam panggilan UpdateAsync() yang sama. Jika nilai kunci ini ada ketika server lain mencoba membaca atau menulis kunci tersebut, server tidak melanjutkan.
Pendekatan

SessionLockedDataStoreWrapper adalah meta-wrapper di sekitar kelas DataStoreWrapper. DataStoreWrapper menyediakan fungsionalitas antrean dan pengulangan, yang dilengkapi dengan penguncian sesi oleh SessionLockedDataStoreWrapper.
SessionLockedDataStoreWrapper meneruskan setiap permintaan DataStore—terlepas dari apakah itu GetAsync, SetAsync atau UpdateAsync—melalui UpdateAsync. Ini karena UpdateAsync memungkinkan kunci untuk dibaca dan ditulis secara atomik. Juga dimungkinkan untuk membatalkan penulisan berdasarkan nilai yang dibaca dengan mengembalikan nil dalam callback transformasi.
Fungsi transformasi yang diteruskan ke UpdateAsync untuk setiap permintaan melakukan operasi berikut:
Memverifikasi bahwa kunci aman untuk diakses, membatalkan operasi jika tidak. "Aman untuk diakses" berarti:
Objek metadata kunci tidak menyertakan nilai LockId yang tidak dikenal yang terakhir diperbarui kurang dari waktu kedaluwarsa kunci. Ini memperhitungkan menghormati kunci yang ditempatkan oleh server lain dan untuk mengabaikan kunci tersebut jika sudah kedaluwarsa.
Jika server ini telah menempatkan nilai LockIdnya sendiri di metadata kunci sebelumnya, maka nilai ini masih ada di metadata kunci. Ini memperhitungkan situasi di mana server lain telah mengambil alih kunci server ini (melalui kedaluwarsa atau dengan paksa) dan kemudian melepaskannya. Dengan kata lain, bahkan jika LockId adalah nil, server lain masih bisa mengganti dan menghapus kunci dalam waktu sejak Anda mengunci kunci tersebut.
UpdateAsync melakukan operasi DataStore yang diminta oleh konsumen SessionLockedDataStoreWrapper. Misalnya, GetAsync() diterjemahkan menjadi function(value) return value end.
Bergantung pada parameter yang diteruskan ke permintaan, UpdateAsync mengunci atau membuka kunci kunci:
Jika kunci akan dikunci, UpdateAsync mengatur LockId di metadata kunci menjadi GUID. GUID ini disimpan dalam memori di server sehingga dapat diverifikasi saat berikutnya mengakses kunci. Jika server sudah memiliki kunci pada kunci ini, tidak ada perubahan yang dilakukan. Ini juga menjadwalkan tugas untuk memperingatkan Anda jika Anda tidak mengakses kunci lagi untuk mempertahankan kunci dalam waktu kedaluwarsa kunci.
Jika kunci akan dibuka kuncinya, UpdateAsync menghapus LockId di metadata kunci.
Handler pengulangan kustom diteruskan ke DataStoreWrapper yang mendasarinya sehingga operasi diulang jika dibatalkan pada langkah 1 karena sesi terkunci.
Pesan kesalahan kustom juga dikembalikan kepada konsumen, memungkinkan sistem data pemain untuk melaporkan kesalahan alternatif dalam kasus penguncian sesi kepada klien.
Caveat
Regime penguncian sesi bergantung pada server selalu melepaskan kuncinya pada kunci ketika sudah selesai menggunakannya. Ini harus selalu terjadi melalui instruksi untuk membuka kunci kunci sebagai bagian dari penulisan akhir dalam PlayerRemoving atau BindToClose().
Namun, pembukaan kunci dapat gagal dalam situasi tertentu. Misalnya:
- Server mengalami crash atau DataStoreService tidak dapat dioperasikan untuk semua upaya mengakses kunci.
- Karena kesalahan dalam logika atau bug serupa, instruksi untuk membuka kunci kunci tidak dibuat.
Untuk mempertahankan kunci pada kunci, Anda harus secara teratur mengaksesnya selama kunci tersebut dimuat dalam memori. Ini biasanya dilakukan sebagai bagian dari loop penyimpanan otomatis yang berjalan di latar belakang di sebagian besar sistem data pemain, tetapi sistem ini juga mengekspos metode refreshLockAsync jika Anda perlu melakukannya secara manual.
Jika waktu kedaluwarsa kunci telah terlampaui tanpa kunci diperbarui, maka server mana pun bebas untuk mengambil alih kunci tersebut. Jika server yang berbeda mengambil kunci, upaya oleh server saat ini untuk membaca atau menulis kunci gagal kecuali ia menetapkan kunci baru.
Pemrosesan Produk Pengembang
Singleton: ReceiptHandler
Latar Belakang
Callback ProcessReceipt melakukan pekerjaan penting untuk menentukan kapan menyelesaikan pembelian. ProcessReceipt dipanggil dalam skenario yang sangat spesifik. Untuk jaminan setnya, lihat MarketplaceService.ProcessReceipt.
Meskipun definisi "menangani" pembelian dapat berbeda antara permainan, kami menggunakan kriteria berikut
Pembelian belum pernah ditangani sebelumnya.
Pembelian tercermin dalam sesi saat ini.
Ini memerlukan melakukan operasi berikut sebelum mengembalikan PurchaseGranted:
- Verifikasi bahwa PurchaseId belum dicatat sebagai ditangani.
- Berikan pembelian dalam data pemain yang ada dalam memori.
- Catat PurchaseId sebagai ditangani dalam data pemain yang ada dalam memori.
- Tulis data pemain yang ada dalam memori ke DataStore.
Penguncian sesi menyederhanakan alur ini, karena Anda tidak perlu khawatir tentang skenario berikut:
- Data pemain dalam memori di server saat ini mungkin sudah ketinggalan zaman, memerlukan Anda untuk mengambil nilai terbaru dari DataStore sebelum memverifikasi riwayat PurchaseId
- Callback untuk pembelian yang sama berjalan di server lain, memerlukan Anda untuk membaca dan menulis riwayat PurchaseId dan menyimpan data pemain yang diperbarui dengan pembelian yang tercermin secara atomik untuk mencegah kondisi balapan
Penguncian sesi menjamin bahwa, jika upaya untuk menulis ke DataStore pemain berhasil, tidak ada server lain yang telah berhasil membaca atau menulis ke DataStore pemain antara data yang dimuat dan disimpan di server ini. Singkatnya, data pemain dalam memori di server ini adalah versi terbaru yang tersedia. Ada beberapa caveat, tetapi itu tidak mempengaruhi perilaku ini.
Pendekatan
Komentar dalam ReceiptProcessor menggarisbawahi pendekatan:
Verifikasi bahwa data pemain saat ini dimuat di server ini dan bahwa itu dimuat tanpa kesalahan.
Karena sistem ini menggunakan penguncian sesi, pemeriksaan ini juga memverifikasi bahwa data dalam memori adalah versi terbaru.
Jika data pemain belum dimuat (yang diharapkan ketika seorang pemain bergabung dengan permainan), tunggu hingga data pemain dimuat. Sistem ini juga mendengarkan pemain yang meninggalkan permainan sebelum data mereka dimuat, karena tidak boleh menunggu tanpa batas dan memblokir callback ini dari dipanggil lagi di server ini untuk pembelian ini jika pemain bergabung kembali.
Verifikasi bahwa PurchaseId belum dicatat sebagai diproses dalam data pemain.
Karena penguncian sesi, array PurchaseIds yang dimiliki sistem dalam memori adalah versi terbaru. Jika PurchaseId dicatat sebagai diproses dan tercermin dalam nilai yang telah dimuat atau disimpan ke DataStore, kembalikan PurchaseGranted. Jika dicatat sebagai diproses, tetapi tidak tercermin dalam DataStore, kembalikan NotProcessedYet.
Perbarui Data Pemain secara lokal di server ini untuk "memberikan" pembelian.
ReceiptProcessor mengambil pendekatan callback generik dan menetapkan callback yang berbeda untuk setiap DeveloperProductId.
Perbarui data pemain secara lokal di server ini untuk menyimpan PurchaseId.
Ajukan permintaan untuk menyimpan data dalam memori ke DataStore, mengembalikan PurchaseGranted jika permintaan berhasil. Jika tidak, kembalikan NotProcessedYet.
Jika permintaan penyimpanan ini tidak berhasil, permintaan berikutnya untuk menyimpan data sesi pemain dalam memori masih bisa berhasil. Selama panggilan ProcessReceipt berikutnya, langkah 2 menangani situasi ini dan mengembalikan PurchaseGranted.
Data Pemain
Singletons: PlayerData.Server, PlayerData.Client
Latar Belakang
Modul yang menyediakan antarmuka untuk kode untuk membaca dan menulis data sesi pemain secara sinkron adalah umum dalam permainan Roblox. Bagian ini mencakup PlayerData.Server dan PlayerData.Client.
Pendekatan
PlayerData.Server dan PlayerData.Client menangani hal-hal berikut:
- Memuat data pemain ke dalam memori, termasuk menangani kasus di mana pemuatan gagal
- Menyediakan antarmuka untuk kode server untuk menanyakan dan mengubah data pemain
- Mereplikasi perubahan dalam data pemain ke klien sehingga kode klien dapat mengaksesnya
- Mereplikasi kesalahan pemuatan dan/atau penyimpanan ke klien sehingga dapat menampilkan dialog kesalahan
- Menyimpan data pemain secara berkala, ketika pemain meninggalkan, dan ketika server dimatikan
Memuat data pemain

SessionLockedDataStoreWrapper membuat permintaan getAsync ke data store.
Jika permintaan ini gagal, data default digunakan dan profil ditandai sebagai "errored" untuk memastikan tidak ditulis ke data store nanti.
Opsi alternatif adalah untuk mengeluarkan pemain, tetapi kami merekomendasikan membiarkan pemain bermain dengan data default dan pesan yang jelas tentang apa yang terjadi daripada mengeluarkan mereka dari permainan.
Payload awal dikirim ke PlayerDataClient yang berisi data yang dimuat dan status kesalahan (jika ada).
Semua thread yang ditunggu menggunakan waitForDataLoadAsync untuk pemain dilanjutkan.
Menyediakan antarmuka untuk kode server
- PlayerDataServer adalah singleton yang dapat diminta dan diakses oleh kode server mana pun yang berjalan di lingkungan yang sama.
- Data pemain diorganisir ke dalam kamus kunci dan nilai. Anda dapat memanipulasi nilai ini di server menggunakan metode setValue, getValue, updateValue dan removeValue. Metode ini semua beroperasi secara sinkron tanpa menunggu.
- Metode hasLoaded dan waitForDataLoadAsync tersedia untuk memastikan data telah dimuat sebelum Anda mengaksesnya. Kami merekomendasikan melakukan ini sekali selama layar pemuatan sebelum sistem lain dimulai untuk menghindari harus memeriksa kesalahan pemuatan sebelum setiap interaksi dengan data di klien.
- Metode hasErrored dapat menanyakan apakah pemuatan awal pemain gagal, menyebabkan mereka menggunakan data default. Periksa metode ini sebelum memungkinkan pemain melakukan pembelian, karena pembelian tidak dapat disimpan ke data tanpa pemuatan yang berhasil.
- Sinyal playerDataUpdated dipicu dengan player, key, dan value setiap kali data pemain diubah. Sistem individu dapat berlangganan ini.
Mereplikasi perubahan ke klien
- Setiap perubahan pada data pemain di PlayerDataServer direplikasi ke PlayerDataClient, kecuali kunci tersebut ditandai sebagai pribadi menggunakan setValueAsPrivate
- setValueAsPrivate digunakan untuk menunjukkan kunci yang tidak boleh dikirim ke klien
- PlayerDataClient mencakup metode untuk mendapatkan nilai kunci (get) dan sinyal yang dipicu saat diperbarui (updated). Metode hasLoaded dan sinyal loaded juga disertakan, sehingga klien dapat menunggu data untuk dimuat & direplikasi sebelum memulai sistemnya
- PlayerDataClient adalah singleton yang dapat diminta dan diakses oleh kode klien mana pun yang berjalan di lingkungan yang sama
Mereplikasi kesalahan ke klien
- Status kesalahan yang ditemui saat menyimpan atau memuat data pemain direplikasi ke PlayerDataClient.
- Akses informasi ini dengan metode getLoadError dan getSaveError, bersama dengan sinyal loaded dan saved.
- Ada dua jenis kesalahan: DataStoreError (permintaan DataStoreService gagal) dan SessionLocked (lihat Penguncian Sesi).
- Gunakan peristiwa ini untuk menonaktifkan prompt pembelian klien dan menerapkan dialog peringatan. Gambar ini menunjukkan contoh dialog:

Menyimpan data pemain

Ketika pemain meninggalkan permainan, sistem mengambil langkah-langkah berikut:
- Periksa apakah aman untuk menulis data pemain ke data store. Skenario di mana tidak aman termasuk data pemain gagal dimuat atau masih dalam proses pemuatan.
- Buat permintaan melalui SessionLockedDataStoreWrapper untuk menulis nilai data dalam memori saat ini ke data store dan menghapus kunci sesi setelah selesai.
- Menghapus data pemain (dan variabel lain seperti metadata dan status kesalahan) dari memori server.
Dalam loop berkala, server menulis data setiap pemain ke data store (dengan syarat aman untuk menyimpan). Redundansi yang menyambut ini mengurangi kehilangan dalam kasus crash server dan juga diperlukan untuk mempertahankan kunci sesi.
Contoh ini memulai satu loop bersama setelah AUTO_SAVE_INTERVAL detik (180 secara default) dan kemudian menyimpan setiap pemain yang dimuat secara paralel. Loop tersebut tidak menggeser server atau pemain, sehingga server yang dimulai pada waktu yang sama dapat flush bersama.
Geser penyimpanan pertama setiap pemain dengan durasi acak dalam interval sehingga server langsung tidak semua menulis pada waktu yang sama:
local AUTO_SAVE_INTERVAL = 180local function startAutoSave(player)task.spawn(function()task.wait(math.random() * AUTO_SAVE_INTERVAL)while player.Parent doif canSave(player) thensavePlayerData(player)endtask.wait(AUTO_SAVE_INTERVAL)endend)endKetika permintaan untuk mematikan server diterima, hal berikut terjadi dalam callback BindToClose:
- Permintaan dibuat untuk menyimpan data setiap pemain di server, mengikuti proses yang biasanya dilalui ketika seorang pemain meninggalkan server. Permintaan ini dilakukan secara paralel, karena callback BindToClose hanya memiliki 30 detik untuk diselesaikan.
- Untuk mempercepat penyimpanan, semua permintaan lain dalam antrean setiap kunci dihapus dari DataStoreWrapper yang mendasarinya (lihat Pengulangan).
- Callback tidak mengembalikan hingga semua permintaan selesai.