Keep Player references 'alive' when serializing Player instances as dictionary keys across the network

As a Roblox developer, it is currently too troublesome to send dictionaries through RemoteEvents or RemoteFunctions when the dictionaries use Player instances as keys.

A common pattern in code is to have a dictionary on the server which uses Player instances as keys to map certain data to each player. In my game I use this pattern dozens of times to assign for example SpawnLocations to each player, or equipped items, or locations inside the game, and a lot more.

It is common that you may want to replicate this data to clients. The naive assumption is that you can send the dictionary through a RemoteEvent to the clients. There’s a caveat with this however, which is that the Player instance ‘key’ gets serialized which will change how a lot of checks may acts.

Consider the following server Script and client LocalScript:

-- server Script
local Dictionary = {}

game.Players.PlayerAdded:Connect(
	function(Plr: Player)
		Dictionary[Plr] = Plr
		
		for k, v in pairs(Dictionary) do
			print(k, v)
			print(k == Plr, v == Plr)
			print(k.UserId, v.UserId)
		end
		
		wait(2)
		game.ReplicatedStorage.RemoteEvent:FireClient(Plr, Dictionary)
	end
)
-- client LocalScript
game.ReplicatedStorage.RemoteEvent.OnClientEvent:Connect(
	function(Dictionary)
		for k, v in pairs(Dictionary) do
			print(k, v)
			print(k == game.Players.LocalPlayer, v == game.Players.LocalPlayer)
			print(k.UserId, v.UserId)
		end
	end
)

The server will print the following information (in my case):

Zomebody Zomebody
true true
8291118 8291118

The client will print this information however:

<Instance> (Zomebody) Zomebody
false true
nil 8291118

The interesting part about what is printed on the client is that the value in the dictionary is still fully recognized as a player instance, but the key is serialized in some manner that makes it impossible to retrieve certain information. The key is no longer equal to my player character and the UserId is also set to nil so there seems to be no way to ‘recover’ the player in this case.

This means that when sending dictionaries of the form Dictionary[Player] = value to clients, you will need to first transform the dictionary such that the player key is replaced with the player’s UserId so that the client can then use game.Players:GetPlayerByUserId() to get back the player instance. This is very cumbersome because it usually involves creating a new table to copy information into and the client will then also need to take extra steps to get back the Player instance.

The existing behavior may also cause bugs in your code. The output will still print <Instance> (name) which may be interpreted by the developer as a Player instance, even though it doesn’t quite work that way. Trying to compare this key to player instances will always equal false, which has caught me off-guard many times by now.

I would like the current behavior to change such that the dictionary keys can be reconstructed into Player instances. This would simplify a lot of code that involves sending such dictionaries across the network.

9 Likes

This doesn’t happen only on RemoteEvents and RemoteFunctions, it happens with the BindableEvent and BindableFunction as well. Any key that is not a string gets turned into a string.

I used to send dictionaries to the client using keys that were instances and I had to send two tables to work around this behavior.


I would like this behavior to change as well.
Though I understand it can get tricky for tables that are used as keys as well…

4 Likes

The reflection system only supports two types of tables: Dictionaries with string keys, or Arrays with integer keys and no gaps.

What you’re seeing here is consequence of that limitation, nothing to do with Instances in particular. It wouldn’t work with tables or booleans as keys either.

For some more context, things could get complicated fast if more complex structures were allowed like reference types in keys. What should happen if the same value is in multiple keys? What should happen if the value is also in one of the keys? etc.

3 Likes

For whatever it’s worth, you can send any kind of dictionary by flattening it into an array.

local function flattenDictionary(dict)
	local flat = {}
	for k, v in dict do
		table.insert(flat, k)
		table.insert(flat, v)
	end
	return flat
end

local function unflattenDictionary(flat)
	local dict = {}
	for i = 1, #flat, 2 do
		dict[flat[i]] = flat[i+1]
	end
	return dict
end
6 Likes