Triển khai hệ thống dữ liệu và mua sắm của người chơi

*Nội dung này được dịch bằng AI (Beta) và có thể có lỗi. Để xem trang này bằng tiếng Anh, hãy nhấp vào đây.

Bối cảnh

Roblox cung cấp một bộ API để tương tác với kho dữ liệu thông qua DataStoreService. Trường hợp sử dụng phổ biến nhất cho những API này là để lưu, tải và tái tạo dữ liệu của người chơi. Đó là, dữ liệu liên quan đến tiến trình, các giao dịch mua, và các đặc điểm khác trong phiên chơi của người chơi mà vẫn giữ nguyên giữa các phiên chơi khác nhau.

Hầu hết các trò chơi trên Roblox sử dụng những API này để triển khai một dạng hệ thống dữ liệu của người chơi. Các triển khai này khác nhau trong cách tiếp cận của chúng, nhưng nói chung tìm cách giải quyết cùng một tập hợp vấn đề.

Vấn đề phổ biến

Dưới đây là một số vấn đề phổ biến mà các hệ thống dữ liệu của người chơi cố gắng giải quyết:

  • Truy cập trong bộ nhớ: Các yêu cầu DataStoreService thực hiện các yêu cầu web hoạt động bất đồng bộ và chịu giới hạn tốc độ. Điều này phù hợp cho một lần tải ban đầu tại đầu phiên, nhưng không phải cho các thao tác đọc và ghi tần suất cao trong quá trình chơi game bình thường. Hầu hết hệ thống dữ liệu của người chơi của các nhà phát triển lưu trữ dữ liệu này trong bộ nhớ trên máy chủ Roblox, giới hạn các yêu cầu DataStoreService đến các tình huống sau:

    • Đọc ban đầu tại đầu phiên
    • Ghi cuối cùng tại cuối phiên
    • Ghi định kỳ tại một khoảng thời gian để giảm thiểu tình huống mà ghi cuối cùng thất bại
    • Ghi để đảm bảo dữ liệu được lưu trong khi xử lý một giao dịch mua
  • Lưu trữ hiệu quả: Lưu trữ tất cả dữ liệu phiên của người chơi trong một bảng duy nhất cho phép bạn cập nhật nhiều giá trị một cách nguyên tử và xử lý cùng một lượng dữ liệu với ít yêu cầu hơn. Nó cũng loại bỏ rủi ro mất đồng bộ giữa các giá trị và làm cho việc phục hồi trở nên dễ dàng hơn.

    Một số nhà phát triển cũng thực hiện nối dữ liệu tùy chỉnh để nén các cấu trúc dữ liệu lớn (thường để lưu nội dung do người dùng tạo trong trò chơi).

  • Tái tạo: Khách hàng cần truy cập thường xuyên vào dữ liệu của người chơi (ví dụ, để cập nhật giao diện người dùng). Một cách tiếp cận tổng quát để tái tạo dữ liệu của người chơi cho khách hàng cho phép bạn truyền tải thông tin này mà không cần tạo ra các hệ thống tái tạo độc quyền cho từng thành phần dữ liệu. Các nhà phát triển thường muốn có tùy chọn chọn lọc giữa những gì được tái tạo và không được tái tạo cho khách hàng.

  • Xử lý lỗi: Khi các DataStores không thể truy cập được, hầu hết các giải pháp sẽ triển khai một cơ chế thử lại và tham chiếu đến dữ liệu 'mặc định'. Cần đặc biệt chú ý để đảm bảo rằng dữ liệu phục hồi không ghi đè lên dữ liệu 'thực tế', và điều này được thông báo cho người chơi một cách thích hợp.

  • Thử lại: Khi các kho dữ liệu không thể truy cập, hầu hết các giải pháp triển khai một cơ chế thử lại và tham chiếu đến dữ liệu mặc định. Cần chú ý đặc biệt để đảm bảo rằng dữ liệu phục hồi không ghi đè lên "dữ liệu thực" và thông báo tình huống này cho người chơi một cách thích hợp.

  • Khóa phiên: Nếu dữ liệu của một người chơi đơn được tải và lưu trong bộ nhớ trên nhiều máy chủ, có thể xảy ra sự cố mà một máy chủ lưu thông tin không cập nhật. Điều này có thể dẫn đến mất dữ liệu và các kẽ hở về nhân đôi vật phẩm phổ biến.

  • Xử lý mua sắm nguyên tử: Xác minh, trao thưởng và ghi lại các giao dịch mua một cách nguyên tử để tránh bị mất hoặc được trao thưởng nhiều lần.

Mã mẫu

Roblox có mã tham khảo để hỗ trợ bạn trong việc thiết kế và xây dựng các hệ thống dữ liệu của người chơi. Phần còn lại của trang này xem xét bối cảnh, chi tiết triển khai và những lưu ý chung.


Sau khi bạn nhập mô hình vào Studio, bạn sẽ thấy cấu trúc thư mục sau:

Cửa sổ Explorer hiển thị mô hình hệ thống mua sắm.

Kiến trúc

Sơ đồ cấp cao này minh họa các hệ thống chính trong mẫu và cách chúng tương tác với mã trong phần còn lại của trò chơi.

Một sơ đồ kiến trúc cho mã mẫu.

Thử lại

Class: DataStoreWrapper

Bối cảnh

Khi DataStoreService thực hiện các yêu cầu web, các yêu cầu của nó không đảm bảo sẽ thành công. Khi điều này xảy ra, các phương thức DataStore sẽ ném ra lỗi, cho phép bạn xử lý chúng.

Một "tình huống khó khăn" phổ biến có thể xảy ra nếu bạn cố gắng xử lý các lỗi của kho dữ liệu như sau:


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

Trong khi đây là một cơ chế thử lại hợp lệ cho một hàm tổng quát, nhưng nó không phù hợp cho các yêu cầu DataStoreService vì nó không đảm bảo thứ tự mà các yêu cầu được thực hiện. Việc duy trì thứ tự của các yêu cầu là quan trọng đối với các yêu cầu DataStoreService vì chúng tương tác với trạng thái. Xem xét tình huống sau:

  1. Yêu cầu A được thực hiện để đặt giá trị của khóa K thành 1.
  2. Yêu cầu không thành công, vì vậy một thử lại được lên lịch để chạy trong 2 giây.
  3. Trước khi thử lại xảy ra, yêu cầu B đặt giá trị của K thành 2, nhưng việc thử lại yêu cầu A ngay lập tức ghi đè giá trị này và đặt K thành 1.

Mặc dù UpdateAsync hoạt động trên phiên bản mới nhất của giá trị khóa, nhưng các yêu cầu UpdateAsync vẫn phải được xử lý theo thứ tự để tránh các trạng thái tạm thời không hợp lệ (ví dụ, một giao dịch mua trừ xu trước khi một phép cộng xu được xử lý, dẫn đến xu âm).

Hệ thống dữ liệu của người chơi của chúng tôi sử dụng một lớp mới, DataStoreWrapper, cung cấp các thử lại dưới dạng cho phép được đảm bảo sẽ được xử lý theo thứ tự cho mỗi khóa.

Cách tiếp cận

Một sơ đồ quy trình minh họa hệ thống thử lại

DataStoreWrapper cung cấp các phương thức tương ứng với các phương thức DataStore: DataStore:GetAsync(), DataStore:SetAsync(), DataStore:UpdateAsync()DataStore:RemoveAsync().

Khi các phương thức này được gọi:

  1. Thêm yêu cầu vào hàng đợi. Mỗi khóa có hàng đợi riêng, nơi các yêu cầu được xử lý theo thứ tự và theo chuỗi. Luồng yêu cầu chờ cho đến khi yêu cầu hoàn thành.

    Chức năng này dựa trên lớp ThreadQueue, là một bộ lập lịch tác vụ dựa trên coroutine và giới hạn tốc độ. Thay vì trả về một lời hứa, ThreadQueue sẽ tạm dừng luồng hiện tại cho đến khi hoạt động hoàn thành và ném ra lỗi nếu nó thất bại. Điều này nhất quán hơn với các mẫu bất đồng bộ idiomatic của Luau.

  2. Nếu một yêu cầu thất bại, nó sẽ thử lại với một cách tiếp cận làm gián đoạn theo kiểu hồi quy. Những lần thử lại này là một phần của callback được gửi đến ThreadQueue, vì vậy chúng được đảm bảo sẽ hoàn thành trước khi yêu cầu tiếp theo trong hàng đợi cho khóa này bắt đầu.

  3. Khi một yêu cầu hoàn thành, phương thức yêu cầu trả về với mẫu success, result.

DataStoreWrapper cũng phơi bày các phương thức để lấy chiều dài hàng đợi cho một khóa đã cho và xóa các yêu cầu cũ. Tùy chọn sau đặc biệt hữu ích trong các tình huống khi máy chủ đang tắt và không còn thời gian để xử lý các yêu cầu ngoài yêu cầu gần nhất.

Lưu ý

DataStoreWrapper tuân theo nguyên tắc rằng, ngoài các tình huống cực đoan, mỗi yêu cầu kho dữ liệu đều nên được cho phép hoàn thành (thành công hoặc không), ngay cả khi một yêu cầu gần đây làm cho nó trở nên dư thừa. Khi một yêu cầu mới xảy ra, các yêu cầu cũ không bị xóa khỏi hàng đợi mà thay vào đó cho phép chúng hoàn thành trước khi yêu cầu mới được bắt đầu. Lý do cho điều này bắt nguồn từ khả năng áp dụng của module này như một tiện ích kho dữ liệu tổng quát hơn là một công cụ cụ thể cho dữ liệu người chơi, và như sau:

  1. Rất khó để quyết định một bộ quy tắc trực quan cho khi nào một yêu cầu an toàn để xóa khỏi hàng đợi. Xem xét hàng đợi sau:

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

    Hành vi mong muốn là GetAsync() sẽ trả về 1, nhưng nếu chúng ta xóa yêu cầu SetAsync() khỏi hàng đợi do nó trở nên dư thừa bởi yêu cầu gần nhất, nó sẽ trả về 0.

    Tiến trình logic là khi một yêu cầu ghi mới được thêm vào, chỉ xóa các yêu cầu cũ xa nhất cho đến yêu cầu đọc gần nhất. UpdateAsync(), là hoạt động phổ biến nhất (và là hoạt động duy nhất được sử dụng bởi hệ thống này), có thể đọc và ghi, vì vậy sẽ rất khó để hóa giải trong thiết kế này mà không thêm phức tạp bổ sung.

    DataStoreWrapper có thể yêu cầu bạn chỉ định xem liệu yêu cầu UpdateAsync() có được phép đọc và/hoặc ghi hay không, nhưng điều này sẽ không áp dụng cho hệ thống dữ liệu người chơi của chúng tôi, nơi điều này không thể được xác định trước do cơ chế khóa phiên (sẽ được đề cập chi tiết sau).

  2. Sau khi được xóa khỏi hàng đợi, sẽ rất khó để quyết định một quy tắc trực quan về cách điều này nên được xử lý. Khi một yêu cầu DataStoreWrapper được thực hiện, luồng hiện tại sẽ bị tạm dừng cho đến khi hoàn thành. Nếu chúng ta xóa các yêu cầu cũ khỏi hàng đợi, chúng ta sẽ phải quyết định liệu có trả về false, "Đã xóa khỏi hàng đợi" hay không, hoặc không bao giờ trả về và loại bỏ luồng đang hoạt động. Cả hai cách tiếp cận đều có những nhược điểm riêng và tạo thêm độ phức tạp cho người tiêu dùng.

Cuối cùng, quan điểm của chúng tôi là phương pháp đơn giản (xử lý mọi yêu cầu) là ưu tiên hơn ở đây và tạo ra một môi trường rõ ràng hơn để điều hướng khi tiếp cận các vấn đề phức tạp như khóa phiên. Điều duy nhất ngoại lệ cho điều này là trong DataModel:BindToClose(), nơi việc xóa hàng đợi trở nên cần thiết để lưu dữ liệu của tất cả người dùng kịp thời và giá trị mà các cuộc gọi chức năng cá nhân trả về không còn là mối quan tâm đang diễn ra. Để tính đến điều này, chúng tôi phơi bày một phương thức skipAllQueuesToLastEnqueued. Để biết thêm ngữ cảnh, hãy xem Dữ liệu người chơi.

Khóa phiên

Class: SessionLockedDataStoreWrapper

Bối cảnh

Dữ liệu của người chơi được lưu trong bộ nhớ trên máy chủ và chỉ được đọc và ghi vào các kho dữ liệu cơ bản khi cần thiết. Bạn có thể đọc và cập nhật dữ liệu của người chơi trong bộ nhớ ngay lập tức mà không cần yêu cầu web và tránh vượt quá giới hạn của DataStoreService.

Để mô hình này hoạt động như mong muốn, điều cốt yếu là không có hơn một máy chủ nào có thể tải dữ liệu của một người chơi vào bộ nhớ từ DataStore cùng một lúc.

Ví dụ, nếu máy chủ A tải dữ liệu của một người chơi, máy chủ B không thể tải dữ liệu đó cho đến khi máy chủ A giải phóng khóa của nó trong một lần lưu cuối. Nếu không có cơ chế khóa, máy chủ B có thể tải dữ liệu của người chơi không cập nhật từ kho dữ liệu trước khi máy chủ A có cơ hội lưu phiên bản mới hơn mà nó có trong bộ nhớ. Sau đó, nếu máy chủ A lưu dữ liệu mới hơn của nó sau khi máy chủ B đã tải dữ liệu không cập nhật, máy chủ B sẽ ghi đè dữ liệu mới hơn đó trong lần lưu tiếp theo của nó.

Mặc dù Roblox chỉ cho phép một khách hàng kết nối với một máy chủ tại một thời điểm, bạn không thể giả định rằng dữ liệu từ một phiên luôn được lưu trước khi phiên tiếp theo bắt đầu. Xem xét các tình huống sau có thể xảy ra khi một người chơi rời khỏi máy chủ A:

  1. Máy chủ A thực hiện một yêu cầu DataStore để lưu dữ liệu của họ, nhưng yêu cầu đó không thành công và cần nhiều lần thử lại để hoàn thành thành công. Trong suốt thời gian thử lại, người chơi tham gia vào máy chủ B.
  2. Máy chủ A thực hiện quá nhiều cuộc gọi UpdateAsync() đến cùng một khóa và bị hạn chế. Yêu cầu lưu cuối cùng được đặt vào một hàng đợi. Trong khi yêu cầu đang ở trong hàng đợi, người chơi tham gia vào máy chủ B.
  3. Trên máy chủ A, một số mã liên kết với sự kiện PlayerRemoving tạm dừng trước khi dữ liệu của người chơi được lưu. Trước khi hoạt động này hoàn thành, người chơi tham gia vào máy chủ B.
  4. Hiệu suất của máy chủ A đã suy giảm đến mức mà việc lưu cuối cùng bị trì hoãn cho đến khi người chơi tham gia vào máy chủ B.

Các tình huống này nên hiếm, nhưng chúng xảy ra, đặc biệt trong các tình huống khi một người chơi ngắt kết nối từ một máy chủ và kết nối với máy chủ khác một cách nhanh chóng (ví dụ, trong khi chuyển tiếp). Một số người dùng xấu có thể thậm chí cố gắng lợi dụng hành vi này để thực hiện các hành động mà không cần tiết kiệm. Điều này có thể có tác động đáng kể trong các trò chơi cho phép người chơi giao dịch và là một nguồn phổ biến của các khai thác nhân đôi vật phẩm.

Khóa phiên giải quyết lỗ hổng này bằng cách đảm bảo rằng khi khóa của DataStore của một người chơi được đọc lần đầu tiên bởi máy chủ, máy chủ ghi nguyên tử một khóa vào siêu dữ liệu của khóa trong cùng một cuộc gọi UpdateAsync(). Nếu giá trị khóa này có mặt khi bất kỳ máy chủ khác nào cố gắng đọc hoặc ghi khóa, máy chủ đó sẽ không tiếp tục.

Cách tiếp cận

Một sơ đồ quy trình minh họa hệ thống khóa phiên

SessionLockedDataStoreWrapper là một lớp bọc mét quanh lớp DataStoreWrapper. DataStoreWrapper cung cấp chức năng hàng đợi và thử lại, mà SessionLockedDataStoreWrapper bổ sung với khóa phiên.

SessionLockedDataStoreWrapper truyền mọi yêu cầu DataStore—dù là GetAsync, SetAsync hay UpdateAsync—thông qua UpdateAsync. Điều này là do UpdateAsync cho phép một khóa được đọc và ghi một cách nguyên tử. Nó cũng có thể từ bỏ việc ghi dựa trên giá trị được đọc bằng cách trả về nil trong callback biến đổi.

Hàm biến đổi được truyền vào UpdateAsync cho mỗi yêu cầu thực hiện các thao tác sau:

  1. Xác minh khóa là an toàn để truy cập, từ bỏ thao tác nếu nó không. "An toàn để truy cập" có nghĩa là:

    • Đối tượng siêu dữ liệu khóa không bao gồm giá trị LockId không xác định mà đã được cập nhật cuối cùng chưa đến thời gian hết hạn khóa. Điều này liên quan đến việc tôn trọng một khóa được đặt bởi máy chủ khác và bỏ qua khóa đó nếu nó đã hết hạn.

    • Nếu máy chủ này đã đặt giá trị LockId của riêng nó trong siêu dữ liệu của khóa trước đó, thì giá trị này vẫn nằm trong siêu dữ liệu của khóa. Điều này liên quan đến tình huống mà một máy chủ khác đã chiếm lấy khóa của máy chủ này (bằng cách hết hạn hoặc bằng sức mạnh) và sau đó giải phóng nó. Nói cách khác, ngay cả khi LockIdnil, một máy chủ khác vẫn có thể thay thế và loại bỏ một khóa trong thời gian từ khi bạn khóa khóa.

  2. UpdateAsync thực hiện hoạt động DataStore mà người tiêu dùng của SessionLockedDataStoreWrapper yêu cầu. Ví dụ, GetAsync() sẽ chuyển thành function(value) return value end.

  3. Tùy thuộc vào các tham số được truyền vào yêu cầu, UpdateAsync sẽ khóa hoặc mở khóa khóa:

    1. Nếu khóa cần được khóa, UpdateAsync đặt LockId trong siêu dữ liệu của khóa thành một GUID. GUID này được lưu trong bộ nhớ trên máy chủ để có thể xác minh vào lần truy cập tiếp theo khóa. Nếu máy chủ đã có một khóa trên khóa này, nó không thực hiện bất kỳ thay đổi nào. Nó cũng lên lịch một tác vụ để cảnh báo bạn nếu bạn không truy cập lại vào khóa để duy trì khóa trong thời gian hết hạn của khóa.

    2. Nếu khóa cần được mở khóa, UpdateAsync xóa LockId trong siêu dữ liệu của khóa.

Một trình xử lý thử lại tùy chỉnh được truyền vào DataStoreWrapper cơ bản để thao tác này được thử lại nếu nó bị hủy tại bước 1 do phiên được khóa.

Một thông báo lỗi tùy chỉnh cũng được trả lại cho người tiêu dùng, cho phép hệ thống dữ liệu người chơi báo cáo một lỗi thay thế trong trường hợp khóa phiên đến khách hàng.

Lưu ý

Chế độ khóa phiên phụ thuộc vào một máy chủ luôn giải phóng khóa của nó trên một khóa khi nó đã hoàn tất việc sử dụng. Điều này nên luôn xảy ra thông qua một lệnh để mở khóa khóa như một phần của việc ghi cuối cùng trong PlayerRemoving hoặc BindToClose().

Tuy nhiên, việc mở khóa có thể thất bại trong một số tình huống nhất định. Ví dụ:

  • Máy chủ bị sập hoặc DataStoreService không thể hoạt động cho tất cả các lần cố gắng truy cập vào khóa.
  • Do một lỗi trong logic hoặc lỗi tương tự, lệnh mở khóa khóa không được thực hiện.

Để duy trì khóa trên một khóa, bạn phải thường xuyên truy cập vào nó trong suốt thời gian nó được tải trong bộ nhớ. Điều này thường được thực hiện như một phần của vòng lặp tự động lưu chạy trong nền trong hầu hết các hệ thống dữ liệu của người chơi, nhưng hệ thống này cũng phơi bày một phương thức refreshLockAsync nếu bạn cần thực hiện thủ công.

Nếu thời gian hết hạn của khóa đã bị vượt quá mà khóa không được cập nhật, thì mọi máy chủ đều có quyền chiếm lấy khóa. Nếu một máy chủ khác chiếm lấy khóa, các nỗ lực của máy chủ hiện tại để đọc hoặc ghi khóa sẽ thất bại trừ khi nó thiết lập một khóa mới.

Xử lý sản phẩm của nhà phát triển

Singleton: ReceiptHandler

Bối cảnh

Callback ProcessReceipt thực hiện công việc quan trọng là xác định khi nào hoàn tất một giao dịch mua. ProcessReceipt được gọi trong các tình huống rất cụ thể. Để biết các đảm bảo của nó, xem MarketplaceService.ProcessReceipt.

Mặc dù định nghĩa về "xử lý" một giao dịch mua có thể khác nhau giữa các trò chơi, nhưng chúng tôi sử dụng các tiêu chí sau

  1. Giao dịch mua chưa được xử lý trước đó.

  2. Giao dịch mua được phản ánh trong phiên hiện tại.

  3. Giao dịch mua đã được lưu vào DataStore.

    Mỗi giao dịch mua, ngay cả các vật phẩm tiêu thụ một lần, đều nên được phản ánh trong DataStore để lịch sử giao dịch của người dùng được bao gồm cùng với dữ liệu phiên của họ.

Điều này yêu cầu thực hiện các thao tác sau trước khi trả về PurchaseGranted:

  1. Xác minh rằng PurchaseId chưa được ghi nhận là đã xử lý.
  2. Trao phần thưởng cho giao dịch mua trong dữ liệu của người chơi được lưu trong bộ nhớ.
  3. Ghi nhận PurchaseId như đã xử lý trong dữ liệu của người chơi được lưu trong bộ nhớ.
  4. Ghi dữ liệu của người chơi được lưu trong bộ nhớ vào DataStore.

Khóa phiên đơn giản hóa quy trình này, vì bạn không còn cần quan tâm đến các tình huống sau:

  • Dữ liệu người chơi trong bộ nhớ trên máy chủ hiện tại có thể đã không cập nhật, yêu cầu bạn tải giá trị mới nhất từ DataStore trước khi xác minh lịch sử PurchaseId
  • Callback cho cùng một giao dịch mua chạy trên máy chủ khác, yêu cầu bạn phải đọc và ghi lịch sử PurchaseId và lưu dữ liệu người chơi đã cập nhật với giao dịch mua được phản ánh một cách nguyên tử để ngăn ngừa điều kiện cạnh tranh

Khóa phiên đảm bảo rằng, nếu việc ghi dữ liệu vào DataStore của người chơi thành công, không có máy chủ khác nào đã đọc hoặc ghi thành công dữ liệu vào DataStore của người chơi giữa việc dữ liệu được nạp và lưu trong máy chủ này. Nói ngắn gọn, dữ liệu người chơi trong bộ nhớ trên máy chủ này là phiên bản mới nhất có sẵn. Có một số lưu ý, nhưng chúng không ảnh hưởng đến hành vi này.

Cách tiếp cận

Các chú thích trong ReceiptProcessor giải thích cách tiếp cận:

  1. Xác minh rằng dữ liệu của người chơi hiện đang được tải trên máy chủ này và nó đã được tải mà không có lỗi nào.

    Bởi vì hệ thống này sử dụng khóa phiên, việc kiểm tra này cũng xác minh rằng dữ liệu trong bộ nhớ là phiên bản mới nhất.

    Nếu dữ liệu của người chơi chưa được tải (điều này là mong đợi khi người chơi tham gia trò chơi), hãy chờ cho dữ liệu của người chơi tải thành công. Hệ thống cũng lắng nghe sự kiện người chơi rời khỏi trò chơi trước khi dữ liệu của họ được tải, vì nó không nên tạm dừng vô thời hạn và chặn callback này không được gọi lại trên máy chủ này cho giao dịch mua này nếu người chơi tham gia lại.

  2. Xác minh rằng PurchaseId không được ghi nhận là đã xử lý trong dữ liệu của người chơi.

    Do khóa phiên, mảng PurchaseIds mà hệ thống có trong bộ nhớ là phiên bản mới nhất. Nếu PurchaseId được ghi nhận là đã xử lý và phản ánh trong một giá trị đã được tải hoặc lưu vào DataStore, hãy trả về PurchaseGranted. Nếu nó được ghi nhận là đã xử lý, nhưng không được phản ánh trong DataStore, hãy trả về NotProcessedYet.

  3. Cập nhật Dữ liệu Người chơi địa phương trên máy chủ này để "trao phần thưởng" cho giao dịch mua.

    ReceiptProcessor có một cách tiếp cận callback tổng quát và chỉ định một callback khác cho mỗi DeveloperProductId.

  4. Cập nhật dữ liệu người chơi địa phương trên máy chủ này để lưu PurchaseId.

  5. Gửi yêu cầu để lưu dữ liệu được lưu trong bộ nhớ vào DataStore, trả về PurchaseGranted nếu yêu cầu thành công. Nếu không, hãy trả về NotProcessedYet.

    Nếu yêu cầu lưu này không thành công, một yêu cầu sau đó để lưu dữ liệu phiên của người chơi trong bộ nhớ vẫn có thể thành công. Trong cuộc gọi ProcessReceipt tiếp theo, bước 2 xử lý tình huống này và trả về PurchaseGranted.

Dữ liệu người chơi

Singletons: PlayerData.Server, PlayerData.Client

Bối cảnh

Các module cung cấp giao diện cho mã để đọc và ghi một cách đồng bộ dữ liệu phiên của người chơi là phổ biến trong các trò chơi Roblox. Phần này đề cập đến PlayerData.ServerPlayerData.Client.

Cách tiếp cận

PlayerData.ServerPlayerData.Client xử lý các công việc sau:

  1. Tải dữ liệu của người chơi vào bộ nhớ, bao gồm việc xử lý các trường hợp mà nó không thể tải thành công
  2. Cung cấp một giao diện để mã máy chủ truy vấn và thay đổi dữ liệu người chơi
  3. Tái tạo các thay đổi trong dữ liệu của người chơi tới khách hàng để mã client có thể truy cập nó
  4. Tái tạo các lỗi tải và/hoặc lưu tới khách hàng để họ có thể hiển thị các hộp thoại lỗi
  5. Lưu dữ liệu của người chơi định kỳ, khi người chơi rời đi, và khi máy chủ tắt

Tải dữ liệu người chơi

Một sơ đồ quy trình minh họa hệ thống tải
  1. SessionLockedDataStoreWrapper thực hiện yêu cầu getAsync tới kho dữ liệu.

    Nếu yêu cầu này không thành công, dữ liệu mặc định sẽ được sử dụng và hồ sơ được đánh dấu là "có lỗi" để đảm bảo rằng nó sẽ không được ghi vào kho dữ liệu sau này.

    Một tùy chọn thay thế là đá người chơi ra, nhưng chúng tôi khuyên bạn nên cho phép người chơi chơi với dữ liệu mặc định và có thông điệp rõ ràng về những gì đã xảy ra thay vì loại bỏ họ khỏi trò chơi.

  2. Một tải ban đầu được gửi đến PlayerDataClient chứa dữ liệu đã tải và trạng thái lỗi (nếu có).

  3. Tất cả các luồng đã tạm dừng sử dụng waitForDataLoadAsync cho người chơi sẽ được tiếp tục.

Cung cấp một giao diện cho mã máy chủ

  • PlayerDataServer là một singleton có thể được yêu cầu và truy cập bởi bất kỳ mã máy chủ nào đang chạy trong cùng một môi trường.
  • Dữ liệu người chơi được tổ chức thành một từ điển các khóa và giá trị. Bạn có thể thao tác các giá trị này trên máy chủ bằng cách sử dụng các phương thức setValue, getValue, updateValueremoveValue. Tất cả các phương thức này hoạt động đồng bộ mà không cần tạm dừng.
  • Các phương thức hasLoadedwaitForDataLoadAsync có sẵn để đảm bảo dữ liệu đã được tải trước khi bạn truy cập vào nó. Chúng tôi khuyên bạn thực hiện điều này một lần trong quá trình tải trước khi các hệ thống khác bắt đầu để tránh phải kiểm tra lỗi tải trước mỗi tương tác với dữ liệu trên khách hàng.
  • Một phương thức hasErrored có thể truy vấn nếu việc tải ban đầu của người chơi đã thất bại, khiến họ sử dụng dữ liệu mặc định. Kiểm tra phương thức này trước khi cho phép người chơi thực hiện bất kỳ giao dịch mua nào, vì các giao dịch mua không thể được lưu vào dữ liệu mà không có một lần tải thành công.
  • Một tín hiệu playerDataUpdated sẽ được kích hoạt với player, key, và value mỗi khi dữ liệu của một người chơi được thay đổi. Các hệ thống riêng lẻ có thể đăng ký tín hiệu này.

Tái tạo các thay đổi tới khách hàng

  • Mọi thay đổi đối với dữ liệu người chơi trong PlayerDataServer sẽ được tái tạo tới PlayerDataClient, trừ khi khóa đó được đánh dấu là riêng tư bằng cách sử dụng setValueAsPrivate.
    • setValueAsPrivate được sử dụng để chỉ định những khóa không nên được gửi tới khách hàng.
  • PlayerDataClient bao gồm một phương thức để nhận giá trị của một khóa (get) và một tín hiệu sẽ được kích hoạt khi nó được cập nhật (updated). Một phương thức hasLoaded và một tín hiệu loaded cũng được bao gồm, vì vậy khách hàng có thể chờ dữ liệu tải và tái tạo trước khi bắt đầu các hệ thống của nó.
  • PlayerDataClient là một singleton có thể được yêu cầu và truy cập bởi bất kỳ mã khách nào đang chạy trong cùng một môi trường.

Tái tạo lỗi tới khách hàng

  • Các trạng thái lỗi gặp phải khi lưu hoặc tải dữ liệu người chơi sẽ được tái tạo tới PlayerDataClient.
  • Truy cập thông tin này với các phương thức getLoadErrorgetSaveError, cùng với các tín hiệu loadedsaved.
  • Có hai loại lỗi: DataStoreError (yêu cầu DataStoreService không thành công) và SessionLocked (xem Khóa phiên).
  • Sử dụng những sự kiện này để vô hiệu hóa các lời mời mua trong khách hàng và triển khai các hộp thoại cảnh báo. Hình ảnh này cho thấy một ví dụ về hộp thoại:
Một hình ảnh chụp màn hình của ví dụ về cảnh báo có thể được hiển thị khi dữ liệu người chơi không tải thành công

Lưu dữ liệu người chơi

Một sơ đồ quy trình minh họa hệ thống lưu
  1. Khi người chơi rời trò chơi, hệ thống thực hiện các bước sau:

    1. Kiểm tra xem có an toàn để ghi dữ liệu của người chơi vào kho dữ liệu hay không. Các tình huống mà nó sẽ không an toàn bao gồm việc dữ liệu của người chơi không tải thành công hoặc vẫn đang trong quá trình tải.
    2. Thực hiện một yêu cầu thông qua SessionLockedDataStoreWrapper để ghi giá trị dữ liệu hiện tại trong bộ nhớ tới kho dữ liệu và loại bỏ khóa phiên một khi hoàn thành.
    3. Xóa dữ liệu của người chơi (và các biến khác như siêu dữ liệu và trạng thái lỗi) khỏi bộ nhớ của máy chủ.
  2. Trong một vòng lặp định kỳ, máy chủ ghi dữ liệu của mỗi người chơi vào kho dữ liệu (với điều kiện nó an toàn để lưu). Việc lặp lại này giúp giảm thiểu tổn thất nếu máy chủ gặp sự cố và cũng cần thiết để duy trì khóa phiên.

  3. Khi nhận được yêu cầu tắt máy chủ, các bước sau sẽ diễn ra trong callback BindToClose:

    1. Một yêu cầu được thực hiện để lưu dữ liệu của từng người chơi trong máy chủ, theo quy trình thông thường được thực hiện khi một người chơi rời máy chủ. Các yêu cầu này được thực hiện song song, vì các callback BindToClose chỉ có 30 giây để hoàn thành.
    2. Để thúc đẩy quá trình lưu, tất cả các yêu cầu khác trong hàng đợi của mỗi khóa sẽ được xóa khỏi DataStoreWrapper cơ bản (xem Thử lại).
    3. Callback sẽ không trả về cho đến khi tất cả các yêu cầu đã hoàn thành.
©2026 Roblox Corporation. Roblox, logo Roblox và Powering Imagination là các nhãn hiệu đã đăng ký và chưa đăng ký của chúng tôi tại Hoa Kỳ và các quốc gia khác.