A common pattern that some people may run into while coding is something like this:
Input.OnServerEvent:Connect(function(player, key)
if key == Enum.KeyCode.F then
...
elseif key == Enum.KeyCode.E then
...
end -- repeat
end)
And, as a person who cares too much about optimisation, this pattern feels sacrilegious, a common way to avert this problem is to make a remote per keycode, so that the keys which are not being pressed are not being sent to the server (or, you send to the client or the client knows the keys which are bound to some function.)
Hereâs how a simple âequippingâ system may be done using Whisper.
--// SERVER \\--
local Whisper = require(game.ReplicatedStorage.Whisper)
local InputNamespace = Whisper.Namespace("InputDown", {
FireRoot = false;
})
local ToolNamespace = Whisper.Namespace("Tool", {
FireRoot = false;
})
local VFX = ToolNamespace:CreateTunnel("SpecialVFX") --// you need to do this to send data to the client
local Equipped = false;
local Tunnel = InputNamespace:ServerTunnel(Enum.KeyCode.F);
local SpecialConn;
InputNamespace:ServerTunnel(Enum.KeyCode.E):Connect(function(a0: Player)
Equipped = not Equipped;
if Equipped then
SpecialConn = Tunnel:Connect(function(a0: Player)
print('player used special')
ToolNamespace:FireClient(a0, "SpecialVFX")
end)
else
SpecialConn:Disconnect()
end
end)
--// CLIENT \\--
local Whisper = require(game.ReplicatedStorage.Whisper)
local Input = Whisper.Namespace("InputDown");
local ToolNamespace = Whisper.Namespace("Tool")
game.UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean)
if not gameProcessedEvent then
Input:FireServer(input.KeyCode);
end
end)
ToolNamespace:ClientTunnel('SpecialVFX'):Connect(function()
print('client received vfx.')
end)
note that the server tunnels become inactive after all connections have been disconnected, this is to stop the client from sending data to remotes which are not being used & also allows for some calls to get sunk (if FireRoot is disabled) to gain performance (this is the recommended option)
Features and Advanced Functionality
- Propagating signals:
- Having two tunnels which share the same identifiers such as like this:
These would both fire if the client did :FireServer(âType1â, âSpecificâ)Namespace:ServerTunnel("Type1"):Connect(...) Namespace:ServerTunnel("Type1", "Specific"):Connect(...)
- Typematched identifiers
- The module allows for identifiers to follow any type (but not the value of all types, just Instances, numbers, strings, bools and nil), For the time being there are 3 custom data types, they may be used as shown.
Namespace:ServerTunnel("Type1", Whisper.DataTypes.any(), "other"):Connect(...) -- this works to allow for the middle arg to be anything Namespace:ServerTunnel("Type1", Whisper.DataTypes.many(1,""), "other"):Connect(...) -- this would only work if the middle arg was either a number or a string (contents do not matter) Namespace:ServerTunnel("Type1", Whisper.DataTypes.ambigious(workspace), "other"):Connect(...) -- this would only work if the middle arg was a Instance, (the contents do not matter again)
- The âRequiredâ Datatype (Advanced)
- The custom datatypes / filters can be problematic, for example, if you wanted to have one connection where one value was either a string or a number, but another script expects that same connection tree to have a number (and requires its contents) you will get an âomittedâ argument, to fix this, you can label certain fields as ârequiredâ (you usually shouldnt need to use this). A scuffed example (I am aware this isnt valid because it calls ping twice, but it is for the sake of showcasing):
local Whisper = require(game.ReplicatedStorage.Whisper);
local Req = Whisper.Namespace('RequiredFieldTests')
Req:CreateTunnel('Pong')
Req:CreateTunnel('PongWithPayload')
Req:ServerTunnel('Ping', Whisper.DataTypes.ambigious(1)):Connect(function(Player: Player, ...: any)
Req:FireClient(Player, "Pong");
end) -- Ping without payload.
Req:ServerTunnel("Ping", Whisper.DataTypes.required()):Connect(function(Player: Player, ...: any)
local requiredFields = Whisper.PopRequiredArgs()
Req:FireClient(Player, "PongWithPayload", requiredFields[1]);
end) -- Ping with payload.
--// Client \\
local Whisper = require(game.ReplicatedStorage.Whisper);
local Req = Whisper.Namespace('RequiredFieldTests')
Req:FireServer('Ping', 1);
Req:ClientTunnel('Pong'):Connect(function(...)
print('got pong')
end)
Req:ClientTunnel('PongWithPayload'):Connect(function(...)
print('got pong payload', ...)
end)
- in this case, removing the .required() field and printing ⌠will show: (This value has been omitted due to filters, if this is unintended behaviour, you can specify this argument to be required by using the .required datatype.), upon readding the required field it should print ânilâ. To actually access the required variables, you need to do, PopRequiredArgs (best practice) or GetRequiredArgs , you can only use poprequiredargs once so saving its result is also good practice. (The reason why it is done like this is due to the fact that it is completely seperate from the arguments of the function, and it is a design decision made by me)
Overall, the best way to use this module is to use it with many namespaces, sticking to just global is fine but namespaces are great for isolating just a few remotes which allows the search algorithm to find the âshortestâ payload to send to any of the remotes.
https://create.roblox.com/store/asset/85744814956550/Whisper