关于 roblox-architecture
roblox-architecture 是用于组织 Roblox 游戏服务器端与客户端代码和状态的开发技能,介绍项目布局、生命周期处理、标签组件、状态共享及流式加载实践。
主要功能
- 01服务器端与客户端代码布局
- 02Rojo 风格的项目结构
- 03服务与控制器启动方式
- 04玩家和角色生命周期处理
- 05标签组件与属性的使用
- 06状态共享和流式加载指引
适用场景
- 01规划 Roblox 项目的初始结构
- 02组织服务器端与客户端系统
- 03处理流式世界内容和玩家状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-architecture安装后在 Claude Code 中输入/roblox-architecture确认已加载。
附带文件references/ecosystem.mdreferences/streaming.md
详细介绍
Roblox 游戏架构
一段话概括模型
每个 Roblox 游戏都是一个 服务端(权威端,运行 Script,能看到一切,拥有 DataStore)
再加上每位玩家一个 客户端(运行 LocalScript / 客户端 Script,负责渲染、读取输入)。
服务端的 DataModel 会自动 复制 到客户端。客户端的更改 不会 复制
回服务端,除了:客户端拥有网络所有权的部件物理(默认是其角色)、
其自身的 Humanoid 状态/动画,以及远程调用。假设每个客户端都已被攻破
(见 roblox-security)。将每个功能都设计为:客户端请求 → 服务端验证并
决策 → 状态复制 → 客户端渲染。
各类内容放在哪里
| 容器 | 运行/可见于 | 放置内容 |
|---|---|---|
ServerScriptService | 仅服务端 | 服务端 Script 和仅服务端的 ModuleScript(游戏逻辑、数据、反作弊)。 |
ServerStorage | 仅服务端 | 仅服务端的资产:地图、NPC 模板、待克隆的工具、涉及机密的配置。 |
ReplicatedStorage | 两端 | 共享模块(类型、配置、纯工具函数)、RemoteEvent、客户端克隆的资产。 |
ReplicatedFirst | 客户端,最先 | 仅加载画面。在其他一切之前复制。 |
StarterPlayer.StarterPlayerScripts | 客户端(一次性复制到 PlayerScripts) | 客户端入口点和控制器。 |
StarterPlayer.StarterCharacterScripts | 客户端(每次生成时复制到角色中) | 按角色运行的客户端脚本(很少需要)。 |
StarterGui | 客户端(复制到 PlayerGui) | ScreenGui。除非你希望死亡时重置,否则设置 ResetOnSpawn = false。 |
Workspace | 两端(流式传输到客户端) | 仅 3D 世界。不是放代码或模板的地方。 |
Lighting、SoundService、Teams、TextChatService | 两端 | 各自对应的配置对象。 |
规则:
ReplicatedStorage中的代码 可被漏洞利用者下载。绝不要将服务端逻辑、机密、 管理员名单或反作弊阈值放在那里。Script.RunContext:Legacy(默认:行为取决于容器)、Server、Client或Plugin。非 Legacy 的 RunContext 会让Script无论位于哪个容器 都运行(例如ReplicatedStorage中的ClientScript 会在每个客户端上运行)——所以你克隆的模板 不能无意中包含此类脚本。LegacyScript不会在ReplicatedStorage/ServerStorage中运行。
项目布局(推荐 Rojo 风格)
src/
server/ -> ServerScriptService.Server (init.server.luau = entry point)
Services/ -> one ModuleScript per system (DataService, CombatService, ...)
client/ -> StarterPlayer.StarterPlayerScripts.Client (init.client.luau)
Controllers/ -> one ModuleScript per client system (InputController, UIController, ...)
shared/ -> ReplicatedStorage.Shared (types, config, pure logic, network definitions)
Packages/ -> ReplicatedStorage.Packages (Wally/pesde dependencies)
ServerPackages/ -> ServerScriptService.ServerPackages
每一端一个入口脚本,其余都是 ModuleScript。这样可以获得确定性的加载顺序、
可测试的模块,以及一个统一连接依赖的地方。Roblox 自己的推荐也与之等效:
在 ServerScriptService 中放一个 Script(RunContext = Server),在
ReplicatedStorage 中放一个 Script(RunContext = Client),各自 require 模块并调用其 start()。工具链设置:roblox-tooling。
引导服务(无需框架)
--!strict
-- ServerScriptService/Server/init.server.luau
type Service = {
init: ((self: Service) -> ())?, -- sync setup; may reference other services; must not yield
start: ((self: Service) -> ())?, -- runs after every init; may yield / connect events
}
local servicesFolder = script:WaitForChild("Services")
local services: { [string]: Service } = {}
for _, module in servicesFolder:GetChildren() do
if module:IsA("ModuleScript") then
services[module.Name] = (require :: any)(module)
end
end
for name, service in services do
if service.init then
debug.setmemorycategory(name)
service:init()
debug.resetmemorycategory()
end
end
for name, service in services do
if service.start then
task.spawn(function()
debug.setmemorycategory(name)
service:start()
end)
end
end
- 服务之间通过普通的
require相互引用(无循环)或在init中接收依赖。 - 客户端用
Controllers镜像此结构。保持名称对称(ShopService/ShopController)。 - 为此并不需要 Knit 之类的框架;如果项目已经在使用某个框架,请遵循其约定。
玩家与角色生命周期
--!strict
local Players = game:GetService("Players")
local function onCharacterAdded(player: Player, character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
humanoid.Died:Once(function()
print(`{player.Name} died`)
end)
end
local function onPlayerAdded(player: Player)
-- Load data first (see roblox-data-stores), then set up character handling.
if player.Character then
task.spawn(onCharacterAdded, player, player.Character)
end
player.CharacterAdded:Connect(function(character)
onCharacterAdded(player, character)
end)
end
local function onPlayerRemoving(player: Player)
-- Save data, release session locks, clear every table keyed by this player.
print(`{player.Name} left`)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
for _, player in Players:GetPlayers() do -- players who joined before this script connected
task.spawn(onPlayerAdded, player)
end
game:BindToClose(function()
-- Server shutdown: flush saves for remaining players (max ~30 s total).
end)
- 始终处理代码启动时已存在的玩家/角色。
- 角色会重生:任何绑定到角色的内容都必须在
CharacterAdded时重新创建。 - 在客户端,角色的子对象可能尚不存在(流式传输):对它们使用
WaitForChild。
带有 CollectionService 标签的组件
为实例添加标签(通过 Studio 的 Tags 部分或 CollectionService:AddTag),并在代码中附加行为。
相比将脚本复制到每个模型中,这种方式更易扩展;同时也支持流式加载,因为实例流入和流出时会触发添加/移除信号。
--!strict
local CollectionService = game:GetService("CollectionService")
local TAG = "KillBrick"
local cleanups: { [Instance]: () -> () } = {}
local function onAdded(instance: Instance)
if not instance:IsA("BasePart") then
return
end
local connection = instance.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
cleanups[instance] = function()
connection:Disconnect()
end
end
local function onRemoved(instance: Instance)
local cleanup = cleanups[instance]
if cleanup then
cleanup()
cleanups[instance] = nil
end
end
CollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)
CollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)
for _, instance in CollectionService:GetTagged(TAG) do
task.spawn(onAdded, instance)
end
使用属性(instance:GetAttribute("Damage")、
GetAttributeChangedSignal)配置组件,而不是使用 IntValue/StringValue 子项。属性会从服务器复制到客户端,开销更低,并且会显示在 Properties 面板中。
共享状态
| 需求 | 使用 |
|---|---|
| 客户端可见的少量实例/玩家级值 | 实例/Player 上的属性(由服务器设置)。 |
| 事件和请求 | RemoteEvent/UnreliableRemoteEvent(参见 roblox-networking)。 |
| 大量或每位玩家私有的状态(物品栏、任务) | 服务器持有的表格 + 仅向对应玩家发送快照/差异的远程事件。 |
| 同一侧通信 | 模块函数调用或类型化 Signal 模块,而不是 BindableEvent、_G 或 shared。 |
| 持久化数据 | roblox-data-stores。跨服务器通信:MessagingService/MemoryStore。 |
流式加载(新场景默认启用)
启用 Workspace.StreamingEnabled 后,客户端只能获取附近的部件。客户端代码必须:
- 对
Workspace中的内容使用WaitForChild(可能永远不会出现时应设置超时),或检查是否为 nil。 - 对需要同时加载其部件的模型使用
ModelStreamingMode = Atomic; 仅当模型必须始终存在时才使用Persistent(会占用内存)。 - 将流出范围的部件视为部分可用/可能过时的数据,不要依赖其
GetChildren、射线检测和属性读取;完整世界查询应在服务器上执行。 - 传送角色前,在服务器上使用
player:RequestStreamAroundAsync(position)预先加载目的地。 - 瞄准镜、竞速、自由视角:
player.FrustumStreaming = Enum.FrustumStreamingMode.Automatic会流式加载摄像机可见的内容,而不是扩大半径。
完整的模式目录和推荐的 Workspace 设置:references/streaming.md。
反模式
- 在部件/模型中放置大量零散的
Script(改用标签 + 一个服务)。 - 使用轮询循环(
while task.wait(0.1) do check() end),而不是事件或带deltaTime的Heartbeat。 - 信任客户端提供的游戏状态,或每帧触发远程事件。
- 使用
_G/shared全局变量、循环 require,或在模块顶层让代码产生让步。 - 从客户端将地图克隆到
Workspace(仅客户端的副本不会收到服务器更新)。 - 将每位玩家的数据存储在角色中(角色死亡时会被销毁)。
库生态系统(信号、清理、网络、UI、ECS、数据):references/ecosystem.md。
相关技能
roblox-networking、roblox-security、roblox-data-stores、roblox-tooling、roblox-performance、roblox-game-systems。