I’m using OOP in my project and have a Player class that is created and managed on the server.
The class stores player data and contains game logic methods.
I want the client to be able to read player data, but not modify it or access server-only methods.
I couldn’t find a clean way to replicate or expose this class to the client safely.
What is the best practice to structure server-only classes while allowing the client read-only access to the data?
Below is the class code I wrote. I have already checked it and understand that it does not work. But I couldn’t come up with a better way.
--!strict
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local IS_CLIENT = RunService:IsClient()
local IS_SERVER = RunService:IsServer()
local players = {} :: { [Player]: PlayerClass }
local PlayerClass = {}
PlayerClass.__index = PlayerClass
export type PlayerClass = typeof(setmetatable(
{} :: {
player: Player,
},
PlayerClass
))
-- Shared function
function PlayerClass.GetFromPlayer(player: Player): PlayerClass | nil
if not player or not player:IsA("Player") then return end
return players[player]
end
-- Only server function
function PlayerClass.new(player: Player): PlayerClass | nil
if not IS_SERVER then return end
local self = setmetatable({
player = player,
}, PlayerClass) :: PlayerClass
players[player] = self
return self
end
return PlayerClass
You lose the __index metatable and any functions on a table when you cross across the fuzzy world of the remotes.
A design pattern that might be better for you is to avoid putting functions on tables completely and instead call those methods directly through a module.
(its also faster, but i digress)
local PlayerData = {}
function PlayerData.new()
return {
}
end
function PlayerData.doSomething(playerData)
end
return PlayerData
What are exactly does the class do? Do the client and server data have to exactly be the same?
Depending on use case and what methods/attributes it’ll have, you can just make another class for the client in parallel with the server.
You could also just make the client’s class and store it in ReplicatedStorage, which then the server will take and add the server methods (polymorphism).
Or you could even not use OOP at all on the client. Let the server do the work and have the client take their data from the server and use a normal module to manage it without OOP.
You should also consider whether making a player class is even worth it at all. I’ve certainly considered doing it before but to keep the programming simple, you can just not use a class at all!
The class exists to group all player data and logic accessible only to the server into a single, well-defined object, rather than distributing it across multiple scripts.
The idea is that the server owns the actual Player class, while the client only reads the player’s data without being able to modify it or call methods that are only available to the server.
Avoid putting server-only and client-only code in the same module, this is really bad practice.
Is the OOP idiom really necessary here? Seems like it’d be better to use the ECS style.
You’re storing the [spoiler]faux-[/spoiler]objects in a table internally, and exposing it through a function that could have been just players[Player].