mcpField
R
roblox-architecture

roblox-architecture

github.com/el4cteo开发87812 次安装4 天前更新
代码仓库

关于 roblox-architecture

roblox-architecture 是用于组织 Roblox 游戏服务器端与客户端代码和状态的开发技能,介绍项目布局、生命周期处理、标签组件、状态共享及流式加载实践。

主要功能

  1. 01服务器端与客户端代码布局
  2. 02Rojo 风格的项目结构
  3. 03服务与控制器启动方式
  4. 04玩家和角色生命周期处理
  5. 05标签组件与属性的使用
  6. 06状态共享和流式加载指引

适用场景

  1. 01规划 Roblox 项目的初始结构
  2. 02组织服务器端与客户端系统
  3. 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 中的 Client Script 会在每个客户端上运行)——所以你克隆的模板 不能无意中包含此类脚本。Legacy Script 不会在 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。

评论

支持 Markdown