Latar belakang
Roblox menyediakan serangkaian API untuk berinteraksi dengan penyimpanan data melalui DataStoreService . Kasus penggunaan yang paling umum untuk API ini adalah untuk menyimpan, memuat, dan mengkloning 数据存储服务 pemain. Artinya, data yang terkait dengan kemajuan, pembelian, dan fitur lain dari pemain yang bertahan di antara setiap ses
Sebagian besar pengalaman di Roblox menggunakan API ini untuk menerapkan beberapa bentuk sistem data pemain. implementasi ini berbeda dalam pendekatan mereka, tetapi umumnya mencari untuk menyelesaikan set serupa masalah.
Masalah Umum
Di bawah ini adalah beberapa masalah paling umum yang data pemain mencoba untuk menyelesaikan:
Dalam Akses Memori: DataStoreService permintaan membuat permintaan web yang beroperasi secara asinkron dan dikenakan batas tingkat. Ini cocok untuk pemuatan awal saat dimulai sesi, tetapi bukan untuk operasi baca dan tulis berkecepatan tinggi selama kursus normal gameplay. Kebanyakan data p
- Bacaan awal saat dimulai sesi
- Tulis akhir di akhir sesi
- Periode menulis dalam interval untuk mengurangi skenario di mana tulisan terakhir gagal
- Menulis untuk menjamin data disimpan saat memproses pembelian
Penyimpanan Efisien: Menyimpan semua data sesi seorang pemain dalam satu tabel memungkinkan Anda untuk menyimpan banyak nilai secara atomik dan menangani jumlah data yang sama dalam lebih sedikit permintaan. Ini juga menghapus risiko desinkronisasi antar nilai dan membuat rollback lebih mudah untuk dimengerti.
Beberapa pengembang juga menerapkan serialisasi khusus untuk mengkompresi struktur data besar ( biasanya untuk disimpan dalam game konten pengguna yang dihasilkan).
Replikasi: Klien memerlukan akses teratur ke data pemain (seperti untuk menyetujui UI). Pendekatan umum untuk mengkloning data pemain ke klien memungkinkan Anda untuk mengirimkan informasi ini tanpa harus membuat sistem replikasi khusus untuk setiap komponen data. Pengembang sering ingin pilihan untuk menjadi selektif tentang apa yang di replikasi ke klien.
Penanganan Kesalahan: Ketika DataStores tidak dapat diakses, sebagian besar solusi akan menerapkan mekanisme putus ulang dan cadangan untuk '数据' data. Perhatikan khusus untuk memastikan cadangan data tidak menulis '数据' yang sebenarnya, dan bahwa ini dikomunikasikan kepada pemain dengan benar.
Coba kembali: Ketika data tidak dapat diakses, sebagian besar solusi menerapkan mekanisme coba kembali dan mekanisme cadangan untuk data default. Perhatikan dengan seksama untuk memastikan bahwa data cadangan tidak menghapus data "nyata", dan komunikasikan situasi ke pemain dengan benar.
Pemblokiran Sesi: Jika data pemain tunggal dimuat dan dalam memori di banyak server, masalah dapat muncul di mana salah satu server menyimpan informasi kedaluwarsa. Ini dapat menyebabkan kerusakan data dan masalah duplikasi item umum.
Penanganan Pembelian Atomik: Verifikasi, berikan, dan catat pembelian atomik untuk mencegah item hilang atau diberikan beberapa kali.
Kode Sampel
Roblox memiliki kode referensi untuk membantu Anda dengan mendesain dan membangun sistem data pemain. Sisa dari halaman ini memeriksa latar belakang, rincian implementasi, dan peringatan umum.
Setelah Anda mengimpor model ke Studio, Anda harus melihat struktur folder berikut:
Arsitektur
Diagram tingkat tinggi ini menunjukkan sistem kunci dalam sampel dan bagaimana mereka berinteraksi dengan kode di sisa pengalaman.
Coba lagi
Kelas: DataStoreWraper
Latar belakang
Sebagai DataStoreService membuat permintaan web di bawah kerudung, permintaannya tidak dijamin untuk berhasil. Saat ini, metode DataStore menangkap kesalahan, memungkinkan Anda untuk menangani mereka.
Sebuah "diterima" umum dapat terjadi jika Anda mencoba menangani kesalahan penyimpanan data seperti ini:
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
Sementara ini adalah mekanisme coba kembali yang benar-benar valid untuk fungsi generik, itu tidak cocok dengan permintaan DataStoreService karena tidak menjamin urutan di mana permintaan dibuat. Menyimpan urutan permintaan penting untuk permintaan DataStoreService karena mereka berinteraksi dengan state. Lihat skenario berikut:
- Permintaan A dibuat untuk menetapkan nilai kunci K ke 1.
- Permintaan gagal, jadi pencobaan ulang rencana untuk dijalankan dalam 2 detik.
- Sebelum terjadi ulang, minta B menetapkan nilai K ke 2, tetapi ulang permintaan A segera menulis nilai ini dan menetapkan K ke 1.
Meskipun UpdateAsync beroperasi pada versi terbaru dari nilai unit, UpdateAsync permintaan masih harus diproses untuk menghindari negatif transisi status (misalnya, pembelian menghapus koin sebelum koin tambahan dibrosikan, menyebabkan koin negatif).
Sistem data pemain kami menggunakan kelas baru, DataStoreWrapper, yang memberikan hasil ulang yang dijamin untuk diproses per unit.
Pendekatan
DataStoreWrapper menyediakan metode yang sesuai dengan metode DataStore : DataStore:GetAsync(), 0> Class.GlobalDataStore:SetAsync()|DataStore
Metode-metode ini, saat dipanggil:
Tambahkan permintaan ke antrian. Setiap kunci memiliki antrian sendiri, di mana permintaan diproses dalam urutan dan seri. Thread permintaan menghasilkan sampai permintaan telah selesai.
Funggsi ini didasarkan pada kelas ThreadQueue, yang merupakan pembuat jadwal tugas berdasarkan rutinitas dan batas tingkat. Alih-alih mengembalikan janji, ThreadQueue menghasilkan thread saat ini sampai operasi selesai dan menempatkan kesalahan jika gagal. Ini lebih konsisten dengan pola Lua idiomis.
Jika permintaan gagal, itu mencoba kembali dengan backoff eksponensial yang dapat diatur. Formulir retries ini merupakan bagian dari panggilan yang dikirim ke ThreadQueue, jadi mereka dijamin untuk menyelesaikan sebelum permintaan berikutnya dalam antrian untuk kunci ini dimulai.
Ketika permintaan selesai, metode permintaan kembali dengan pola success, result
DataStoreWrapper juga mengekspos metode untuk mendapatkan panjang antrian untuk kunci tertentu dan menghapus permintaan tertinggal. Pilihan terakhir ini sangat berguna dalam skenario ketika server dihentikan dan tidak ada waktu untuk memproses apa pun kecuali permintaan terbaru.
Kecuali
DataStoreWrapper mengikuti prinsip bahwa, di luar skenario ekstrim, setiap permintaan toko data harus diizinkan untuk selesai (berhasil atau sebaliknya), bahkan jika permintaan yang lebih baru membuatnya redundant. Saat permintaan baru muncul, permintaan tertunda tidak dihapus dari antrian, t
Sulit untuk memutuskan set seri aturan intuitif untuk ketika permintaan aman untuk dihapus dari antrian. Lihat队 berikut:
Value=0, SetAsync(1), GetAsync(), SetAsync(2)
Perilaku yang diharapkan adalah bahwa GetAsync() akan mengembalikan 1 , tetapi jika kita menghapus permintaan SetAsync() dari antrian karena dibuat tidak layan karena yang terbaru, itu akan mengembalikan 1> 0
Progresi logikal adalah bahwa ketika permintaan tulis baru ditambahkan, hanya mengurangi permintaan tulis tertinggi sejauh permintaan tulis terbaru. UpdateAsync() , oleh far sebagian besar operasi (dan satu-satunya yang digunakan oleh sistem ini), dapat keduanya membaca dan menulis, jadi akan sulit untuk mengkons
DataStoreWrapper mungkin mengharuskan Anda untuk menentukan apakah permintaan UpdateAsync() diizinkan untuk dibaca dan/atau ditulis, tetapi itu tidak akan berlaku untuk sistem data pemain kami, di mana ini tidak dapat ditentukan sebelum waktu karena mekanisme pemblokiran sesi (ditangani lebih lanjut nanti).
Setelah dihapus dari antrian, sulit untuk memutuskan atas aturan intuitif untuk bagaimana ini seharusnya ditangani. Saat permintaan DataStoreWrapper dibuat, thread saat ini dihasilkan sampai selesai. Jika kita menghapus permintaan tertinggal dari ant
Pada dasarnya, pandangan kami adalah bahwa pendekatan sederhana (memproses setiap permintaan) lebih disukai di sini dan menciptakan lingkungan yang lebih jelas untuk dinavigasi ketika mendekati masalah kompleks seperti sesi locking. Satu-satunya
Mengunci Sesi
Kelas: SessionLockedDataStoreWrapper >
Latar belakang
Data pemain disimpan dalam memori di server dan hanya dibaca dan ditulis ke dalam penyimpanan data yang mendasar ketika perlu. Anda dapat membaca dan menulis data pemain dalam memori tanpa memerlukan permintaan web dan menghindari melebihi batas DataStoreService.
Untuk model ini berfungsi sebagaimana dimaksud, tidak mungkin lebih dari satu server dapat memuat data pemain ke dalam memori dari DataStore pada saat yang sama.
Misalnya, jika server A memuat data pemain, server B tidak dapat memuat data pemain itu sampai server A melepaskan kunci pada itu selama penyimpanan terakhir. Tanpa mekanisme pemblokir, server B dapat memuat data pemain yang lebih baru dari data store sebelum server A memiliki kesempatan untuk menyimpan versi terbaru yang dimilik
Meskipun Roblox hanya memungkinkan seorang klien untuk terhubung ke satu server pada satu waktu, Anda tidak dapat menyimpulkan bahwa data dari satu sesi selalu disimpan sebelum sesi berikutnya dimulai. Pertimbangkan beberapa skenario berikut yang dapat terjadi ketika seorang pemain meninggalkan server A:
- Server A membuat permintaan DataStore untuk menyimpan data mereka, tetapi permintaan gagal dan memerlukan beberapa upaya kembali untuk menyelesaikan dengan sukses. Selama periode pengecualian, pemain bergabung dengan server B.
- Server A membuat terlalu banyak panggilan UpdateAsync() ke kunci yang sama dan mendapatkan kelambatan. Permintaan penyimpanan terakhir ditempatkan dalam antrian. Saat permintaan berada dalam antrian, pemain bergabung dengan server B.
- Pada server A, beberapa kode terhubung ke acara PlayerRemoving , menghasilkan sebelum data pemain disimpan. Sebelum operasi ini selesai, pemain bergabung dengan server B.
- Kinerja server A telah menurun sehingga penyimpanan terakhir ditunda sampai setelah pemain bergabung dengan server B.
Sebelas skenario ini harus langka, tetapi mereka terjadi, khususnya dalam situasi di mana seorang pemain terputus dari satu server dan terhubung ke server lain dalam waktu cepat (misalnya, saat teleportasi). Beberapa pengguna jahat bahkan mungkin mencoba untuk menyalahgunakan perilaku ini untuk menyelesaikan tindakan tanpa mereka bertahan. Ini dapat sangat mempengaruhi game yang memung
Session locking menangani kelemahan ini dengan menjamin bahwa ketika kunci pemain DataStore dibaca pertama oleh server, server atomik menulis kunci ke metadata dalam panggilan UpdateAsync() yang sama. Jika nilai kunci ini hadir ketika server mencoba untuk membaca atau menulis unit
Pendekatan
SessionLockedDataStoreWrapper adalah meta-wrapper di sekitar DataStoreWrapper kelas. DataStoreWrapper menyediakan fungsi队列和重试, yang 0> SessionLockedDataStore0> tambahkan dengan fungsi pemblokiran sesi.
SessionLockedDataStoreWrapper passes every
Fungsi transformasi dilewati menjadi UpdateAsync untuk setiap permintaan melakukan operasi berikut:
Verifikasi bahwa kunci aman untuk diakses, meninggalkan operasi jika tidak. "Aman untuk diakses" berarti:
Objek metriksi kunci tidak mencakup nilai LockId yang tidak dikenali yang terakhir diperbarui kurang dari waktu kedaluwarsa kunci. Ini mencakup menghormati kunci yang ditempatkan oleh server lain dan menghindari kunci jika kedaluwarsa.
Jika server ini telah menempatkan nilai LockId sendiri di metadata kunci sebelumnya, maka nilai ini masih ada di metadata kunci. Ini mencakup situasi di mana server lain mengambil alih kunci ini (oleh kadaluarsa atau dengan kekuatan) dan kemudian melepaskannya. Secara alternatif, b
Class.GlobalDataStore:UpdateAsync()|UpdateAsync melakukan operasi DataStore yang diminta oleh konsumen SessionLockedDataStoreWrapper. Misalnya, 0> Class.GlobalDataStore:GetAsync()|GetAsync()0> menerjemahkan menjadi UpdateAsync3> .
Tergantung pada parameter yang dikirimkan dalam permintaan, UpdateAsync mengunci atau membuka kunci unit:
Jika kunci dihapus, UpdateAsync menetapkan LockId dalam metadata unitke GUID. GUID ini disimpan dalam memori pada server sehingga dapat diverifikasi lain kali ketika mengakses kunci. Jika server sudah memiliki kunci
Jika kunci itu harus dibuka, UpdateAsync menghapus LockId di metadata unit.
Sebuah penanganan coba kembali khusus diberikan ke dalam DataStoreWrapper yang berada di bawahnya sehingga operasi diulang jika dihentikan pada langkah 1 karena sesi sedang dikunci.
Pesan kesalahan khusus juga dikembalikan ke konsumen, memungkinkan sistem data pemain untuk melaporkan kesalahan alternatif dalam kasus sesi locking ke klien.
Kecuali
Mechanism pemblokan sesi mengandalkan server selalu melepaskan kunci saat ia selesai dengan itu. Ini seharusnya selalu terjadi melalui instruksi untuk menangkap kunci sebagai bagian dari tulisan terakhir di PlayerRemoving atau BindToClose() .
Namun, unlock dapat gagal dalam beberapa situasi. Misalnya:
- Server jatuh atau DataStoreService tidak dapat diakses untuk semua upaya untuk mengakses unit.
- Karena kesalahan dalam logika atau bug serupa, instruksi untuk membuka kunci kunci tidak dibuat.
Untuk menjaga kunci pada unit, Anda harus sering mengaksesnya selama kunci di memori. Ini biasanya dilakukan sebagai bagian dari siklus save otomatis yang berjalan di latar belakang dalam kebanyakan sistem data pemain, tetapi sistem ini juga mengekspos metode refreshLockAsync jika Anda perlu melakukannya secara manual.
Jika waktu kedaluwarsa kunci telah diperpanjang tanpa kunci diperbarui, maka server mana pun adalah gratis untuk mengambil kunci. Jika server yang berbeda mengambil kunci, upaya oleh server saat ini untuk membaca atau menulis kunci gagal kecuali jika itu tidak membangun kunci baru.
Proses Pengembangan Produk
Singleton: ReceiptHandler ”
Latar belakang
Panggilan ProcessReceipt mengeksekusi pekerjaan kritis untuk menentukan kapan menyelesaikan pembelian. ProcessReceipt dikenal dalam skenario yang sangat spesifik. Untuk set jaminannya, lihat MarketplaceService.ProcessReceipt.
Meskipun definisi "penanganan" menangani pembelian dapat bervariasi antara pengalaman, kami menggunakan kriteria berikut
Pembelian ini sebelumnya tidak ditangani.
Pembelian dit反映在当前 sesi.
Ini mengharuskan melakukan operasi berikut sebelum mengembalikan PurchaseGranted :
- Verifikasi bahwa PurchaseId belum dicatat sebagai tangani.
- Hadiah pembelian di data pemain dalam memori.
- Catat PurchaseId sebagai yang diperlakukan dalam data pemain dalam memori pemain.
- Tulis data pemain dalam memori pemain ke DataStore .
Pengunci sesi memudahkan aliran ini, karena Anda tidak perlu lagi khawatir tentang skenario berikut:
- Data pemain dalam memori di server saat ini secara potensial keluar dari date, mengharuskan Anda untuk mengambil nilai terbaru dari DataStore sebelum memverifikasi sejarah PurchaseId
- Panggilan untuk pembelian yang sama yang berjalan di server lain, mengharuskan Anda untuk membaca dan menulis sejarah PurchaseId dan menyimpan data pemain yang diperbarui dengan pembelian yang dibaca secara atomik untuk menghindari kondisi balapan
Jaminan pengunci sesi yang menjamin bahwa, jika upaya menulis ke DataStore pemain berhasil, tidak ada server lain yang berhasil membaca atau menulis ke DataStore antara data yang dimuat dan disimpan di server ini. Secara keseluruhan, data pem
Pendekatan
Komentar dalam ReceiptProcessor menyoroti pendekatan:
Verifikasi bahwa data pemain diunduh di server ini saat ini dan bahwa itu dimuat tanpa kesalahan apa pun.
Karena sistem ini menggunakan kunci sesi, check ini juga memverifikasi bahwa data dalam memori adalah versi terbaru.
Jika data pemain belum dimuat (yang diharapkan ketika seorang pemain bergabung dengan game), tunggu data pemain mereka untuk load. Sistem juga mendengarkan pemain meninggalkan permainan sebelum data mereka dimuat, karena itu tidak boleh menghasilkan secara permanen dan menghalang panggilan kembali pada server ini untuk pembelian ini jika pemain bergabung kembali.
Verifikasi bahwa PurchaseId tidak sudah dicatat sebagai diproses dalam data pemain.
Karena kunci sesi, array dari PurchaseIds yang dimiliki sistem dalam memori adalah versi terbaru. Jika PurchaseId dicatat sebagai diproses
Perbarui Data Pemain lokal di server ini untuk "menghadiahkan" pembelian.
ReceiptProcessor mengambil pendekatan panggilan umum dan menetapkan panggilan pengembalian berbeda untuk setiap DeveloperProductId .
Perbarui data pemain lokal di server ini untuk menyimpan PurchaseId .
Kirim permintaan untuk menyimpan data dalam memori ke DataStore, mengembalikan PurchaseGranted jika permintaan berhasil. Jika tidak, kembalikan NotProcessedYet .
Jika permintaan penyimpanan ini gagal, permintaan penyimpanan nanti 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 permainan untuk dibaca dan ditulis secara sinkron data sesi pemain adalah umum dalam pengalaman Roblox. Bagian ini mencakup PlayerData.Server dan PlayerData.Client .
Pendekatan
PlayerData.Server dan PlayerData.Client menangani mengikuti:
- Memuat data pemain ke dalam memori, termasuk kasus pengelolaan di mana ia gagal load
- Menyediakan antarmuka untuk kode server untuk mengekstraksi dan mengubah data pemain
- Mereplikasi perubahan di data pemain ke klien sehingga kode klien dapat mengaksesnya
- Mereplikasi kesalahan pemuatan dan/atau penyimpanan ke klien sehingga itu dapat menunjukkan dialog kesalahan
- Menyimpan data pemain secara berkala, saat pemain meninggalkan, dan saat server dihentikan
Memuat Data Pemain
SessionLockedDataStoreWrapper menyimpan permintaan getAsync ke penyimpan data.
Jika permintaan ini gagal, data default digunakan dan profil ditandai sebagai "bercacat" untuk memastikan bahwa tidak ditulis ke toko data nanti.
Pilihan alternatif adalah menendang pemain, tetapi kami rekomendasikan membiarkan pemain bermain dengan data default dan menghapus pesan yang tidak diinginkan sehingga terjadi daripada menghapus mereka dari pengalaman.
Sebuah pemuatan awal dikirim ke PlayerDataClient mengandung data yang dimuat dan status kesalahan (jika ada).
Setiap thread dihasilkan menggunakan waitForDataLoadAsync untuk pemain diambil kembali.
Menyediakan Antarmuka untuk Kode Server
- PlayerDataServer adalah singleton yang dapat diperlukan dan diakses oleh kode server mana pun yang berjalan di lingkungan yang sama.
- Data pemain diatur menjadi kumpulan kunci dan nilai. Anda dapat menyegarkan nilai ini di server menggunakan metode setValue, getValue, updateValue dan 1>RemoveValue1>. Semua metode ini beroperasi secara sinkron tanpa menghasilkan.
- Metode hasLoaded dan waitForDataLoadAsync tersedia untuk menjamin bahwa 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 mengambil jika pemain's awal load gagal, menyebabkan mereka menggunakan data default. Periksa metode ini sebelum mengizinkan pemain untuk melakukan pembelian, karena pembelian tidak dapat disimpan ke data tanpa pemuatan yang berhasil.
- Sinyal playerDataUpdated dengan player , key , dan 1> value1> setiap kali data pemain diubah. Sistem individual dapat berlangganan ke ini.
Mereplikasi Perubahan ke Klien
- Setiap perubahan data pemain di PlayerDataServer dikirim ke PlayerDataClient, kecuali jika kunci itu ditandai sebagai pribadi menggunakan setValueAsPrivate
- setValueAsPrivate digunakan untuk menyatakan kunci yang seharusnya tidak dikirim ke klien
- PlayerDataClient termasuk metode untuk mendapatkan nilai kunci (dapatkan) dan sinyal yang diaktifkan saat diperbarui (diperbarui). A hasLoaded metode dan sinyal loaded juga termasuk, sehingga klien dapat menunggu sampai data dimuat & bereplikasi sebelum memulai sistemnya
- PlayerDataClient adalah singleton yang dapat diperlukan dan diakses oleh kode klien mana pun yang berjalan di lingkungan yang sama
Mengkloning Kesalahan ke Klient
- Status kesalahan yang ditemukan saat menyimpan atau memuat data pemain ditiru ke PlayerDataClient .
- Akses informasi ini dengan metode getLoadError dan getSaveError bersama dengan sinyal loaded dan 1>saved1>.
- Ada dua jenis kesalahan: DataStoreError (permintaan DataStoreService gagal) dan SessionLocked (lihat 1> Pemblokiran Sesi1>).
- Gunakan acara ini untuk menonaktifkan prompt pembelian klien dan menerapkan dialog peringatan. Gambar ini menunjukkan contoh dialog:
Menyimpan Data Pemain
Ketika pemain meninggalkan game, sistem mengambil langkah-langkah berikut:
- Periksa apakah aman untuk menulis data pemain ke toko data. Szenario di mana itu 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 sesi kunci setelah selesai.
- Menyimpan data pemain (dan variabel lain seperti metadata dan status kesalahan) dari memori server.
Pada loop periodik, server menulis data setiap pemain ke store data (catatan bahwa itu aman untuk disimpan). Ini menyambung ketidakamanan penyambungan ketika server crash dan juga perlu untuk mempertahankan sesi kunci.
Ketika permintaan untuk menutup server diterima, berikut adalah yang terjadi dalam panggilan kembali BindToClose :
- Permintaan dibuat untuk menyimpan data setiap pemain di server, mengikuti proses biasanya yang dilalui ketika seorang pemain meninggalkan server. Panggilan ini dibuat secara paralel, karena BindToClose kembali panggilan hanya memiliki waktu 30 detik untuk menyelesaikan.
- Untuk mengurangi waktu pengiriman, semua permintaan lain dalam unitmasing-masing kunci dihapus dari DataStoreWrapper (lihat Retries).
- Panggilan tidak akan dikembalikan sampai semua permintaan telah selesai.