Installation
Pigeon is one folder of Luau files. Put it in ReplicatedStorage and
require it. There is nothing to build and nothing else to install.
What you are copying
The whole library is src/Pigeon. Six files, no dependencies beyond
Roblox itself.
| File | What it is |
|---|---|
init.luau | The module you require. Becomes the ModuleScript named Pigeon. |
Carrier.luau | Carriers: the named channels you send and listen on. |
Network.luau | The pooled RemoteEvents and the transport underneath. |
Transformer.luau | Transformers: which pooled remote traffic rides on, and group teardown. |
StagedTable.luau | Staged tables: plain tables whose writes replicate. |
Types.luau | The Luau types Pigeon exports. |
Pigeon does not pull in any other library. It only asks Roblox for
Players, RunService, ReplicatedStorage and
HttpService.
Where the folder goes
ReplicatedStorage. Both machines require the same module, so it has
to be somewhere both of them can see, and ReplicatedStorage is that
place.
Careful
Do not put Pigeon in ServerScriptService or
ServerStorage. The client cannot see either one, so the client half
of every channel would have nothing to require. StarterPlayerScripts
has the same problem in reverse.
With Rojo
Copy src/Pigeon into your project and map it into
ReplicatedStorage. This is the smallest project file that does it.
{
"name": "MyGame",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Pigeon": {
"$path": "src/Pigeon"
}
}
}
}
The key on the left is the Instance name, so "Pigeon" is what you
will require. The $path on the right is where the files live on
disk. Rojo sees init.luau inside the folder and turns the folder
itself into a ModuleScript, with the other five files as its children.
Here is the same mapping inside a fuller project, next to your own code.
{
"name": "MyGame",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Pigeon": {
"$path": "src/Pigeon"
},
"Shared": {
"$path": "src/shared"
}
},
"ServerScriptService": {
"Server": {
"$path": "src/server"
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"Client": {
"$path": "src/client"
}
}
}
}
}
Without Rojo
You can build the same tree by hand in Studio. The names have to match exactly, because the files require each other by name.
- Make a ModuleScript in
ReplicatedStorageand name itPigeon. - Paste the contents of
init.luauinto it. - Make five ModuleScripts inside it, named
Types,Network,Transformer,StagedTableandCarrier. - Paste the matching file into each one.
The finished tree looks like this.
ReplicatedStorage
└── Pigeon (ModuleScript, from init.luau)
├── Carrier
├── Network
├── StagedTable
├── Transformer
└── Types
String requires
Four of the five inner files require their neighbours by string, like this:
-- inside Carrier.luau
local types = require("./Types")
local Network = require("./Network")
"./Types" means "the module called Types next to me". That is a
string require, and your place has to allow it. If string requires are off, the
first inner require errors and Pigeon will not load.
init.luau is the exception. It uses ordinary Instance requires, so
the entry point itself does not depend on the setting:
-- inside init.luau
local Types = require(script.Types)
local Network = require(script.Network)
Note
If you cannot turn string requires on, you can swap them yourself. Every
require("./Name") means exactly the same thing as
require(script.Parent.Name). Types.luau has no
requires at all, so there are only four files to touch, and Pigeon then works
without the setting.
Requiring it
From a server Script, usually in ServerScriptService:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
local shop = Pigeon.new("Shop")
shop:On("Buy", function(player, itemId)
shop:BroadcastTo({ player }, "Stock", itemId, 3)
end)
From a LocalScript, usually in StarterPlayerScripts:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage:WaitForChild("Pigeon"))
local shop = Pigeon.new("Shop")
shop:On("Stock", print)
shop:Init()
shop:Emit("Buy", "sword")
The server handler gets the sending player first. The client handler does not, because on the client there is only ever one sender.
Note
Use WaitForChild on the client. Your LocalScript can run before
the Pigeon folder has replicated, and plain ReplicatedStorage.Pigeon
errors when it has not arrived yet. On the server it is already there, so the
plain index is fine.
What Pigeon adds at runtime
The server creates one Folder called PigeonRefs in
ReplicatedStorage and puts the pooled remotes inside it. They are
named PigeonRef_1, PigeonRef_2 and so on, each with a
matching PigeonRefU_1 unreliable twin. The folder also carries a
RefCount attribute so clients know how big the pool is.
ReplicatedStorage
└── PigeonRefs (Folder, created by the server)
├── PigeonRef_1 (RemoteEvent)
└── PigeonRefU_1 (UnreliableRemoteEvent)
Careful
Do not create, rename or delete anything in PigeonRefs yourself.
Pigeon owns that folder. Remove or rename a ref and everything that needed it
is held on the client for 30 seconds and then dropped, and the channels riding
on it never open.
Checking it works
Drop these two scripts in, press play, and look at the output. If you see the message on the client, everything is wired up.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
local test = Pigeon.new("InstallCheck")
Players.PlayerAdded:Connect(function(player)
test:BroadcastTo({ player }, "Hello", player.Name)
end)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage:WaitForChild("Pigeon"))
local test = Pigeon.new("InstallCheck")
test:On("Hello", function(name)
print("Pigeon works. Hello,", name)
end)
test:Init()
The server fires on PlayerAdded, which almost certainly happens
before your LocalScript has finished loading. It still arrives, because the
server holds it until the client calls Init. That is
Startup and Buffering doing its job on your first run.
Next
- Getting Started builds a real channel step by step.
- Carriers covers the object you use every day.