Best way to expose server-side class data to the client?

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
1 Like

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

@Yarik_superpro index user spotted

1 Like

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!

1 Like

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.

Replicate just the data to the client.

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].

2 Likes

I think you’re right about ECS, thanks for the advice!

That would be the best option for me!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.