Skip to content

读取相机并用射线选择目标 ​

读取活跃相机、完成坐标转换与准星射线,并区分相机配置和渲染状态。

什么时候查这篇 ​

需要准星检测、目标标记或了解相机入口时,使用本专题。按当前需求选择小节即可,不必按专题编号顺序读完。

  • 开始前:完成基本客户端脚本;射线组合需先了解物理专题的 direction 与距离。
  • 安装与示例范围:所有相机查询示例放 client,需先进入试玩且活跃相机就绪。屏幕坐标示例仅打印数据,完整 UI 标记还要接入 EUI;ShakeCamera 当前阻断,不可运行。
  • 本次要看到:视口中心射线能报告命中或未命中;相机未就绪时停止查询并输出原因。

如果还没有完成可运行的小游戏,先回到主线:09 课。语法卡住时查Lua 速查,运行结果不符时查按现象排错。

你会学到什么 ​

  • CameraService 和 CameraUnit 的分工。
  • 如何获取当前相机,以及什么时候才需要创建自定义相机。
  • 如何把世界坐标转换成屏幕坐标,用 UI 标记场景目标。
  • 如何从屏幕点生成射线,配合 Raycast 做准星/点击拾取。
  • 如何理解相机震动的公开契约与行为验证边界。

先分清两个概念 ​

相机系统最容易混淆的是“服务”和“相机实例”:

名称你可以把它理解成常见用途
CameraService全局相机管理器坐标转换、屏幕/视口射线、当前相机引用、相机震动
CameraUnit场景里的相机实例位置、旋转、FOV、跟随目标、遮挡检测、每帧相机状态

新手最常用的是 CameraService 的坐标转换和 World.CurrentCamera 的当前相机信息。只有做过场、脚本相机、相机跟随策略时,才需要深入 CameraUnit 的行为模式。

获取当前相机 ​

当你只是读取玩家当前看到的画面时,不需要新建相机:从 World.CurrentCamera 获取当前相机,或从 CameraService.LiveCamera 查看当前实际渲染的相机。当前 Meta 允许创建 CameraUnit;创建能力留给过场、多相机切换等明确需求,不能再写成“CameraUnit 不可创建”。

运行端:client

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
local World = game:GetService("World")
local CameraService = game:GetService("CameraService")
if not World or not CameraService then return end

local camera = World.CurrentCamera
if not camera then
    print("[SE Lua Guide] 当前还没有可用相机")
    return
end

print("[SE Lua Guide] 当前相机:", camera.Name)
local liveCamera = CameraService.LiveCamera
print("[SE Lua Guide] 实际渲染相机:", liveCamera and liveCamera.Name or "尚未就绪")
print("[SE Lua Guide] 相机 FOV:", camera.FieldOfView)

CameraService.MainCamera 是可手动设置的主控相机引用,初始化完成后默认指向当前展示相机;在初始化窗口仍应允许它暂时为 nil。调试“此刻实际渲染什么”时,优先看 LiveCamera 或 World.CurrentCamera。

世界坐标转屏幕坐标 ​

把 3D 世界坐标转成屏幕坐标,常用于“目标头顶图标”“任务点箭头”“敌人标记”。WorldToScreenPoint / WorldToViewportPoint 都需要当前有活跃相机;没有相机时,结果可能是零向量。

运行端:client

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
local CameraService = game:GetService("CameraService")
if not CameraService or not CameraService.LiveCamera then
    print("[SE Lua Guide][WARN] 当前没有活跃相机。")
    return
end

local targetPos = Vector3(10, 5, 20)
local screenPos, isOnScreen = CameraService:WorldToScreenPoint(targetPos)
local viewportPos, isInViewport = CameraService:WorldToViewportPoint(targetPos)

print("[SE Lua Guide] 屏幕坐标:", screenPos, "屏幕内:", isOnScreen)
print("[SE Lua Guide] 视口坐标:", viewportPos, "视口内:", isInViewport)

坐标转换有两个细节:

问题说明
返回值是什么返回 Vector3 和一个布尔值;X/Y 是屏幕或视口坐标,Z 表示深度,布尔值表示是否在屏幕/视口内。
什么时候会不可靠当前没有活跃相机、目标在相机后方、目标离裁剪面太近或太远时。

做 UI 标记时,不要只判断“有没有返回值”,还要结合深度和项目实际效果处理屏幕外目标。例如目标在背后时,可以隐藏标记或把箭头吸附到屏幕边缘。

屏幕点或视口点转射线 ​

准星拾取、点击物体、射击检测通常分两步:

text
屏幕像素 -> CameraService:ScreenPointToRay()
归一化视口点 -> CameraService:ViewportPointToRay()
射线 -> PhysicsService:Raycast()

注意 Raycast 的第二个参数是“方向 × 距离”,不是终点坐标。这一点和选择碰撞、触发器、射线和运动机关保持一致。

运行端:client

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
local CameraService = game:GetService("CameraService")
local PhysicsService = game:GetService("PhysicsService")
if not CameraService or not PhysicsService or not CameraService.LiveCamera then return end

-- ViewportPointToRay 使用 [0, 1] 归一化坐标;(0.5, 0.5) 是视口中心。
local ray = CameraService:ViewportPointToRay(0.5, 0.5, 0)
local maxDistance = 500
local result = PhysicsService:Raycast(ray.Origin, ray.Direction * maxDistance)

if result then
    local hitUnit = result.Instance
    print("[SE Lua Guide] 准星命中:", hitUnit and hitUnit.Name or "未知对象")
    print("[SE Lua Guide] 命中位置:", result.Position)
else
    print("[SE Lua Guide] 准星没有命中")
end

如果要每帧检测准星命中,需要节流输出日志,并在不再使用时断开 Heartbeat 连接。不要每帧打印完整命中信息,否则日志会很快被刷满。

CameraUnit 能做什么 ​

当前相机是 CameraUnit,可以读取相机状态,也可以使用它的专门能力。

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
local World = game:GetService("World")
if not World then return end

local camera = World.CurrentCamera
if camera and camera:IsA("CameraUnit") then
    print("[SE Lua Guide] 相机位置:", camera.Position)
    print("[SE Lua Guide] 相机朝向:", camera.Rotation)
    print("[SE Lua Guide] 相机模式:", camera.CameraType)
end

常用字段:

字段用途
Position / Rotation当前相机位置和朝向。
FieldOfView视野宽窄。数值越大,看得越广,透视感越强。
TrackingUnit / ExtraSubject相机跟随目标与额外关注目标。
Priority多相机切换时的展示优先级,数值越大越容易接管画面。
DevCameraOcclusionMode相机被遮挡时的处理策略。
AfterUpdateState相机状态更新后的本地事件。

更进阶的脚本相机需要理解 ScriptableBehaviour、Mode、CameraType、Priority 和相机切换。本系列只要求你先会读当前相机、做坐标转换和拾取检测;这些字段的组合行为必须按具体模式单独验证,不能靠字段名猜测。

遮挡检测 ​

CameraUnit:GetPartsObscuringTarget(castPoints, ignoreList) 可以检查相机到若干目标点之间是否有遮挡物,适合做“角色被墙挡住时半透明”“目标被遮挡时隐藏标记”等表现。

示例类别:独立实验;运行端:client;文件:client/main.lua。

lua
local World = game:GetService("World")
local Players = game:GetService("Players")
if not World or not Players then return end

local camera = World.CurrentCamera
local player = Players.LocalPlayer
if not player then return end

local character = player.Character

if camera and camera:IsA("CameraUnit") and character and character:IsA("EggyUnit") then
    local targetPos = character:GetPosition()
    local obscuredUnits = camera:GetPartsObscuringTarget({ targetPos }, { character })

    for _, unit in ipairs(obscuredUnits) do
        print("[SE Lua Guide] 遮挡相机的对象:", unit.Name)
    end
end

相机震动 ​

当前实时 Meta/API 已公开 ShakeCamera 方法及其 player、shakeType、maxAmplitude、shakeTime、shakeCurve 参数,也公开了 Enums.CameraShakeType.FRONT_AND_BACK / UP_AND_DOWN / ROTATE 与 Enums.CameraShakeCurve.SINE / NOISE。

当前停止点:暂不使用 ShakeCamera。 最近可用的运行记录中,访问配套枚举出现 module not found,目前没有新的干净试玩态行为证据证明完整链路已经恢复。先用已验证的音效或 HUD 提示表达受击反馈;不要用裸数字或字符串绕过枚举。恢复状态以运行与兼容说明为准。

表现层归属 ​

相机通常是表现层,但不等于所有相机代码都放在 client。

需求推荐运行端原因
本地准星检测、UI 标记client只影响本地画面,响应要快。
玩家受击震动server 决定结果,完整调用链需实测当前契约已公开,运行行为仍需在干净试玩环境验证。
过场镜头视情况单人本地过场可 client;多人共同事件由 server 统一触发。
玩法命中判定serverclient 的射线可用于预览,最终结果要由 server 校验。

常见错误 ​

错误:为了读取当前画面重复创建 CameraUnit ​

CameraUnit 当前允许创建,但读取当前画面不需要新建对象。先从 World.CurrentCamera 或 CameraService.LiveCamera 获取;只有明确设计自定义过场或多相机切换时才创建,并负责激活、优先级和恢复。

错误:把 Raycast 第二个参数当终点 ​

PhysicsService:Raycast(origin, direction, raycastParams) 的 direction 是方向向量乘最大距离;第三个过滤参数按 Meta 可省略。射线检测应写 ray.Direction * 500,不是 ray.Origin + ray.Direction * 500。

错误:把屏幕像素和归一化视口坐标混用 ​

ScreenPointToRay(x, y) 的 x/y 是屏幕像素;ViewportPointToRay(x, y) 的 x/y 才是 [0, 1] 归一化视口坐标。视口中心用 ViewportPointToRay(0.5, 0.5, 0),不能把相同参数直接传给 ScreenPointToRay。

错误:用数字绕过相机震动枚举 ​

公开枚举是调用契约的一部分。即使目标运行包出现导出问题,也应停止调用并记录版本证据,不能把枚举值改成裸数字或字符串规避错误。

错误:每帧打印相机和命中信息 ​

调试时可以临时打印,但 Heartbeat 中必须节流,否则日志会淹没真正的问题。

练习任务 ​

  1. 读取 World.CurrentCamera,打印相机位置、FOV 和 CameraService.LiveCamera。
  2. 把场景中一个目标的世界坐标转换为屏幕坐标,设计一个 UI 标记。
  3. 用 ViewportPointToRay + PhysicsService:Raycast 实现视口中心准星拾取。
  4. 为相机射线示例补充活跃相机未就绪与未命中的保护分支。

本专题验收标准 ​

  • [ ] 我知道 CameraService 与 CameraUnit 的区别。
  • [ ] 我知道当前相机应从 World.CurrentCamera / CameraService.LiveCamera 获取。
  • [ ] 我能用 WorldToScreenPoint 做 3D 目标标记。
  • [ ] 我能区分 ScreenPointToRay 的像素坐标与 ViewportPointToRay 的归一化坐标。
  • [ ] 我能用视口射线配合 Raycast 做拾取检测。
  • [ ] 我知道 ShakeCamera 当前已公开,但本轮尚无干净试玩态行为证据,也不会使用裸数字绕过枚举。

本专题产物 ​

  • 一个准星拾取或 3D 目标标记示例,能打印命中对象和屏幕坐标。
  • 一段带相机就绪检查与未命中处理的射线检测代码。
  • 一张相机表现分工表,区分本地画面、共同事件和玩法判定。

本专题 API 对照 ​

把结果带回小游戏 ​

先确认本页“本次要看到”的现象,再把选中的功能接到已有模块。保留原有入口和清理逻辑,只迁入需要的部分;不要把多个试验入口拼在一起。

返回主线对应步骤,或去专题导航选择下一项能力。新的代码尚未完成目标地图实测时,记录为待验证,不把编译通过当作行为通过。