Skip to content

第 4 章:Game 与 Services ​

通过 game:GetService 获取公开服务,并按运行端和返回值安全使用。

开始前与本章路线 ​

  • 准备:先生成脚本工程;玩家示例需要进入试玩并有玩家在线。
  • 安装范围:按代码块标注的端替换对应 main.lua;一次只做一种查询。
  • 本次要看到:server 的 IsServer 为 true;client 的 IsClient 为 true;就绪后能读到本地玩家。

停止试玩后再换入口;这些章节是能力实验,第 22 章才提供统一的六文件项目。每次只改变一个条件,并保留本次输出用于对照。

你会学到什么 ​

  • game 为什么是运行时入口。
  • 如何用 game:GetService("服务名") 获取服务。
  • Realm(可用端)和推荐运行端为什么不是一回事。
  • 如何查服务 API,避免凭名字猜方法。
  • 如何把 Service 获取写成可维护的入口代码。

game 是全局入口 ​

game 是引擎默认注入的全局对象,不需要创建,也不需要 require。获取服务统一使用:

示例 1|独立实验;整体替换 server/main.lua。 本次演示端:server。

lua
-- @runtime server
local World = game:GetService("World")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

Service 是全局单例和功能接口,不能 .New(),也不能 CreateUnit("World")。CreateUnit 用来创建 Unit,不用来创建 Service。

Realm 与推荐运行端 ​

公开 API 页面会标出每个 Service 的 Realm(可用端):

  • common:client 和 server 都能获取这个 Service。
  • server:只能在 server 使用。
  • client:只能在 client 使用。

但 Realm 只说明“这个类型在哪端可用”,不等于“业务上应该在哪端调用”。例如 SocialService 的类型 Realm 是 common,但 GetPartyAsync 的公开说明明确标注仅服务端使用;CameraService 是 common,但屏幕坐标转换通常服务于客户端画面。遇到这种情况,要继续阅读具体成员的说明,不能只看类型级 Realm。

常用服务速查 ​

服务Realm新手推荐用法
Worldcommonserver 创建/销毁场景 Unit;双端可读服务器时间
Playerscommonserver 管理玩家和反查角色;client 读取 LocalPlayer
RunServicecommon双端判断环境、监听 Heartbeat
Taskcommon双端启动协程、延迟、等待
TimerServicecommon需要固定次数计时器时使用
UserInputServiceclientclient 读取键盘、鼠标、触屏输入
CameraServicecommonclient 做屏幕坐标转换和取射线
PhysicsServicecommonserver 权威物理查询更常见;Raycast 要传方向向量
SoundServicecommonclient 播本地反馈,server 播所有人都应感知的世界声音
DataStoreServiceserverserver-only,所有 Async 调用都要 pcall
AnalyticsServiceserverserver 记录玩家行为和经济/进度事件
MessageServiceserver跨服消息按 Meta 契约用 PublishAsync / SubscribeAsync
TeleportServiceserver传送调用放 server,Async 接口进协程并 pcall
TextChatServicecommon管理文本频道和发送消息;具体调用端按成员说明和业务权威边界决定
SocialServicecommon当前公开成员用于队伍查询;GetPartyAsync 按说明放在 server
CommodityServicecommon查询商品/权益或打开商品界面;消耗和奖励结果由 server 校验

这张表不是完整 API,只是学习路线图。具体函数、属性、事件和 Realm 以当前公开 API 页面为准;页面没有公开的成员,一律当作当前不可用。

最小示例:获取服务并判断运行端 ​

运行端:server

示例 2|独立实验;整体替换 server/main.lua。 本次演示端:server。

lua
-- @runtime server
local World = game:GetService("World")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

if not World or not Players or not RunService then
    print("[SE Lua Guide] ERROR: 必需服务获取失败")
    return
end

print("[SE Lua Guide] World:", World)
print("[SE Lua Guide] Players:", Players)
print("[SE Lua Guide] IsServer:", RunService:IsServer())
print("[SE Lua Guide] ServerTime:", World:GetServerTime())

运行端:client

示例 3|独立实验;整体替换 client/main.lua。 本次演示端:client。

lua
-- @runtime client
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local World = game:GetService("World")

if not Players or not RunService or not World then
    print("[SE Lua Guide] ERROR: 必需服务获取失败")
    return
end

print("[SE Lua Guide] IsClient:", RunService:IsClient())
print("[SE Lua Guide] LocalPlayer:", Players.LocalPlayer)
print("[SE Lua Guide] SyncedTime:", World:GetServerTime())

遍历玩家:server 和 client 的差别 ​

server 侧适合维护权威在线列表和广播回合状态。下例在 client 只读当前可见玩家并打印变化,不执行权威管理。当前公开 Camps / Camp 能查询阵营和监听成员变化,但不能主动分配阵营;完整边界和兼容写法见第 7 章:

示例 4|独立实验;整体替换 client/main.lua。 本次演示端:client。

lua
-- @runtime client
local Players = game:GetService("Players")

if not Players then
    print("[SE Lua Guide] ERROR: Players 服务获取失败")
    return
end

local allPlayers = Players:GetPlayers()
print("[SE Lua Guide] 当前在线人数:", #allPlayers)

for _, player in ipairs(allPlayers) do
    print("[SE Lua Guide] 玩家:", player:GetName())
end

Players.PlayerAdded:Connect(function(player)
    print("[SE Lua Guide] 玩家加入:", player:GetName())
end)

Players.PlayerRemoving:Connect(function(player)
    print("[SE Lua Guide] 玩家离开:", player:GetName())
end)

client 侧通常只关心本地玩家:

示例 5|独立实验;整体替换 client/main.lua。 本次演示端:client。

lua
-- @runtime client
local Players = game:GetService("Players")

if not Players then
    print("[SE Lua Guide] ERROR: Players 服务获取失败")
    return
end

local localPlayer = Players.LocalPlayer
if localPlayer then
    print("[SE Lua Guide] 本地玩家:", localPlayer:GetName())
end

API 查阅顺序 ​

写 Service 代码时,不要只看方法名猜。推荐顺序:

  1. 先打开本站当前版本的公开 API 页面,确认类型、成员名、参数、返回值和 Realm。
  2. 再用编辑器的 API 搜索或补全交叉确认,并在正确运行端做最小验证。
  3. 官方 Demo 和本教程只用于学习组织方式;旧代码与当前 API 页面冲突时,不要照抄。
  4. 如果一个能力没有出现在当前公开 API 中,就把它视为尚未开放,不用内部名字或历史写法绕过限制。

典型例子:旧清单里可能写 MessageService.Publish / Subscribe,但当前公开 API 只提供 PublishAsync / SubscribeAsync。这时必须使用当前名称,并按异步失败可能性做好保护。

常见错误 ​

错误:Realm 是 common,就在两端随便做权威逻辑 ​

World、Players、PhysicsService 都可能是 common,但计分、奖励、存储、胜负判定仍应放 server。client 可以请求和显示,不能成为权威。

错误:在 client 调 DataStoreService ​

DataStoreService 是 server-only。排行榜、背包、金币、最高分都必须通过 server 读写,然后用 RemoteEvent 同步给 client 显示。

错误:凭服务名猜 API ​

不要把 MessageService 写成 Publish,也不要把 TeleportService 当成同步接口。带 Async 的方法应放进允许等待的任务中,并用 pcall 捕获失败。

错误:每个模块都到处 GetService ​

小示例可以直接写;项目变大后,Manager 可以在 main.lua 里注入依赖,或者在模块顶部集中获取。不要在每个函数里重复获取同一个 Service。

练习任务 ​

  1. 在 server 打印 World、Players、RunService,并输出当前在线人数。
  2. 在 client 打印 Players.LocalPlayer 和 RunService:IsClient()。
  3. 查公开 API 页面,确认 DataStoreService、UserInputService、MessageService 的 Realm。
  4. 找出一个“Realm 是 common,但推荐在 server 做权威逻辑”的服务,并解释原因。

练习参考与复核 ​

先完成练习,再展开对照;没有运行条件的项目填写“未验证”和缺少的条件。

参考思路与应观察的结果

DataStoreService 和 MessageService 属于 server,UserInputService 属于 client。World 可双端获取,但权威得分与物体复位应由 server 执行。启动时 LocalPlayer 为 nil 是未就绪,不能记录为“玩家查询通过”。

  • [ ] 我已完成本章选定实验,记录了上面的预期现象。
  • [ ] 我能故意改变一个前置条件,解释失败发生在哪一步,并在恢复后重跑。

本章验收标准 ​

  • [ ] 我知道 game:GetService("服务名") 的用法。
  • [ ] 我知道 Realm 只表示可用端,不等于业务推荐端。
  • [ ] 我能区分 Players:GetPlayers() 和 Players.LocalPlayer 的典型使用场景。
  • [ ] 我知道 Service 方法要查当前公开 API,不凭名字猜。
  • [ ] 我知道 Async 服务调用需要协程和 pcall。

本章产物 ​

本章产物是服务获取和端归属判断。第 22 章服务端会拿 World、Players、RunService、Task,客户端会拿 Players、World、RunService,每个 Service 都有明确运行端。

本章 API 对照 ​

下一章预告 ​

你掌握了服务获取和端归属。下一章我们将深入 Unit:编辑器里的对象在 Lua 中如何查找、创建、挂载和销毁。