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