Appearance
Best Practices
Guidelines and recommendations for using PlayerState effectively in your ROBLOX game.
Performance Optimization
Minimize Change Listeners
lua
-- DON'T: Listen to all changes unless necessary
PlayerState.OnChanged(".", function(newValue, oldValue, path)
-- This fires for EVERY data change
updateEntireUI()
end)
-- DO: Listen to specific paths
PlayerState.OnChanged("Coins", updateCoinsDisplay)
PlayerState.OnChanged("Level", updateLevelDisplay)
PlayerState.OnChanged("Plot.Likes", updateLikesDisplay)
Batch Updates
lua
-- DON'T: Multiple individual calls
PlayerState.Set(player, "Coins", newCoins)
PlayerState.Set(player, "Level", newLevel)
PlayerState.Set(player, "Experience", newExp)
-- DO: Single batch update
PlayerState.SetValues(player, {
Coins = newCoins,
Level = newLevel,
Experience = newExp
})
Error Handling
Server Validation
lua
-- DO: Always validate inputs
function CurrencyService.AddCoins(player: Player, amount: number): boolean
-- Validate player
if not player or not player.Parent then
warn("[Currency] Invalid player")
return false
end
-- Validate amount
if typeof(amount) ~= "number" or amount <= 0 or amount > 1000000 then
warn("[Currency] Invalid coin amount:", amount)
return false
end
-- Check if player data exists
local currentCoins = PlayerState.Get(player, "Coins")
if not currentCoins then
warn("[Currency] Player data not loaded")
return false
end
PlayerState.Set(player, "Coins", currentCoins + amount)
return true
end
Client Safety
lua
-- DO: Always provide fallbacks
local function getPlayerCoins(): number
return PlayerState.Get("Coins") or 0
end
-- ✅ Handle missing data gracefully
local function updateInventoryUI()
local inventory = PlayerState.GetPath("Inventory")
if not inventory or typeof(inventory) ~= "table" then
inventory = {} -- Fallback to empty inventory
end
-- Update UI with safe data
for i, item in ipairs(inventory) do
if item and item.Name then -- Validate item structure
createInventorySlot(item)
end
end
end
Data Structure Design
Keep It Flat When Possible
lua
-- DON'T: Overly nested structure
{
Player = {
Stats = {
Combat = {
Weapons = {
Sword = {
Damage = 10
}
}
}
}
}
}
-- DO: Flatter structure
{
CombatStats = {
SwordDamage = 10,
SwordLevel = 1
}
}
Use Arrays for Lists
lua
-- ✅ Use arrays for ordered collections
{
Inventory = {
{Id = "sword_001", Name = "Iron Sword"},
{Id = "potion_001", Name = "Health Potion"}
}
}
-- ✅ Use dictionaries for key-value data
{
Settings = {
MusicEnabled = true,
GraphicsQuality = "High"
}
}
Security Considerations
Client-Server Communication
No Built-in Remotes
PlayerState does NOT include built-in RemoteEvents or RemoteFunctions for security reasons. If you want clients to request data modifications (like adding coins when completing a task), you must create your own RemoteEvents and validate all requests on the server.
lua
-- ❌ PlayerState has no built-in client → server communication
-- PlayerState.RequestAddCoins() -- This doesn't exist!
-- ✅ Create your own RemoteEvents for client requests
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CoinRequest = ReplicatedStorage.Remotes.CoinRequest -- Your RemoteEvent
-- Server: Handle client requests with validation
CoinRequest.OnServerEvent:Connect(function(player, action, amount)
if action == "TaskCompleted" then
-- Validate the request
if isValidTask(player, amount) then
local currentCoins = PlayerState.Get(player, "Coins")
PlayerState.Set(player, "Coins", currentCoins + amount)
end
end
end)
-- Client: Request coin addition
CoinRequest:FireServer("TaskCompleted", 50)
Server Authority
lua
-- ✅ All data modifications on server only
-- Never trust client data directly
-- Server Remote Handler
local function handleCoinPurchase(player, itemId)
local itemPrice = ShopConfig[itemId].Price
local playerCoins = PlayerState.Get(player, "Coins")
if playerCoins >= itemPrice then
PlayerState.Set(player, "Coins", playerCoins - itemPrice)
PlayerState.AddToArray(player, "Inventory", ShopConfig[itemId])
return true
end
return false
end
Input Validation
lua
-- ✅ Validate all inputs thoroughly
local function setPlayerSetting(player: Player, settingName: string, value: any): boolean
-- Whitelist valid settings
local validSettings = {
MusicEnabled = "boolean",
SoundEnabled = "boolean",
GraphicsQuality = "string"
}
if not validSettings[settingName] then
return false
end
if typeof(value) ~= validSettings[settingName] then
return false
end
-- Additional validation for specific settings
if settingName == "GraphicsQuality" then
local validQualities = {"Low", "Medium", "High"}
if not table.find(validQualities, value) then
return false
end
end
PlayerState.SetPath(player, "Settings." .. settingName, value)
return true
end
Memory Management
Clean Up Connections
lua
-- ✅ Proper connection cleanup
local PlayerConnections = {}
local function setupPlayer(player)
local connections = {}
connections.coins = PlayerState.OnChanged("Coins", function(newValue)
updateCoinsUI(newValue)
end)
connections.level = PlayerState.OnChanged("Level", function(newValue)
updateLevelUI(newValue)
end)
PlayerConnections[player] = connections
end
local function cleanupPlayer(player)
local connections = PlayerConnections[player]
if connections then
for _, connection in pairs(connections) do
if connection then
connection:Disconnect()
end
end
PlayerConnections[player] = nil
end
end
game.Players.PlayerRemoving:Connect(cleanupPlayer)
Testing Strategies
Development Environment
lua
-- ✅ Use different data stores for testing in PlayerStateConfig.lua
local Config = {
Server = {
DataStore = {
Name = game.PlaceId == DEV_PLACE_ID and "PlayerData_DEV" or "PlayerData_PROD",
Scope = game.PlaceId == DEV_PLACE_ID and "Testing" or "Production"
}
}
}
Mock Data for Testing
lua
-- ✅ Create test utilities
local TestUtils = {}
function TestUtils.createMockPlayerData()
return {
Coins = 1000,
Level = 5,
Experience = 250,
Inventory = {
{Id = "sword_001", Name = "Test Sword"},
{Id = "potion_001", Name = "Test Potion"}
},
Settings = {
MusicEnabled = true,
SoundEnabled = true
}
}
end
function TestUtils.setupTestPlayer(player)
PlayerState.Init(player, TestUtils.createMockPlayerData())
end
Code Organization
Modular Services
lua
-- ✅ Create focused service modules
-- CurrencyService.lua - Only handles currency
-- InventoryService.lua - Only handles inventory
-- LevelService.lua - Only handles leveling
-- SettingsService.lua - Only handles settings
-- Each service has a clear, single responsibility
Consistent Naming
lua
-- ✅ Use consistent naming patterns
local function getPlayerCoins(player) end -- get + noun
local function setPlayerCoins(player, amount) end -- set + noun
local function addPlayerCoins(player, amount) end -- add + noun
-- Service functions
CurrencyService.GetCoins()
CurrencyService.AddCoins()
CurrencyService.SpendCoins()
Common Pitfalls to Avoid
Don't Modify Returned Data
lua
-- ❌ Don't modify returned data directly
local inventory = PlayerState.GetPath("Inventory")
table.insert(inventory, newItem) -- This won't sync!
-- ✅ Use proper API methods
PlayerState.AddToArray(player, "Inventory", newItem)
Don't Assume Data Exists
lua
-- ❌ Assuming data exists
local coins = PlayerState.Get("Coins")
if coins > 100 then -- Error if coins is nil
-- ✅ Check for nil values
local coins = PlayerState.Get("Coins") or 0
if coins > 100 then
Don't Create Excessive Listeners
lua
-- ❌ Creating listeners in loops
for i = 1, 100 do
PlayerState.OnChanged("Coins", function()
updateSlot(i)
end)
end
-- ✅ Single listener with efficient updates
PlayerState.OnChanged("Coins", function(newValue)
updateAllSlots(newValue)
end)
Following these best practices will help you create a robust, performant, and maintainable game using PlayerState.