Skip to content

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.

PlayerState - High-Performance Roblox Data Management