roblox-networking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox networking

Roblox 网络

Pick the right channel

选择合适的通道

ChannelDeliveryUse for
RemoteEvent
Reliable, orderedRequests (client→server), game events, UI updates. The default.
UnreliableRemoteEvent
May drop or reorder; payload ≤ 1000 bytes (larger payloads are dropped)Continuous, superseded-by-next data: aim direction, cosmetic VFX, positions of non-physics objects.
RemoteFunction
Request/response, yields callerClient→server queries that need an answer (e.g. "can I buy X?").
Attributes / propertiesAutomatic replication server→clientPersistent state clients read (health, team, round state).
Physics replicationAutomatic for owned assembliesMoving parts; see
roblox-physics
.
Never use
RemoteFunction:InvokeClient
: if the client errors, disconnects, or never returns, the server throws or yields forever. Use a
RemoteEvent
to the client and another back if you need a reply.
通道交付方式用途
RemoteEvent
可靠、有序请求(客户端→服务器)、游戏事件、UI 更新。默认选项。
UnreliableRemoteEvent
可能会丢弃或乱序;载荷 ≤ 1000 字节(更大的载荷会被丢弃)连续发送且后续数据会取代前一份的数据:瞄准方向、纯视觉 VFX、非物理对象的位置。
RemoteFunction
请求/响应,调用方会 yield客户端→服务器的、需要返回结果的查询(例如“我能否购买 X?”)。
Attributes / properties服务器→客户端自动复制供客户端读取的持久状态(生命值、队伍、回合状态)。
Physics replication对已取得所有权的 assemblies 自动复制移动部件;参见
roblox-physics
。
绝不要使用
RemoteFunction:InvokeClient
:如果客户端报错、断开连接或永不返回,服务器会抛出异常或永久 yield。如需回复,请使用一个发往客户端的
RemoteEvent
,并在需要回复时再使用另一个发回服务器。

Defining remotes once (typed)

集中定义 remotes(类型化)

Create remotes on the server; clients wait for them. Centralize names so typos can't happen.
luau
--!strict
-- ReplicatedStorage/Shared/Net.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local FOLDER_NAME = "Remotes"
local IS_SERVER = RunService:IsServer()

local folder: Instance
if IS_SERVER then
	folder = ReplicatedStorage:FindFirstChild(FOLDER_NAME) or Instance.new("Folder")
	folder.Name = FOLDER_NAME
	folder.Parent = ReplicatedStorage
else
	folder = ReplicatedStorage:WaitForChild(FOLDER_NAME)
end

local function get<T>(className: string, name: string): T
	if IS_SERVER then
		local existing = folder:FindFirstChild(name)
		if existing then
			return existing :: any
		end
		local remote = Instance.new(className)
		remote.Name = name
		remote.Parent = folder
		return remote :: any
	end
	return folder:WaitForChild(name) :: any
end

return {
	RequestPurchase = get("RemoteEvent", "RequestPurchase") :: RemoteEvent,
	InventoryChanged = get("RemoteEvent", "InventoryChanged") :: RemoteEvent,
	AimDirection = get("UnreliableRemoteEvent", "AimDirection") :: UnreliableRemoteEvent,
	GetShopStock = get("RemoteFunction", "GetShopStock") :: RemoteFunction,
}
在服务器上创建 remotes;客户端等待它们创建完成。集中管理名称,从而避免拼写错误。
luau
--!strict
-- ReplicatedStorage/Shared/Net.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local FOLDER_NAME = "Remotes"
local IS_SERVER = RunService:IsServer()

local folder: Instance
if IS_SERVER then
	folder = ReplicatedStorage:FindFirstChild(FOLDER_NAME) or Instance.new("Folder")
	folder.Name = FOLDER_NAME
	folder.Parent = ReplicatedStorage
else
	folder = ReplicatedStorage:WaitForChild(FOLDER_NAME)
end

local function get<T>(className: string, name: string): T
	if IS_SERVER then
		local existing = folder:FindFirstChild(name)
		if existing then
			return existing :: any
		end
		local remote = Instance.new(className)
		remote.Name = name
		remote.Parent = folder
		return remote :: any
	end
	return folder:WaitForChild(name) :: any
end

return {
	RequestPurchase = get("RemoteEvent", "RequestPurchase") :: RemoteEvent,
	InventoryChanged = get("RemoteEvent", "InventoryChanged") :: RemoteEvent,
	AimDirection = get("UnreliableRemoteEvent", "AimDirection") :: UnreliableRemoteEvent,
	GetShopStock = get("RemoteFunction", "GetShopStock") :: RemoteFunction,
}

Server handler template

服务端处理程序模板

Every
OnServerEvent
/
OnServerInvoke
handler: rate limit → type-check every argument → check game rules → act. The first parameter is always the real
Player
(Roblox fills it in; it can't be forged).
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Net = require(ReplicatedStorage.Shared.Net)

local ITEM_PRICES: { [string]: number } = { Sword = 100, Shield = 150 }
local MIN_INTERVAL = 0.25
local lastRequest: { [Player]: number } = {}

Net.RequestPurchase.OnServerEvent:Connect(function(player: Player, itemId: unknown)
	local now = os.clock()
	if now - (lastRequest[player] or 0) < MIN_INTERVAL then
		return -- rate limited
	end
	lastRequest[player] = now

	if typeof(itemId) ~= "string" or #itemId > 64 then
		return -- malformed: never trust argument types
	end
	local price = ITEM_PRICES[itemId]
	if not price then
		return -- unknown item
	end
	-- check the player's server-side balance, deduct, grant, then notify:
	Net.InventoryChanged:FireClient(player, { added = itemId })
end)

game:GetService("Players").PlayerRemoving:Connect(function(player)
	lastRequest[player] = nil -- avoid leaking Player keys
end)
Full validation toolkit (NaN/inf checks, instance ownership, distance and cooldown checks):
roblox-security
.
每个
OnServerEvent
/
OnServerInvoke
处理程序都应遵循:速率限制 → 对每个参数进行类型检查 → 检查游戏规则 → 执行操作。第一个参数始终是真实的
Player
(由 Roblox 填充,客户端无法伪造)。
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Net = require(ReplicatedStorage.Shared.Net)

local ITEM_PRICES: { [string]: number } = { Sword = 100, Shield = 150 }
local MIN_INTERVAL = 0.25
local lastRequest: { [Player]: number } = {}

Net.RequestPurchase.OnServerEvent:Connect(function(player: Player, itemId: unknown)
	local now = os.clock()
	if now - (lastRequest[player] or 0) < MIN_INTERVAL then
		return -- rate limited
	end
	lastRequest[player] = now

	if typeof(itemId) ~= "string" or #itemId > 64 then
		return -- malformed: never trust argument types
	end
	local price = ITEM_PRICES[itemId]
	if not price then
		return -- unknown item
	end
	-- check the player's server-side balance, deduct, grant, then notify:
	Net.InventoryChanged:FireClient(player, { added = itemId })
end)

game:GetService("Players").PlayerRemoving:Connect(function(player)
	lastRequest[player] = nil -- avoid leaking Player keys
end)
完整验证工具包(NaN/inf 检查、Instance 所有权、距离和冷却检查):
roblox-security
。

What survives the wire

哪些数据可以通过网络传输

  • Tables are copied: identity and metatables are lost; functions become
    nil
    .
  • Don't mix array and dictionary keys in one table; don't put
    nil
    holes in arrays.
  • Non-string dictionary keys (Instances, numbers in sparse tables) are converted to strings — send arrays of pairs instead.
  • Instances arrive as
    nil
    if the receiver can't see them (e.g.
    ServerStorage
    items, or parts not yet streamed in on the client).
  • Supported:
    nil
    , booleans, numbers, strings, tables,
    buffer
    , Roblox datatypes (
    Vector3
    ,
    CFrame
    ,
    Color3
    ,
    EnumItem
    , ...), and replicated
    Instance
    references.
  • Server→client ordering vs. property replication is not guaranteed: a remote may arrive before or after a property change it relates to.
  • 表会被复制:对象标识和元表会丢失;函数会变成
    nil
    。
  • 不要在同一个表中混用数组键和字典键;不要在数组中留下
    nil
    空洞。
  • 作为字典键的非字符串值(
    Instance
    、稀疏表中的数字)会被转换为字符串——应改为发送由键值对组成的数组。
  • 如果接收方看不到某个
    Instance
    ,它到达时会是
    nil
    (例如
    ServerStorage
    中的物品,或客户端尚未流入的部件)。
  • 支持:
    nil
    、布尔值、数字、字符串、表、
    buffer
    、Roblox 数据类型(
    Vector3
    、
    CFrame
    、
    Color3
    、
    EnumItem
    等)以及可复制的
    Instance
    引用。
  • 服务器→客户端的远程消息到达顺序与属性复制顺序不保证一致:远程消息可能先于关联的属性变更到达,也可能后于它到达。

Bandwidth and CPU

带宽与 CPU

  • Send changes, not full state (item added, not the whole inventory). Send on change, not per frame.
  • Throttle input-driven sends on the client (e.g. aim at most 20 Hz) and still rate-limit on the server.
  • Send IDs/enums as small numbers; quantize floats; pack hot, high-frequency data into a
    buffer
    .
  • Batch many small messages into one per frame (queue in a table, flush on
    Heartbeat
    ).
  • Server-side
    TweenService
    replicates every frame and looks jittery: tween on clients instead.
  • VFX: the server decides the outcome and fires a minimal event; each client spawns effects locally.
  • Profile with the MicroProfiler (
    ProcessPackets
    ,
    Allocate Bandwidth and Run Senders
    ) and the Developer Console Network tab.
luau
--!strict
-- Pack a hit notification into 11 bytes instead of a table with string keys.
local function encodeHit(targetId: number, damage: number, position: Vector3): buffer
	local b = buffer.create(11)
	buffer.writeu32(b, 0, targetId)
	buffer.writeu16(b, 4, math.clamp(math.round(damage), 0, 65535))
	buffer.writei16(b, 6, math.clamp(math.round(position.X), -32768, 32767))
	buffer.writei16(b, 8, math.clamp(math.round(position.Z), -32768, 32767))
	buffer.writeu8(b, 10, math.clamp(math.round(position.Y / 4), 0, 255))
	return b
end

local function decodeHit(b: buffer): (number, number, Vector3)
	return buffer.readu32(b, 0),
		buffer.readu16(b, 4),
		Vector3.new(buffer.readi16(b, 6), buffer.readu8(b, 10) * 4, buffer.readi16(b, 8))
end

print(decodeHit(encodeHit(42, 25, Vector3.new(10, 20, 30))))
For many remotes with hot paths, schema-driven generators (Zap, Blink) produce typed, validated, buffer-packed remotes automatically. Adopt them when profiling shows network cost.
  • 发送变更,而不是完整状态(发送“新增了物品”,而不是整个背包)。仅在状态变化时发送,不要每帧发送。
  • 在客户端限制输入驱动发送的频率(例如瞄准数据最多以 20 Hz 发送),并且服务端仍要执行速率限制。
  • 将 ID/enum 编码为较小的数字;量化浮点数;将高频热数据打包到
    buffer
    中。
  • 每帧将许多小消息批处理为一条消息(用表进行排队,在
    Heartbeat
    时刷新)。
  • 服务端的
    TweenService
    会逐帧复制,视觉上容易抖动:应改为在客户端执行补间动画。
  • VFX:服务器决定结果并触发最小化事件;每个客户端在本地生成特效。
  • 使用 MicroProfiler(
    ProcessPackets
    、
    Allocate Bandwidth and Run Senders
    )和 Developer Console 的 Network 选项卡进行性能分析。
luau
--!strict
-- Pack a hit notification into 11 bytes instead of a table with string keys.
local function encodeHit(targetId: number, damage: number, position: Vector3): buffer
	local b = buffer.create(11)
	buffer.writeu32(b, 0, targetId)
	buffer.writeu16(b, 4, math.clamp(math.round(damage), 0, 65535))
	buffer.writei16(b, 6, math.clamp(math.round(position.X), -32768, 32767))
	buffer.writei16(b, 8, math.clamp(math.round(position.Z), -32768, 32767))
	buffer.writeu8(b, 10, math.clamp(math.round(position.Y / 4), 0, 255))
	return b
end

local function decodeHit(b: buffer): (number, number, Vector3)
	return buffer.readu32(b, 0),
		buffer.readu16(b, 4),
		Vector3.new(buffer.readi16(b, 6), buffer.readu8(b, 10) * 4, buffer.readi16(b, 8))
end

print(decodeHit(encodeHit(42, 25, Vector3.new(10, 20, 30))))
对于 remotes 数量较多且存在高频热路径的场景,基于 schema 的生成器(Zap、Blink)可以自动生成类型化、经过验证且采用
buffer
打包的 remotes。当性能分析显示网络成本较高时,应采用这些生成器。

Time and latency

时间与延迟

  • workspace:GetServerTimeNow()
    is a synchronized clock on server and clients: send a start timestamp and let clients compute progress locally (round timers, cooldown bars, projectiles).
  • player:GetNetworkPing()
    returns round-trip network latency in seconds (clients may only query
    LocalPlayer
    ); use it to size lag-compensation windows.
  • Don't send countdown ticks every second; send the end time once.
More patterns (request/response with timeouts, state snapshots + deltas, per-frame batching, client prediction): references/patterns.md.
  • workspace:GetServerTimeNow()
    是服务器与客户端共享的同步时钟:发送起始时间戳,让客户端在本地计算进度(回合计时器、冷却条、投射物)。
  • player:GetNetworkPing()
    返回以秒为单位的往返网络延迟(客户端只能查询
    LocalPlayer
    );使用它来确定滞后补偿窗口的大小。
  • 不要每秒发送倒计时 tick;只需发送一次结束时间。
更多模式(带超时的请求/响应、状态快照 + 增量、逐帧批处理、客户端预测):references/patterns.md。

Related skills

相关 skills

roblox-security
(validation, anti-exploit),
roblox-architecture
(where remotes live),
roblox-physics
(network ownership, server authority),
roblox-performance
.
roblox-security
(验证、防作弊)、
roblox-architecture
(remotes 应放在哪里)、
roblox-physics
(网络所有权、服务器权威)、
roblox-performance
。