Networking instances of classes (objects)

Hello everybody! For a new gamemode, I decided that I would do make it object oriented by making a simple class system via metatables. It’s pretty neat and it works really well, I already made a database class using MySQLOO but then I found myself completly stuck, I need to network my classes. I wanted to make it in a way that I just had to tell my class function “This class is networked” and then the instances of this class would be networked between the server and the client. Here is my class code for now (without any networking):


--[[
	Description: Creates a class table ready to be used.

	Arguments:	cls		table		Class table, should only contain basics such as the parent.

	Returns:	table				A class table ready to be used.	
]]--
function class(cls)
	if (cls == nil) then
		cls = {};
	end
	
	cls.__index = cls;
	
	local parent = {};
	if (cls.parent != nil) then
		parent = cls.parent;
	end

	for k, v in pairs(parent) do
		if (cls[k] == nil) then
			cls[k] = v;
		end
	end
	
	setmetatable(cls, {
			__call = function(cls, ...)
				local obj = setmetatable({}, cls);
				if (obj._construct != nil) then
					obj:_construct(...);
				end
				return obj;
			end,
	});
	
	return cls;
end

--[[
	Description: Checks if the given object is an instance of the given class.

	Arguments:	obj		table		The object.
				cls		table		The class.

	Returns:	boolean				True if the object is an instance of the given class, false otherwise.
]]--
function instanceOf(obj, cls)
	return getmetatable(obj) == cls;
end

--[[
	Description: Checks if the given object is a child of the given class.

	Arguments:	obj		table		The object.
				cls		table		The class.

	Returns:	boolean				True if the object is a child of the given class, false otherwise.
]]--
function childOf(obj, cls)
	return getmetatable(obj).parent == cls;
end

I know how to use the net library. But networking objects automatically is way harder than I thought. I tried to make something, each time an object was created, I stored it in a networked table with a unique ID, but the way I did it ended up being the biggest mess ever. Maybe there could be a way to do like for entities with the function “NetworkedVar” but I still haven’t found a way to do so.

I am not asking for someone to create a code for me, I just would like to have insights on the issue and ideas on how I could network my objects. Thank you all in advance!

I’d give the base class a method which marks variables as “networkable” - store them in a table on the instance
Look into the __newindex metamethod - in it check if the variable is in the “networkable” table, if it is then network it and set it on the class clientside (give each class a unique id)

seriously whats with that tabbing

Most use 4 spaces = 1 tab in programs but browsers may display it as 5, 6, 8 or whatever so each tab means something else meaning they don’t like up in different programs.

Copy and paste it into Notepad++ and it lines up correctly.

On topic: When you network objects without needing to network code ( keep it shared ) then you’ll need to create a method which turns the data into a string, or a table, which can then be networked and reassembled on the other end. This should only happen once. Next, when data changes, you will need to update the clients that need to know without networking all of it again ( so you’re not wasting networking bandwidth like Garry’s Mod already does by networking the mouse deltas even when the game isn’t focused to the tune of up to ~3KB/sec )…

Take a look at my networking addon, or my simple networking system. They network data on load then update info that the client needs but only when the data changes… The main addon is being updated slowly but surely to implement the registry system ( which will optimize the networking even further ) but the simple version should provide enough…

https://dl.dropboxusercontent.com/u/26074909/tutoring/_systems/simple_networking_system/sh_basic_networking_and_data_system.lua.html

Another example would be a card object. The only thing special about a card would be the value so you can recreate the object anywhere you want by reading the value and creating a copy using that value. Find what makes your object become what it is and network the specifics. You won’t be able to share the exact same reference on the client that the server has because they run independently and don’t share their hardware memory…

If you want a way to be able to reference the same id on both the server and client, then simply assign the id on the server, and on the client when you re-create the class you will pass that id along so it assumes that identity like how I use EntIndex for client and server to reference the data ( which is stored in a global table ) associated with each entity… When it is assigned the first time then you should have no issue grabbing the data in the future.

Thank you both rejax and Acecool, I understand both your answers but what I came up with is messed up in every way:

What I tried is that classes can be marked as “networked”, meaning they will be shared between the server and the client. When an instance of a “networked” class is created, this instance is stored in a global table that stores every instances of networked objects. To give the objects unique IDs, I just used the index at which they were inserted at in the global table.
Then, if a server side object needs to update it client side self, I created a function, using the net library, that updates the client side object with the same unique ID and sets the given variable of it to the given value.

That ended up being an abominable mess, as the unique IDs where not given properly and the values were not set…

Moreover, I found another problem: In my gamemode, each player will have objects assigned to him (like an inventory object…), but every clients must be aware of each player objects, meaning that a player can, for example, fetch the items from another player’s inventory. Meaning each time a player connects, I have to send him the global table containing the objects. This isn’t truly hard to do, but this must imperatively be done before the client cause the creation of new objects, otherwise it messes with the unique IDs (At least when I used the global table index as unique IDs).

If you want to see, here is my current code (it is completely unfinished, and was just done for test purposes):


if (SERVER) then
	util.AddNetworkString('OORPObjectNW');
else
	net.Receive('OORPObjectNW', function(len, ply)
		local netIndex = net.ReadUInt(13);
		local index	= net.ReadString();
		local data = net.ReadString();
		print('NETID:' .. tostring(netIndex) .. ' INDEX: ' .. index .. ' DATA: ' .. data)
		
		if (net.objects[netIndex] != nil) then
			
			net.objects[netIndex][index] = data;
			print(net.objects[netIndex][index]);
		end
	end)
end

net.objects = {};

--[[
	Description: Creates a class table ready to be used.

	Arguments:	cls		table		Class table, should only contain basics such as the parent.

	Returns:	table				A class table ready to be used.	
]]--
function class(cls)
	if (cls == nil) then
		cls = {};
	end
	
	cls.__index = cls;
	
	local parent = {};
	if (cls.parent != nil) then
		parent = cls.parent;
	end
	
	local networked = {};
	if (cls.networked != nil) then
		networked = cls.networked;
	end

	for k, v in pairs(parent) do
		if (cls[k] == nil) then
			cls[k] = v;
		end
	end
	
	if (networked == true) then
	
		-- UNFISHED, just for test purposes
		
		cls.NetworkData = function(self, index, data)
			net.Start('OORPObjectNW')
				net.WriteUInt(self.netIndex, 13);
				net.WriteString(index);
				net.WriteString(data);
			net.Broadcast();
		end
	end
	
	setmetatable(cls, {
			__call = function(cls, ...)
				local obj = setmetatable({}, cls);
				if (obj._construct != nil) then
					obj:_construct(...);
				end
				if (obj.networked == true) then
					-- local netIndex = table.LastKey(net.objects)
				
					print('INDEX: ' .. table.insert(net.objects, obj));
					-- print('NET INDEX ON CONSTRUCT: ' .. obj.netIndex)
				end
				return obj;
			end,
	});
	
	return cls;
end

I just think the way I’m doing it is wrong. First of all, I think that using the indexes as unique IDs is a major problem, I will have to do it another way, or at least change the way I use this system. But still I don’t know what other system I could use, maybe I just shouldn’t give the unique IDs automatically, and just have them "hardcoded’, but I think that would end up being a mess.
Or maybe I’m just doing it all wrong. Maybe the “card” system as Acecool said could be a way. I still don’t know exactly how to do it the more efficiently, but I’m still searching.

Insights on my current issues are welcome, thank you all for your help!

What are you trying to do? There’s probably an easier way.

I tried to make it like the “SetNetworked” functions of entities to network between a server side object and its client side self, as I described In my previous post, but I did it all wrong.

What are these objects being used for? What exactly are you going to be networking?
Try to ‘step back’ and describe the actual problem you’re trying to solve, not the solution.

I need to network are values between a server side object and it client side self. I also need every clients to have access to every networked objects. As for the major problem of my actual system, I described it in my previous post: The unique IDs system I use doesn’t work properly. Here are the major problems, plus I don’t know if the “solution” I found is the best one. I hope that’s what you were asking for (And thank you for your quick replies).

[LUA]local nw_tables = {}

if SERVER then
util.AddNetworkString(“nw_table”)
else
net.Receive(“nw_table”, function()
local uid = net.ReadString()
local tab = nw_tables[uid]
if not tab then error("unregistered " … uid) return end
local key = net.ReadString()
local value
if tab.variables[key] then
value = tab.variables[key].read()
else
error("unregistered variable " … key … " for " … uid)
end

	rawset(tab, key, value)
end)

end

local meta = {}
meta.__index = meta

function meta:__call(id)
local obj = setmetatable({}, meta)

nw_tables[id] = obj

rawset(obj, "id", id)
rawset(obj, "variables", {})

return obj

end

function meta:register_var(name, read, write)
self.variables[name] = {read = read, write = write}
end

function meta:__newindex(k, v)
if SERVER and self.variables[k] then
net.Start(“nw_table”)
net.WriteString(self.id)
net.WriteString(k)
self.variables[k].write(v)
net.Broadcast()
rawset(self, k, v)
end
end

return setmetatable({}, meta)[/LUA]

I wrote up a quick system for you - I can’t test it but it should work.
Usage is like so:
[LUA]
local tab = nw_table(“test”)
tab:register_var(“something”, net.ReadBool, net.WriteBool)
tab.something = true
[/LUA]

make sure you create any tables in the shared realm, and that the script is included shared

Thank you a lot! I’m going to try it and tell you about the results. I’ll have to adapt it to my current class system but that shouldn’t be truly hard. Some tweaking could also be of use, so I’ll also do that. Thank you a lot for your help, the system you made is way better than the one I thought of.

Well, I tried for the whole day, but it doesn’t work, each time I make an object in the shared realm (I did it in the Init function of my player class), the client tells me that the object is unregistered. I think I know why but I don’t know how to fix it efficiently. The problem is that with this method, I can’t set any variables in the constructor of my classes, because it calls __newindex before the client side class being instanciated. Here is my current code if you want to take a glance at it:


net.objects = {};

if (SERVER) then
	util.AddNetworkString('OORPObjectNW');
else
	net.Receive('OORPObjectNW', function()
		local uid = net.ReadString()
		local tab = net.objects[uid]
		if not tab then error("unregistered " .. uid) return end
		local key = net.ReadString()
		local value
		if tab.vars[key] then
			value = tab.vars[key].read()
		else
			error("unregistered variable " .. key .. " for " .. uid)
		end
		
		rawset(tab, key, value)
	end)
end

--[[
	Description: Creates a class table ready to be used.

	Arguments:	cls		table		Class table, should only contain basics such as the parent.

	Returns:	table				A class table ready to be used.	
]]--
function class(tbl)
	cls = {};
	if (tbl) then
		cls = tbl;
	end
	
	cls.__index = cls;
	
	cls.networked = cls.networked || false;
	
	if (cls.parent) then
		for k, v in pairs(cls.parent) do
			if (cls[k] == nil) then
				cls[k] = v;
			end
		end
	end
	
	local meta = {};
	
	meta.__call = function(cls, ...)
		local obj = setmetatable({}, cls);
		local arg = {...};
				
		if (obj.networked) then
			local netUID = arg[1];
			
			rawset(obj, 'netUID', netUID);
			rawset(obj, 'vars', {});
			table.remove(arg, 1);
	
			net.objects[obj.netUID] = obj;
		end
	
		if (obj._construct != nil) then
			obj:_construct(...);
		end
	
		return obj;
	end
	
	if (cls.networked) then
		cls.NetworkVar = function(self, name, write, read)
			self.vars[name] = {write = write, read = read};
		end
	
		if (SERVER) then
			cls.__newindex = function(self, key, value)
				print(key, SERVER, table.ToString(self.vars))
				if (self.vars[key]) then
					net.Start('OORPObjectNW')
						net.WriteString(self.netUID);
						net.WriteString(key);
						self.vars[key].write();
					net.Broadcast();
					rawset(self, key, value);
				end
			end
		end
	end
	
	return setmetatable(cls, meta);
end

Here is the way I make a class:


example = class({networked = true})

example:_construct(test)
	self:NetworkVar('test', net.WriteString, net.ReadString)
	
	self.test = test;
end

And here is how I make an instance of it (in the shared realm):


local object = example('uniqueID', 'Some test string')

And this gives the error “unregistered uniqueID” on the client’s side. Maybe there is a way to wait the client side object to be instantiated, like having a temporary table, but that would be pretty bad I think. So there, I am still working on a stable solution. Still, thank you a lot for your help! Insights are still welcome.

See, the problem with doing something like this is you have to make sure it is sent to each client, no matter when they join (which I didn’t implement).
Instead of actually creating the object on the client, maybe have it just be a table with an id, and fetching values from it actually grabs them from another table which holds sub-tables for each unique class name.

Maybe that could be a good solution, but I thought of another one which also would (I think) allow a networked class to be instantiated in server side only. Here it is:

  1. When you create an object server side, it is stored in the global table, and is NOT sent to the client yet, the __newindex function also doesn’t send the variables to the client yet;
  2. Later, an object is made client side with the same unique ID as the server side object, the client sends a net message to the server, asking it to “synchronize” the client side and server side objects;
  3. When the server gets the request for the synchronization, the server side sends all the networked variables to the client, and the client sets them. As the two objects are now synchronized, __newindex can send the variables to the client.

Then I think that sending the server side global table to joining client should work. What do you think of such a system? Thanks!

Sounds great! Go for it and post how it goes! :slight_smile:

– THE CODE IS BROKEN! See the next post –

The synchronization method seems to work! It makes for quite a long code, but it allows to create an object server side only and then create it client side later. The current code needs a lot of tweaking and cleaning, and doesn’t handle player joining in middle game yet, but here is my current code (It, indeed, looks QUITE LIKE A MESS):


-- Removed as it was broken, see next post.

So, what do you think of that method? It works nice and seems “stable”, but damn was it long to arrive to this (and there’s still a lot to do). And thank you a lot for the help!

PS: I don’t mark this thread as solved as I could get more problems by updating my code in the near future (To handle players joining in middle game for example). Thanks.

Here is the full code, using the sync method, cleaned and tweaked (there is still work to be done but it still is better):


net.objects = {};

if (SERVER) then
	util.AddNetworkString('OORPObjectNW'); -- Used to network vars between server side and client side objects.
	util.AddNetworkString('OORPObjNWSynch'); -- Used to sync an object between the server and clients.
	
	--[[
		Description: Called when the client request to sync an object, 
		sends the currently networked vars of a server side object to its client self.
	]]--
	net.Receive('OORPObjNWSynch', function()
		local netUID = net.ReadString();
		local obj = net.objects[netUID];
		
		if (obj) then
			net.Start('OORPObjNWSynch')
				net.WriteString(netUID);
				for k in pairs(obj.vars) do
					if (obj[k]) then
						net.WriteString(k);
						net.WriteType(obj[k]);
					end
				end
			net.Broadcast();

			obj.synced = true;
		end		
	end)
	
else
	local function ReadVar(netUID, obj)
		local key = net.ReadString();
		local value = net.ReadType(net.ReadUInt(8));
		net.objects[netUID][key] = value;
	end

	--[[
		Description: Called when the server anwsers a request to sync an object,
		applies the received networked vars of the server side object to the client side one.
	]]--
	net.Receive('OORPObjNWSynch', function()
		local netUID = net.ReadString();
		local obj = net.objects[netUID];
		
		if (obj) then
			for k, v in pairs(obj.vars) do
				ReadVar(netUID, obj)
			end
		
			obj.synced = true;
		
			if (obj._synced != nil) then
				obj:_synced(LocalPlayer())
			end
		end
	end)
	
	--[[
		Description: Called when the server wants to set a networked var of an object,
		just sets the var to the received value.
	]]--
	net.Receive('OORPObjectNW', function()
		local netUID = net.ReadString();
		local obj = net.objects[netUID];
		ReadVar(netUID, obj);
	end)
end

--[[
	Description: Creates a class table ready to be used.

	Arguments:	cls		table		Class table, should only contain basics such as the parent.

	Returns:	table				A class table ready to be used.	
]]--
function class(args)
	cls = args || {};
	cls.__index = cls;
	
	cls.networked = cls.networked || false;
	
	-- Basic inheritance.
	if (cls.parent) then
		for k, v in pairs(cls.parent) do
			if (cls[k] == nil) then
				cls[k] = v;
			end
		end
	end
	
	-- For networked classes only.
	if (cls.networked) then
		--[[
			Description: Mark a var to be networked when changed.	
		]]--
		cls.NetworkVar = function(self, name)
			self.vars[name] = true;
		end
	
		if (SERVER) then
			cls.__newindex = function(self, key, value)
				if (self.synced && self.vars[key]) then
					net.Start('OORPObjectNW')
						net.WriteString(self.netUID);
						net.WriteString(key);
						net.WriteType(value);
					net.Broadcast();
				end
				rawset(self, key, value);
			end
		end
	end
	
	local meta = {};
	
	return setmetatable(cls, {__call = function(cls, ...)
		local obj = setmetatable({}, cls);
		local arg = {...}; -- Handling variable number of arguments.
				
		if (obj.networked) then
			local netUID = arg[1];
			
			rawset(obj, 'netUID', netUID);
			rawset(obj, 'vars', {});
			table.remove(arg, 1); -- The first argument needs to be removed as it is now useless.
	
			obj.synced = false; -- Not synced yet...
			net.objects[obj.netUID] = obj;
		end

		if (obj._construct != nil) then
			obj:_construct(unpack(arg));
		end
		
		-- Requesting for sync if the object is client side. 
		if (CLIENT && obj.networked) then
			net.Start('OORPObjNWSynch');
				net.WriteString(obj.netUID);
			net.SendToServer();
		end
	
		return obj; -- obvs
	end})
end