Structure System
The Structure module provides powerful utilities for binding table data to Configuration
instances (binder) and observing/reading structured data (reader). It enables seamless data synchronization between server and client in Roblox games.
Overview
The Structure system offers:
- Data Binding: Bind table data to Configuration instances on the server
- Data Reading: Read and observe data from Configuration instances on the client
- Deep Nesting: Support for nested structures with configurable depth
- Auto-Synchronization: Automatic data sync between server and client
- Type Safety: Support for Roblox's basic data types
- Real-time Updates: Live observation of data changes
Supported Data Types
Structure supports the following data types with automatic conversion:
Lua Type | Roblox ValueBase |
---|---|
string |
StringValue |
number (integer) |
IntValue |
number (decimal) |
NumberValue |
boolean |
BoolValue |
Instance |
ObjectValue |
CFrame |
CFrameValue |
Vector3 |
Vector3Value |
Color3 |
Color3Value |
table |
Configuration (nested) |
Architecture
Core Classes
Structure
Base class providing common functionality for all structure types.
StructureBinder
Server-side class for binding table data to Configuration instances.
StructureReader
Client-side class for reading and observing data from Configuration instances.
ValueStructureBinder
Specialized binder using ValueBase instances as keys.
ValueStructureReader
Corresponding reader for ValueStructureBinder.
API Documentation
Structure.binder()
Creates a StructureBinder instance to bind a table to a Configuration.
Parameters:
- dataTable
- Table containing data to bind
- config
- Configuration instance to bind to
- deepLevel
- Maximum depth for nested tables (default: 100)
Returns: StructureBinder instance
Example:
local config = Instance.new("Configuration")
config.Name = "PlayerData"
config.Parent = game.ReplicatedStorage
local playerData = {
name = "PlayerOne",
level = 15,
inventory = {
coins = 1000,
items = {"sword", "shield"}
},
position = Vector3.new(0, 10, 0)
}
local binder = Structure.binder(playerData, config)
-- Update data (automatically syncs to clients)
binder.level = 16
binder.inventory.coins = 1100
Structure.reader()
Structure.reader(config: Configuration, deepLevel: number?, meta: StructureReader?) -> StructureReader
Creates a StructureReader instance to read data from a Configuration.
Parameters:
- config
- Configuration instance to read from
- deepLevel
- Maximum depth for nested structures (default: 100)
- meta
- Custom metatable (optional)
Returns: StructureReader instance
Example:
-- Client-side
local config = game.ReplicatedStorage:WaitForChild("PlayerData")
local reader = Structure.reader(config)
-- Read current data
print("Player name:", reader.name)
print("Player level:", reader.level)
print("Coins:", reader.inventory.coins)
Structure.fromSkeleton()
Structure.fromSkeleton(
dataTable: table,
config: Configuration,
skeleton: table,
deepLevel: number?,
meta: StructureBinder?
) -> StructureBinder
Binds a table according to a predefined skeleton structure.
Parameters:
- dataTable
- Actual data to bind
- config
- Configuration instance
- skeleton
- Skeleton structure definition
- deepLevel
- Maximum depth
- meta
- Custom metatable
Example:
local skeleton = {
player = {
stats = {
health = 0,
mana = 0
},
settings = {
volume = 0.5,
graphics = "medium"
}
}
}
local data = {
player = {
stats = {
health = 100,
mana = 50
},
settings = {
volume = 0.8,
graphics = "high"
}
}
}
local binder = Structure.fromSkeleton(data, config, skeleton)
Structure.bridger()
Structure.bridger(
dataTable: table,
config: Configuration,
skeleton: table,
deepLevel: number?
) -> StructureBinder | StructureReader
Creates a binder on server or reader on client automatically based on context.
Example:
-- This code works on both server and client
local bridger = Structure.bridger(data, config, skeleton)
-- On server: returns StructureBinder
-- On client: returns StructureReader
StructureBinder Methods
Set()
Replaces all current data with a new table, clearing existing bindings.
Example:
Destroy()
Cleans up resources and destroys all bound instances.
StructureReader Methods
OnChange()
Registers a callback to be called when the structure changes.
Example:
Observe()
Observes when new keys are added or existing keys are removed.
Example:
reader:Observe(
function(key)
print("New key added:", key)
end,
function(key)
print("Key removed:", key)
end
)
ObserveKey()
Observes changes to a specific key.
Example:
reader:ObserveKey("level", function(newLevel)
print("Player leveled up to:", newLevel)
updateLevelUI(newLevel)
end)
OnPairs()
Registers a callback for each existing and new key-value pair.
Example:
Raw()
Returns the raw table data without Structure wrapper.
Wait()
Waits for a key to appear (maximum 30 seconds timeout).
Example:
Complete Examples
Server Implementation
local Structure = require(ReplicatedStorage.Systems.Structure)
-- Game state data
local gameState = {
round = 1,
timeRemaining = 300,
status = "waiting",
players = {},
leaderboard = {
top3 = {}
}
}
-- Create Configuration in ReplicatedStorage
local gameConfig = Instance.new("Configuration")
gameConfig.Name = "GameState"
gameConfig.Parent = game.ReplicatedStorage
-- Bind the data
local gameBinder = Structure.binder(gameState, gameConfig)
-- Game logic updates
game.Players.PlayerAdded:Connect(function(player)
gameBinder.players[player.Name] = {
score = 0,
kills = 0,
deaths = 0,
joinTime = os.time()
}
end)
game.Players.PlayerRemoving:Connect(function(player)
gameBinder.players[player.Name] = nil
end)
-- Update game state
local function startNewRound()
gameBinder.round = gameBinder.round + 1
gameBinder.timeRemaining = 300
gameBinder.status = "active"
-- Reset player scores
for playerName, playerData in gameBinder.players do
playerData.score = 0
playerData.kills = 0
playerData.deaths = 0
end
end
Client Implementation
local Structure = require(ReplicatedStorage.Systems.Structure)
local Players = game:GetService("Players")
-- Read game state from server
local gameConfig = game.ReplicatedStorage:WaitForChild("GameState")
local gameReader = Structure.reader(gameConfig)
-- UI References
local gameUI = Players.LocalPlayer.PlayerGui:WaitForChild("GameUI")
local roundLabel = gameUI.RoundLabel
local timerLabel = gameUI.TimerLabel
local leaderboardFrame = gameUI.LeaderboardFrame
-- Observe round changes
gameReader:ObserveKey("round", function(newRound)
roundLabel.Text = "Round " .. newRound
print("Starting round", newRound)
end)
-- Observe timer changes
gameReader:ObserveKey("timeRemaining", function(timeLeft)
local minutes = math.floor(timeLeft / 60)
local seconds = timeLeft % 60
timerLabel.Text = string.format("%02d:%02d", minutes, seconds)
if timeLeft <= 10 then
timerLabel.TextColor3 = Color3.new(1, 0, 0) -- Red warning
else
timerLabel.TextColor3 = Color3.new(1, 1, 1) -- White
end
end)
-- Observe player data changes
gameReader.players:OnChange(function(playerName, playerData)
if playerData then
updatePlayerInLeaderboard(playerName, playerData)
else
removePlayerFromLeaderboard(playerName)
end
end)
-- Observe game status
gameReader:ObserveKey("status", function(status)
if status == "waiting" then
showWaitingScreen()
elseif status == "active" then
showGameScreen()
elseif status == "ended" then
showEndScreen()
end
end)
-- Helper functions
function updatePlayerInLeaderboard(playerName, playerData)
local playerFrame = leaderboardFrame:FindFirstChild(playerName)
if not playerFrame then
playerFrame = leaderboardFrame.PlayerTemplate:Clone()
playerFrame.Name = playerName
playerFrame.Visible = true
playerFrame.Parent = leaderboardFrame
end
playerFrame.PlayerName.Text = playerName
playerFrame.Score.Text = tostring(playerData.score)
playerFrame.KD.Text = string.format("%d/%d", playerData.kills, playerData.deaths)
end
function removePlayerFromLeaderboard(playerName)
local playerFrame = leaderboardFrame:FindFirstChild(playerName)
if playerFrame then
playerFrame:Destroy()
end
end
Advanced Usage with Skeleton
-- Define a complex game data structure
local gameDataSkeleton = {
settings = {
gameplay = {
roundTime = 300,
maxPlayers = 16,
friendlyFire = false
},
graphics = {
quality = "medium",
shadows = true,
particleEffects = true
}
},
match = {
teams = {
red = {
players = {},
score = 0,
color = Color3.new(1, 0, 0)
},
blue = {
players = {},
score = 0,
color = Color3.new(0, 0, 1)
}
},
powerups = {},
events = {}
}
}
-- Initialize with default data
local defaultGameData = {
settings = {
gameplay = {
roundTime = 600,
maxPlayers = 20,
friendlyFire = true
},
graphics = {
quality = "high",
shadows = true,
particleEffects = true
}
},
match = {
teams = {
red = {
players = {"Player1", "Player2"},
score = 0,
color = Color3.new(1, 0, 0)
},
blue = {
players = {"Player3", "Player4"},
score = 0,
color = Color3.new(0, 0, 1)
}
},
powerups = {
"speed_boost",
"shield"
},
events = {}
}
}
local config = Instance.new("Configuration")
config.Name = "AdvancedGameData"
config.Parent = game.ReplicatedStorage
local advancedBinder = Structure.fromSkeleton(
defaultGameData,
config,
gameDataSkeleton,
5 -- Limit depth to 5 levels
)
-- Update nested data
advancedBinder.settings.gameplay.roundTime = 450
advancedBinder.match.teams.red.score = 10
table.insert(advancedBinder.match.powerups, "health_pack")
Best Practices
Performance Optimization
-
Limit Deep Level: Use appropriate
deepLevel
values to prevent performance issues with overly deep structures. -
Batch Updates: When making multiple changes, batch them to reduce network traffic.
Memory Management
-
Always Destroy: Call
Destroy()
when structures are no longer needed. -
Avoid Memory Leaks: Be careful with circular references in callback functions.
Error Handling
-
Use pcall: Wrap Structure operations in pcall for production code.
-
Validate Data Types: Ensure data types are supported before binding.
Code Organization
-
Separate Concerns: Keep binder logic on server, reader logic on client.
-
Use Modules: Organize Structure usage in dedicated modules.
Limitations and Considerations
Data Type Limitations
- Only supports Roblox's basic data types
- No support for functions or coroutines
- Tables with circular references are not supported
Performance Considerations
- Deep nesting can impact performance
- Large datasets may cause network congestion
- Frequent updates can overwhelm the replication system
Network Limitations
- Attributes have size limitations
- Too many ValueBase instances can impact performance
- Consider using RemoteEvents for large data transfers
Error Scenarios
- Invalid data types will throw errors
- Destroyed Configuration instances will break readers
- Network issues can cause desynchronization
Troubleshooting
Common Issues
-
"Unsupported value type" Error
-
Memory Leaks
-
Synchronization Issues
Debug Tips
-
Enable Verbose Logging
-
Check Configuration Structure
local function printConfigStructure(config, indent) indent = indent or "" print(indent .. config.Name .. " (" .. config.ClassName .. ")") for _, child in config:GetChildren() do if child:IsA("Configuration") then printConfigStructure(child, indent .. " ") else print(indent .. " " .. child.Name .. " = " .. tostring(child.Value)) end end end printConfigStructure(game.ReplicatedStorage.GameData)
This comprehensive documentation should help developers understand and effectively use the Structure system in their Roblox projects.