Skip to content

第 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)
end

UserInputService:监听输入 ​

运行端: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),并对结果判空。

练习任务 ​

  1. 玩家加入时打印欢迎信息,并验证离开事件也能正常记录。
  2. 客户端按下 B 键时打印日志。
  3. 服务端遍历所有阵营,打印当前成员数量并监听玩家加入事件。

练习参考与复核 ​

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

参考思路与应观察的结果

监听未来 PlayerAdded 后补 GetPlayers 快照并去重;对每个玩家先连 CharacterAdded 再读取当前 Character。碰撞对象先 IsA("EggyUnit"),反查结果仍可为 nil。单人未离场不能算通过 PlayerRemoving。

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

本章验收标准 ​

  • [ ] 我知道 PlayerAdded/PlayerRemoving 的用法。
  • [ ] 我知道 LocalPlayer 只在客户端可用。
  • [ ] 我能用 UserInputService 监听按键输入。
  • [ ] 我知道输入逻辑应放在 client。
  • [ ] 我知道 Player 和 Character 不是同一个对象。

本章产物 ​

  • 一个 server 玩家生命周期脚本:玩家加入、角色加载、阵营成员变化日志都能打印。
  • 一个 client 输入脚本:按键触发本地日志或提示,不直接修改权威分数。
  • 一张 Player / Character 对照表,标出身份字段、位置/碰撞字段和反查方式。

本章 API 对照 ​

下一章预告 ​

你掌握了玩家和输入。下一章用 RemoteEvent 请求只读 HUD 快照,服务端校验后回传结果;按键不会直接加分。