Example
Gun Framework
A complete semi-automatic rifle implementation. Server validates hits, Stream syncs player positions at 20 Hz, Broadcast fires unreliable hit FX, and the combat Port rate limits to the weapon's fire rate.
This example accompanies the YouTube walkthrough. The video shows how to build this from scratch.
File structure
ServerScriptService/
CombatServer.luau ← Port, hit validation, damage
MovementServer.luau ← Stream subscribe, position tracking
ReplicatedStorage/
RoExpress/ ← framework
Schemas/
Movement.luau ← shared Stream schema
StarterPlayerScripts/
GunClient.luau ← shoot request, movement send
MovementClient.luau ← Stream send loop
FXClient.luau ← Broadcast hit effects
Shared: Movement schema
-- ReplicatedStorage/Schemas/Movement.luau
local RoExpress = require(game.ReplicatedStorage.RoExpress)
return RoExpress.Stream:Schema({
cframe = "CFrame", -- 24 bytes with delta
velocity = "Vector3", -- 12 bytes
health = "uint8", -- 1 byte
flags = "uint8", -- 1 byte (sprinting, crouching, reloading)
})
Server: CombatServer.luau
local RoExpress = require(game.ReplicatedStorage.RoExpress)
local app = RoExpress.GetApp()
local broadcast = RoExpress("Broadcast")
local MAX_RANGE = 300
local function validateShot(shooter, data)
local target = game.Players:GetPlayerByUserId(data.targetId)
if not (target and target.Character) then return nil end
local hrp = target.Character:FindFirstChild("HumanoidRootPart")
if not hrp then return nil end
local dist = (data.origin - hrp.Position).Magnitude
if dist > MAX_RANGE then return nil end
return target, hrp.Position
end
app:Listen("combat", function(port)
port:Post("shoot", function(req, res)
local target, hitPos = validateShot(req.player, req.data)
if not target then
res:Send({ hit = false })
return
end
local hum = target.Character:FindFirstChildOfClass("Humanoid")
if hum then
hum.Health -= 25
broadcast:EmitAll("hitFX", { pos = hitPos, dmg = 25 })
end
res:Send({ hit = true, damage = 25 })
end)
port:Post("reload", function(req, res)
-- server-side reload validation could go here
res:Send()
end)
end, { max = 10, refill = 8, cost = 1 })
Server: MovementServer.luau
local RoExpress = require(game.ReplicatedStorage.RoExpress)
local MovSchema = require(game.ReplicatedStorage.Schemas.Movement)
local channel = RoExpress.Stream:Channel("movement", MovSchema)
local playerPos = {} -- { [player] = CFrame }
channel:Subscribe(function(player, data)
playerPos[player] = data.cframe
end)
game.Players.PlayerRemoving:Connect(function(p)
playerPos[p] = nil
end)
Client: GunClient.luau
local RoExpress = require(game.ReplicatedStorage.RoExpress)
local network = RoExpress.GetNetwork()
local UserInput = game:GetService("UserInputService")
local ammo = 30
UserInput.InputBegan:Connect(function(input, gp)
if gp or input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
if ammo <= 0 then return end
ammo -= 1
local targetId = getAimedPlayerId() -- raycast logic
network:Post("combat/shoot", {
origin = workspace.CurrentCamera.CFrame.Position,
targetId = targetId,
}, function(res)
if res.data.hit then
showHitmarker()
end
end)
end)
Client: FXClient.luau
local listener = RoExpress("Listener")
listener:On("hitFX", function(data)
-- Unreliable | a missed packet just means no particle
spawnBloodParticle(data.pos)
showDamageNumber(data.pos, data.dmg)
end)
See also
Combat Guide | explanation of the architecture · Ports · Stream · Broadcast