主题
第 7 章:Players、输入与角色
获取玩家与角色、处理输入和阵营信息,并兼容不同运行包的集合形态。
开始前与本章路线
- 准备:先完成双端日志;输入实验需让试玩窗口获得焦点,角色实验需玩家已入场。
- 安装范围:每段独立运行;“反查帮助函数”只是定义,需在真实碰撞回调中调用。
- 本次要看到:按 B 打印一次按下日志;角色就绪打印一次;重新生成角色应处理新引用。
停止试玩后再换入口;这些章节是能力实验,第 22 章才提供统一的六文件项目。每次只改变一个条件,并保留本次输出用于对照。
你会学到什么
- 玩家加入和离开事件。
- LocalPlayer 和 Character 的关系。
- 如何监听键盘、鼠标输入。
- 阵营列表与成员变化监听。
Players 服务
Players 是玩家管理服务,提供玩家列表、玩家事件和查找功能。
| API | 说明 | 运行端 |
|---|---|---|
Players.PlayerAdded | 玩家加入事件 | 双端;权威处理推荐 server |
Players.PlayerRemoving | 玩家离开事件 | 双端;权威处理推荐 server |
Players.LocalPlayer | 本地玩家 | client |
Players:GetPlayers() | 获取所有玩家列表 | 双端 |
Players:GetPlayerByUserId(userId) | 按字符串玩家编号查找玩家 | 双端 |
Players:GetPlayerFromCharacter(char) | 通过角色反查玩家 | 双端 |
两个查询函数在目标玩家不存在、离线或角色无法归属到玩家时会返回 nil,使用返回值前要先判空。
本章先讲 Player 与输入的基础。角色系统本身会在第 13、14 章展开;这里你只需要记住:
Player是玩家账号/连接对象,有字符串玩家编号UserId、GetName()、CharacterAdded等。Character是玩家在场景里的角色 Unit,碰撞、位置、动画发生在 Character 上。- server 负责玩家列表、阵营、得分、反查角色。
- client 负责
LocalPlayer、键盘鼠标触屏输入和本地表现。
最小示例:玩家加入欢迎
运行端:server
文件:server/main.lua
示例 1|独立实验;整体替换 server/main.lua。 本次演示端:server。
lua
-- @runtime server
local Players = game:GetService("Players")
if not Players then
print("[SE Lua Guide][ERROR] Players 服务获取失败")
return
end
-- 玩家加入时触发
Players.PlayerAdded:Connect(function(player)
print("[SE Lua Guide] 玩家加入:", player:GetName(), "UserId:", player.UserId)
print("[SE Lua Guide] 欢迎加入:", player:GetName())
end)
-- 玩家离开时触发
Players.PlayerRemoving:Connect(function(player)
print("[SE Lua Guide] 玩家离开:", player:GetName())
end)进阶:遍历所有玩家
示例 2|独立实验;整体替换 server/main.lua。 本次演示端:server。
lua
-- @runtime server
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(" -", player:GetName(), player.UserId)
endUserInputService:监听输入
运行端:client
文件:client/main.lua
示例 3|独立实验;整体替换 client/main.lua。 本次演示端:client。
lua
-- @runtime client
local UserInputService = game:GetService("UserInputService")
if not UserInputService then
print("[SE Lua Guide][ERROR] UserInputService 获取失败")
return
end
-- 按键按下时触发
UserInputService.InputBegan:Connect(function(inputObject, gameProcessedEvent)
-- gameProcessedEvent 为 true 表示输入被引擎处理(如聊天框打开时),跳过
if gameProcessedEvent then return end
-- 判断是否按了键盘 B 键
if inputObject.UserInputType == Enums.UserInputType.Keyboard
and inputObject.KeyCode == Enums.KeyCode.B then
print("[SE Lua Guide] 玩家按下了 B 键")
end
end)
-- 按键松开时触发
UserInputService.InputEnded:Connect(function(inputObject, gameProcessedEvent)
if gameProcessedEvent then return end
if inputObject.UserInputType == Enums.UserInputType.Keyboard then
print("[SE Lua Guide] 松开了键:", inputObject.KeyCode)
end
end)如果只是检查某个键当前是否按下,也可以用轮询式接口:
示例 4|独立实验;整体替换 client/main.lua。 本次演示端:client。
lua
-- @runtime client
local UserInputService = game:GetService("UserInputService")
if not UserInputService then
print("[SE Lua Guide][ERROR] UserInputService 获取失败")
return
end
if UserInputService:IsKeyDown(Enums.KeyCode.B) then
print("[SE Lua Guide] B 键正在按下")
end事件适合“按下瞬间触发技能”;轮询适合“持续按住移动/蓄力”。
InputObject 结构
| 属性 | 说明 |
|---|---|
UserInputType | 输入类型(Keyboard、MouseButton、Touch 等) |
KeyCode | 按键码(仅键盘输入时有效) |
Position | 输入位置(鼠标/触屏) |
Delta | 位置变化量 |
进阶:通过角色反查玩家
在碰撞事件中,otherUnit 的公开类型只是泛 Unit。只有确认它确实是 EggyUnit 后,才能把它传给 GetPlayerFromCharacter:
示例 5|函数片段;在 server 碰撞回调中调用 GetPlayerFromCollisionUnit。 本次演示端:server。
lua
-- @runtime server
local Players = game:GetService("Players")
if not Players then
print("[SE Lua Guide][ERROR] Players 服务获取失败")
return
end
local function GetPlayerFromCollisionUnit(otherUnit)
if not otherUnit or not otherUnit:IsA("EggyUnit") then
return nil
end
return Players:GetPlayerFromCharacter(otherUnit)
end
-- 在 OnCollisionEnter 回调里调用:
-- local player = GetPlayerFromCollisionUnit(otherUnit)这个帮助函数宁可在无法证明类型时返回 nil,也不猜“碰到角色的某个子对象就一定等于 Character”。调用方仍要对返回的 Player 判空。
进阶:等待角色加载
玩家加入时角色可能还没加载完成。可以监听 CharacterAdded 事件:
示例 6|独立实验;整体替换 server/main.lua。 本次演示端:server。
lua
-- @runtime server
local Players = game:GetService("Players")
if not Players then
print("[SE Lua Guide][ERROR] Players 服务获取失败")
return
end
local function OnCharacterReady(player, character)
print("[SE Lua Guide] 玩家", player:GetName(), "的角色已就绪:", character)
end
local boundPlayers = {}
local characterConnections = {}
local function BindPlayer(player)
if boundPlayers[player] then return end
boundPlayers[player] = true
local lastCharacter = nil
local function HandleCharacter(character)
-- 先监听、后读取快照时,两条路径可能看到同一个角色;用引用去重。
if not character or character == lastCharacter then return end
lastCharacter = character
OnCharacterReady(player, character)
end
-- 先建立监听,避免在读取当前 Character 的瞬间漏掉重生事件。
characterConnections[player] = player.CharacterAdded:Connect(HandleCharacter)
HandleCharacter(player.Character)
end
-- 先监听未来加入,再补脚本启动前已经在线的玩家。
Players.PlayerAdded:Connect(BindPlayer)
for _, player in ipairs(Players:GetPlayers()) do
BindPlayer(player)
end
Players.PlayerRemoving:Connect(function(player)
local connection = characterConnections[player]
if connection then
connection:Disconnect()
characterConnections[player] = nil
end
boundPlayers[player] = nil
end)CharacterAdded 只报告连接建立之后发生的加载或重生,因此必须同时读取已有 player.Character。上面的顺序是“先监听、再读快照、按角色引用去重”,既避免漏事件,也避免同一角色处理两次;玩家离开时还会断开角色连接。第 13 章会继续封装这套生命周期。
阵营信息
运行端:server
示例 7|独立实验;整体替换 server/main.lua。 本次演示端:server。
lua
-- @runtime server
local Camps = game:GetService("Camps")
if not Camps then
print("[SE Lua Guide][ERROR] Camps 服务获取失败")
return
end
-- 当前 Meta 中 Camps:GetCamps() 返回阵营数组,Camp:GetPlayers() 返回迭代函数。
-- 同时兼容返回数组的运行包,统一收集成数组后再计数。
local function ToArray(values)
if type(values) == "table" then
return values
end
local result = {}
if type(values) == "function" then
for _, value in values do
result[#result + 1] = value
end
end
return result
end
for _, camp in ipairs(Camps:GetCamps()) do
local campPlayers = ToArray(camp:GetPlayers())
print("[SE Lua Guide] 当前阵营玩家数:", #campPlayers)
camp.PlayerAdded:Connect(function(player)
print("[SE Lua Guide] 玩家加入阵营:", player:GetName())
end)
end当前公开 Camps / Camp API 只提供阵营数组、成员集合和成员增减事件;阵营分配和出生点查询没有公开成员,不要调用历史代码里的 AssignCamp 或 GetCampSpawnPoint。本轮 Meta 中 Camps:GetCamps() 返回 Array<Camp>,Camp:GetPlayers() 返回迭代函数。示例按当前合同处理,并兼容返回数组的运行包;以后升级时继续核对目标 Meta 和实际返回形态,不凭历史版本改变当前合同。
常见错误
错误:在服务端使用 LocalPlayer
LocalPlayer 只在客户端有意义。服务端应通过 PlayerAdded 事件或 GetPlayerByUserId 获取玩家。
错误:把输入逻辑写在 server
键盘、鼠标等输入属于客户端。涉及玩法判定的结果再通过 RemoteEvent 交给 server。
错误:PlayerAdded 时立刻操作 Character
玩家加入时角色可能还没加载完成,固定等待若干秒也不能证明它已就绪。应先监听 player.CharacterAdded,再处理已有 player.Character,并按角色引用去重。
错误:把 Player 当成 Character
Player.UserId 和 player:GetName() 在 Player 上;位置、碰撞、动画在 Character 上。碰撞事件只给泛 Unit,必须先用 IsA("EggyUnit") 证明类型,再调用 Players:GetPlayerFromCharacter(otherUnit),并对结果判空。
练习任务
- 玩家加入时打印欢迎信息,并验证离开事件也能正常记录。
- 客户端按下 B 键时打印日志。
- 服务端遍历所有阵营,打印当前成员数量并监听玩家加入事件。
练习参考与复核
先完成练习,再展开对照;没有运行条件的项目填写“未验证”和缺少的条件。
参考思路与应观察的结果
监听未来 PlayerAdded 后补 GetPlayers 快照并去重;对每个玩家先连 CharacterAdded 再读取当前 Character。碰撞对象先 IsA("EggyUnit"),反查结果仍可为 nil。单人未离场不能算通过 PlayerRemoving。
- [ ] 我已完成本章选定实验,记录了上面的预期现象。
- [ ] 我能故意改变一个前置条件,解释失败发生在哪一步,并在恢复后重跑。
本章验收标准
- [ ] 我知道 PlayerAdded/PlayerRemoving 的用法。
- [ ] 我知道 LocalPlayer 只在客户端可用。
- [ ] 我能用 UserInputService 监听按键输入。
- [ ] 我知道输入逻辑应放在 client。
- [ ] 我知道 Player 和 Character 不是同一个对象。
本章产物
- 一个 server 玩家生命周期脚本:玩家加入、角色加载、阵营成员变化日志都能打印。
- 一个 client 输入脚本:按键触发本地日志或提示,不直接修改权威分数。
- 一张 Player / Character 对照表,标出身份字段、位置/碰撞字段和反查方式。
本章 API 对照
下一章预告
你掌握了玩家和输入。下一章用 RemoteEvent 请求只读 HUD 快照,服务端校验后回传结果;按键不会直接加分。
