激光的命中检测

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

在本教程中,您将学习如何从创建玩家工具中的能量枪发射激光,并检测它是否击中玩家。

射线投射以查找碰撞

射线投射从起始位置向给定方向创建一条不可见的射线,具有定义的长度。如果射线与路径中的物体或地形发生碰撞,它将返回有关碰撞的信息,例如位置和与之碰撞的物体。

从 A 向 B 射线投射与墙壁发生碰撞

查找鼠标位置

在发射激光之前,您必须首先知道玩家的瞄准位置。这可以通过从玩家在屏幕上的 2D 鼠标位置直接向前射线投射,从相机进入 3D 世界来找到。射线将与玩家用鼠标瞄准的任何物体发生碰撞。

  1. 打开创建玩家工具中的能量枪内的 ToolController 脚本。如果您尚未完成该教程,可以下载能量枪模型并将其插入到 StarterPack 中。

  2. 在脚本顶部声明一个名为 MAX_MOUSE_DISTANCE 的常量,值为 1000

  3. 创建一个名为 getWorldMousePosition 的函数。

    local tool = script.Parent
    local MAX_MOUSE_DISTANCE = 1000
    local function getWorldMousePosition()
    end
    local function toolEquipped()
    tool.Handle.Equip:Play()
    end
    local function toolActivated()
    tool.Handle.Activate:Play()
    end
    -- 将事件连接到适当的函数
    tool.Equipped:Connect(toolEquipped)
    tool.Activated:Connect(toolActivated)
  4. 使用 UserInputService 的 GetMouseLocation 函数获取玩家在屏幕上的 2D 鼠标位置。将其分配给名为 mouseLocation 的变量。

    local UserInputService = game:GetService("UserInputService")
    local tool = script.Parent
    local MAX_MOUSE_DISTANCE = 1000
    local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    end

现在已知 2D 鼠标位置,可以使用其 XY 属性作为参数传递给 Camera:ViewportPointToRay() 函数,该函数从屏幕创建一个 Ray 进入 3D 世界。

  1. 使用 mouseLocationXY 属性作为 ViewportPointToRay() 函数的参数。将其分配给名为 screenToWorldRay 的变量。

    local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    -- 从 2D 鼠标位置创建一条射线
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    end

现在是时候使用 Raycast 函数检查射线是否击中物体。这需要一个起始位置和方向向量:在此示例中,您将使用 screenToWorldRay 的原点和方向属性。

方向向量的长度决定了射线将行进多远。射线需要与 MAX_MOUSE_DISTANCE 一样长,因此您必须将方向向量乘以 MAX_MOUSE_DISTANCE

  1. 声明一个名为 directionVector 的变量,并将其赋值为 screenToWorldRay.Direction 乘以 MAX_MOUSE_DISTANCE

    local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    -- 从 2D 鼠标位置创建一条射线
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    -- 射线的单位方向向量乘以最大距离
    local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
  2. 调用工作区的 Raycast 函数,将 screenToWorldRayOrigin 属性作为第一个参数,将 directionVector 作为第二个参数。将其分配给名为 raycastResult 的变量。

    local function getWorldMousePosition()
    local mouseLocation = UserInputService:GetMouseLocation()
    -- 从 2D 鼠标位置创建一条射线
    local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    -- 射线的单位方向向量乘以最大距离
    local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
    -- 从射线的原点朝其方向进行射线投射
    local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)

碰撞信息

如果射线投射操作找到被射线击中的物体,它将返回一个 RaycastResult,其中包含有关射线与物体之间碰撞的信息。

RaycastResult 属性描述
Instance射线相交的 BasePartTerrain 单元。
Position交点发生的位置;通常是直接在部件或地形表面上的一个点。
Material碰撞点的材料。
Normal相交面的法向量。这可以用来确定面朝向的方向。

Position 属性将是鼠标悬停的物体的位置。如果鼠标没有悬停在距离 MAX_MOUSE_DISTANCE 内的任何物体上,则 raycastResult 将为 nil

  1. 创建一个 if 语句以检查 raycastResult 是否存在。

  2. 如果 raycastResult 有值,则返回其 Position 属性。

  3. 如果 raycastResultnil,则查找射线投射的末端。通过将 screenToWorldRay.OrigindirectionVector 相加来计算鼠标的 3D 位置。

local function getWorldMousePosition()
local mouseLocation = UserInputService:GetMouseLocation()
-- 从 2D 鼠标位置创建一条射线
local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
-- 射线的单位方向向量乘以最大距离
local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
-- 从射线的原点朝其方向进行射线投射
local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
if raycastResult then
-- 返回交点的 3D 点
return raycastResult.Position
else
-- 没有物体被击中,因此计算射线末端的位置
return screenToWorldRay.Origin + directionVector
end
end

朝目标发射

现在已知 3D 鼠标位置,可以将其用作朝向发射激光的 目标位置。可以使用 Raycast 函数在玩家的武器和目标位置之间发射第二条射线。

  1. 在脚本顶部声明一个名为 MAX_LASER_DISTANCE 的常量,并将其赋值为 500,或您选择的激光枪射程。

    local UserInputService = game:GetService("UserInputService")
    local tool = script.Parent
    local MAX_MOUSE_DISTANCE = 1000
    local MAX_LASER_DISTANCE = 500
  2. getWorldMousePosition 函数下创建一个名为 fireWeapon 的函数。

  3. 调用 getWorldMousePosition 并将结果分配给名为 mousePosition 的变量。这将是射线投射的目标位置。

    -- 没有物体被击中,因此计算射线末端的位置
    return screenToWorldRay.Origin + directionVector
    end
    end
    local function fireWeapon()
    local mouseLocation = getWorldMousePosition()
    end
    local function toolEquipped()
    tool.Handle.Equip:Play()
    end

这次,射线投射函数的方向向量将表示从玩家工具位置到目标位置的方向。

  1. 声明一个名为 targetDirection 的变量,并通过从 mouseLocation 中减去工具位置来计算方向向量。

  2. 使用其 Unit 属性对向量进行归一化。这使得它的大小为 1,便于稍后乘以长度。

    local function fireWeapon()
    local mouseLocation = getWorldMousePosition()
    -- 计算归一化的方向向量并乘以激光距离
    local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    end
  3. 声明一个名为 directionVector 的变量,并将其赋值为 targetDirection 乘以 MAX_LASER_DISTANCE

    local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    -- 发射武器的方向,乘以最大距离
    local directionVector = targetDirection * MAX_LASER_DISTANCE
    end

可以使用 RaycastParams 对象来存储射线投射函数的附加参数。它将在您的激光枪中使用,以确保射线投射不会意外与发射武器的玩家发生碰撞。包含在 RaycastParams 对象的 FilterDescendantsInstances 属性中的任何部件将在射线投射中被 忽略

  1. 继续 fireWeapon 函数并声明一个名为 weaponRaycastParams 的变量。将一个新的 RaycastParams 对象分配给它。

  2. 创建一个包含玩家本地 character 的表,并将其分配给 weaponRaycastParams.FilterDescendantsInstances 属性。

  3. 从玩家的工具手柄位置进行射线投射,朝向 directionVector 的方向。记得这次将 weaponRaycastParams 作为参数添加。将其分配给名为 weaponRaycastResult 的变量。

local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local tool = script.Parent
local MAX_MOUSE_DISTANCE = 1000
local MAX_LASER_DISTANCE = 500
local function getWorldMousePosition()
local function fireWeapon()
local mouseLocation = getWorldMousePosition()
-- 计算归一化的方向向量并乘以激光距离
local targetDirection = (mouseLocation - tool.Handle.Position).Unit
-- 发射武器的方向乘以最大距离
local directionVector = targetDirection * MAX_LASER_DISTANCE
-- 忽略玩家的角色以防止他们自我伤害
local weaponRaycastParams = RaycastParams.new()
weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
end

最后,您需要检查射线投射操作是否返回了值。如果返回了值,则表示射线击中了物体,可以在武器和击中位置之间创建激光。如果没有返回任何内容,则需要计算最终位置以创建激光。

  1. 声明一个名为 hitPosition 的空变量。

  2. 使用 if 语句检查 weaponRaycastResult 是否有值。如果击中物体,则将 weaponRaycastResult.Position 赋值给 hitPosition

    local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
    -- 检查在起始位置和结束位置之间是否击中了任何物体
    local hitPosition
    if weaponRaycastResult then
    hitPosition = weaponRaycastResult.Position
    end
  3. 如果 weaponRaycastResult 没有值,则通过将工具手柄的 positiondirectionVector 相加来计算射线投射的结束位置。将其分配给 hitPosition

    local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
    -- 检查在起始位置和结束位置之间是否击中了任何物体
    local hitPosition
    if weaponRaycastResult then
    hitPosition = weaponRaycastResult.Position
    else
    -- 根据最大激光距离计算结束位置
    hitPosition = tool.Handle.Position + directionVector
    end
    end
  4. 导航到 toolActivated 函数并调用 fireWeapon 函数,以便每次激活工具时发射激光。

    local function toolActivated()
    tool.Handle.Activate:Play()
    fireWeapon()
    end

检查击中的物体

要查找激光击中的物体是否是玩家角色的一部分,还是仅仅是场景的一部分,您需要查找 Humanoid,因为每个角色都有一个。

首先,您需要找到 角色模型。如果击中角色的某个部分,您不能假设击中物体的父级就是角色。激光可能击中了身体部位、配件或工具,这些都位于角色层次结构的不同部分。

您可以使用 FindFirstAncestorOfClass 查找激光击中物体的角色模型祖先(如果存在)。如果找到模型并且它包含一个人形,在大多数情况下,您可以假设它是一个角色。

  1. weaponRaycastResult if 语句中添加以下高亮代码以检查是否击中角色。

    -- 检查在起始位置和结束位置之间是否击中了任何物体
    local hitPosition
    if weaponRaycastResult then
    hitPosition = weaponRaycastResult.Position
    -- 击中的实例将是角色模型的子级
    -- 如果在模型中找到人形,则很可能是玩家的角色
    local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
    if characterModel then
    local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
    print("玩家被击中")
    end
    end
    else
    -- 根据最大激光距离计算结束位置
    hitPosition = tool.Handle.Position + directionVector
    end

现在,每当射线投射操作击中另一个玩家时,激光枪应该在 输出 窗口中打印 玩家被击中

使用多个玩家进行测试

需要两个玩家来测试武器射线是否找到其他玩家,因此您需要启动一个本地服务器。有关说明,请参见 多客户端模拟

在一个客户端上,测试用武器射击另一个玩家,点击他们。每次击中玩家时,输出中应显示“玩家被击中”。

查找激光位置

能量枪应向其目标发射一束红光。此功能将位于 ModuleScript 中,以便以后可以在其他脚本中重用。首先,脚本需要找到激光束应呈现的位置。

  1. 创建一个名为 LaserRendererModuleScript,将其作为子项放置在 StarterPlayerScripts 下的 StarterPlayer 中。

  2. 打开脚本并将模块表重命名为脚本的名称 LaserRenderer

  3. 声明一个名为 SHOT_DURATION 的变量,值为 0.15。这将是激光可见的时间(以秒为单位)。

  4. 创建一个名为 createLaser 的 LaserRenderer 函数,具有两个参数,分别称为 toolHandleendPosition

    local LaserRenderer = {}
    local SHOT_DURATION = 0.15 -- 激光可见的时间
    -- 从起始位置朝向结束位置创建激光束
    function LaserRenderer.createLaser(toolHandle, endPosition)
    end
    return LaserRenderer
  5. 声明一个名为 startPosition 的变量,并将 toolHandlePosition 属性设置为其值。这将是玩家的激光枪的位置。

  6. 声明一个名为 laserDistance 的变量,并从 startPosition 中减去 endPosition 以找到两个向量之间的差。使用 Magnitude 属性获取激光束的长度。

    function LaserRenderer.createLaser(toolHandle, endPosition)
    local startPosition = toolHandle.Position
    local laserDistance = (startPosition - endPosition).Magnitude
    end
  7. 声明一个 laserCFrame 变量以存储激光束的位置和方向。位置需要是激光束起点和终点的中点。使用 CFrame.lookAt 创建一个位于 startPosition 并朝向 endPosition 的新 CFrame。将其乘以一个 Z 轴值为负 laserDistance 一半的新 CFrame,以获得中点。

    function LaserRenderer.createLaser(toolHandle, endPosition)
    local startPosition = toolHandle.Position
    local laserDistance = (startPosition - endPosition).Magnitude
    local laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)
    end

创建激光部件

现在您知道在哪里创建激光束,您需要添加激光束本身。这可以通过一个 Neon 部件轻松完成。

  1. 声明一个变量 laserPart,并将其分配给一个新的 Part 实例。

  2. 设置 laserPart 的以下属性:

    1. Size: Vector3.new(0.2, 0.2, laserDistance)
    2. CFrame: laserCFrame
    3. Anchored: true
    4. CanCollide: false
    5. Color: Color3.fromRGB(225, 0, 0)(一种鲜艳的红色)
    6. Material: Enum.Material.Neon
  3. laserPart 作为子项放置在 Workspace 中。

  4. 将该部件添加到 Debris 服务中,以便在 SHOT_DURATION 变量指定的秒数后将其移除。

    function LaserRenderer.createLaser(toolHandle, endPosition)
    local startPosition = toolHandle.Position
    local laserDistance = (startPosition - endPosition).Magnitude
    local laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)
    local laserPart = Instance.new("Part")
    laserPart.Size = Vector3.new(0.2, 0.2, laserDistance)
    laserPart.CFrame = laserCFrame
    laserPart.Anchored = true
    laserPart.CanCollide = false
    laserPart.Color = Color3.fromRGB(225, 0, 0)
    laserPart.Material = Enum.Material.Neon
    laserPart.Parent = workspace
    -- 将激光束添加到 Debris 服务中以便移除和清理
    Debris:AddItem(laserPart, SHOT_DURATION)
    end

现在渲染激光束的函数已完成,可以由 ToolController 调用。

  1. ToolController 脚本的顶部声明一个名为 LaserRenderer 的变量,并要求 PlayerScripts 中的 LaserRenderer ModuleScript。

    local UserInputService = game:GetService("UserInputService")
    local Players = game:GetService("Players")
    local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
    local tool = script.Parent
  2. fireWeapon 函数的底部,使用工具手柄和 hitPosition 作为参数调用 LaserRenderer 的 createLaser 函数。

    -- 根据最大激光距离计算结束位置
    hitPosition = tool.Handle.Position + directionVector
    end
    LaserRenderer.createLaser(tool.Handle, hitPosition)
    end
  3. 通过单击播放按钮测试武器。当激活工具时,激光束应在武器和鼠标之间可见。

控制武器射击速率

武器在每次射击之间需要有延迟,以防止玩家在短时间内造成过多伤害。这可以通过检查自上次射击以来是否经过足够的时间来控制。

  1. ToolController 的顶部声明一个名为 FIRE_RATE 的变量。这将是每次射击之间的最小时间。给它一个您选择的值;本示例使用 0.3 秒。

  2. 在下面声明另一个变量,名为 timeOfPreviousShot,值为 0。这存储玩家上次射击的时间,并将在每次射击时更新。

    local MAX_MOUSE_DISTANCE = 1000
    local MAX_LASER_DISTANCE = 300
    local FIRE_RATE = 0.3
    local timeOfPreviousShot = 0
  3. 创建一个名为 canShootWeapon 的函数,不带参数。此函数将查看自上次射击以来经过了多少时间,并返回 true 或 false。

    local FIRE_RATE = 0.3
    local timeOfPreviousShot = 0
    -- 检查自上次射击以来是否经过足够的时间
    local function canShootWeapon()
    end
    local function getWorldMousePosition()
  4. 在函数内部声明一个名为 currentTime 的变量;将其赋值为调用 tick() 函数的结果。此函数返回自 1970 年 1 月 1 日(一个广泛用于计算时间的任意日期)以来经过的时间(以秒为单位)。

  5. currentTime 中减去 timeOfPreviousShot,如果结果小于 FIRE_RATE,则返回 false;否则返回 true

    -- 检查自上次射击以来是否经过足够的时间
    local function canShootWeapon()
    local currentTime = tick()
    if currentTime - timeOfPreviousShot < FIRE_RATE then
    return false
    end
    return true
    end
  6. fireWeapon 函数的末尾,每次武器射击时使用 tick 更新 timeOfPreviousShot

    hitPosition = tool.Handle.Position + directionVector
    end
    timeOfPreviousShot = tick()
    LaserRenderer.createLaser(tool.Handle, hitPosition)
    end
  7. toolActivated 函数内部,创建一个 if 语句并调用 canShootWeapon 来检查武器是否可以发射。

    local function toolActivated()
    if canShootWeapon() then
    tool.Handle.Activate:Play()
    fireWeapon()
    end
    end

当您测试激光枪时,您应该发现无论您点击多快,每次射击之间始终会有短暂的 0.3 秒延迟。

对玩家造成伤害

客户端无法直接对其他客户端造成伤害;服务器需要负责在玩家被击中时发出伤害。

客户端可以使用 RemoteEvent 告诉服务器角色已被击中。这些应该存储在 ReplicatedStorage 中,以便客户端和服务器都能看到。

  1. 在 ReplicatedStorage 中创建一个名为 Events文件夹

  2. 在 Events 文件夹中插入一个 RemoteEvent,并将其命名为 DamageCharacter

  3. ToolController 中,在脚本开始处为 ReplicatedStorage 和 Events 文件夹创建变量。

    local UserInputService = game:GetService("UserInputService")
    local Players = game:GetService("Players")
    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
    local tool = script.Parent
    local eventsFolder = ReplicatedStorage.Events
    local MAX_MOUSE_DISTANCE = 1000
    local MAX_LASER_DISTANCE = 500
  4. 用一行 Luau 替换 fireWeapon 中的 "玩家被击中" 打印语句,以使用 characterModel 变量作为参数触发 DamageCharacter 远程事件。

    local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
    if characterModel then
    local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
    eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
    end
    end
    else
    -- 根据最大激光距离计算结束位置
    hitPosition = tool.Handle.Position + directionVector
    end

服务器需要在事件触发时对被击中的玩家造成伤害。

  1. 在 ServerScriptService 中插入一个 脚本,并将其命名为 ServerLaserManager

  2. 声明一个名为 LASER_DAMAGE 的变量,并将其设置为 10,或您选择的值。

  3. 创建一个名为 damageCharacter 的函数,具有两个参数,分别称为 playerFiredcharacterToDamage

  4. 在函数内部,查找角色的人形并从其生命值中减去 LASER_DAMAGE

  5. damageCharacter 函数连接到 Events 文件夹中的 DamageCharacter 远程事件。

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local eventsFolder = ReplicatedStorage.Events
    local LASER_DAMAGE = 10
    function damageCharacter(playerFired, characterToDamage)
    local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
    -- 从角色中移除生命值
    humanoid.Health -= LASER_DAMAGE
    end
    end
    -- 将事件连接到适当的函数
    eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
  6. 通过启动本地服务器测试激光枪,使用 2 个玩家。当您射击另一个玩家时,他们的生命值将减少 LASER_DAMAGE 中分配的值。

渲染其他玩家的激光束

目前,激光束是由发射武器的客户端创建的,因此只有他们能够看到激光束。

如果激光束是在服务器上创建的,那么每个人都能看到它。然而,客户端射击武器和服务器接收有关射击的信息之间会有一个小 延迟。这意味着发射武器的客户端会看到激活武器和看到激光束之间的延迟;因此武器会感觉滞后。

为了解决这个问题,每个客户端将创建自己的激光束。这意味着发射武器的客户端将立即看到激光束。其他客户端将在另一个玩家射击和激光束出现之间经历小延迟。这是最佳情况:没有办法更快地将一个客户端的激光传达给其他客户端。

射手的客户端

首先,客户端需要告诉服务器它已发射激光并提供结束位置。

  1. 在 ReplicatedStorage 的 Events 文件夹中插入一个 RemoteEvent,并将其命名为 LaserFired

  2. ToolController 脚本中找到 fireWeapon 函数。在函数末尾,使用 hitPosition 作为参数触发 LaserFired 远程事件。

    hitPosition = tool.Handle.Position + directionVector
    end
    timeOfPreviousShot = tick()
    eventsFolder.LaserFired:FireServer(hitPosition)
    LaserRenderer.createLaser(tool.Handle, hitPosition)
    end

服务器

服务器现在必须接收客户端发射的事件,并告诉所有客户端激光束的起始和结束位置,以便它们也可以渲染它。

  1. ServerLaserManager 脚本中,创建一个名为 playerFiredLaser 的函数,位于 damageCharacter 之上,具有两个参数,分别称为 playerFiredendPosition

  2. 将该函数连接到 LaserFired 远程事件。

    -- 通知所有客户端激光已发射,以便它们可以显示激光
    local function playerFiredLaser(playerFired, endPosition)
    end
    -- 将事件连接到适当的函数
    eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
    eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)

服务器需要激光的起始位置。这可以从客户端发送,但最好尽可能避免信任客户端。角色的武器手柄位置将是起始位置,因此服务器可以从那里找到它。

  1. playerFiredLaser 函数上方创建一个名为 getPlayerToolHandle 的函数,具有一个参数,称为 player

  2. 使用以下代码在玩家角色中查找武器并返回手柄对象。

    local LASER_DAMAGE = 10
    -- 查找玩家持有的工具的手柄
    local function getPlayerToolHandle(player)
    local weapon = player.Character:FindFirstChildOfClass("Tool")
    if weapon then
    return weapon:FindFirstChild("Handle")
    end
    end
    -- 通知所有客户端激光已发射,以便它们可以显示激光
    local function playerFiredLaser(playerFired, endPosition)

服务器现在可以在 LaserFired 远程事件上调用 FireAllClients,以将渲染激光所需的信息发送给客户端。这包括 发射激光的玩家(以便该玩家的客户端不会重复渲染激光)、手柄(作为激光的起始位置)和激光的结束位置。

  1. playerFiredLaser 函数中,使用 getPlayerToolHandle 函数调用 playerFired 作为参数,并将值分配给名为 toolHandle 的变量。

  2. 如果 toolHandle 存在,则使用 playerFiredtoolHandleendPosition 作为参数触发 LaserFired 事件以通知所有客户端。

    -- 通知所有客户端激光已发射,以便它们可以显示激光
    local function playerFiredLaser(playerFired, endPosition)
    local toolHandle = getPlayerToolHandle(playerFired)
    if toolHandle then
    eventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)
    end
    end

在客户端渲染

现在 FireAllClients 已被调用,每个客户端将收到来自服务器的事件,以渲染激光束。每个客户端可以重用之前的 LaserRenderer 模块,以使用服务器发送的工具手柄位置和结束位置值来渲染激光束。最初发射激光束的玩家应忽略此事件,否则他们将看到 2 个激光束。

  1. 在 StarterPlayerScripts 中创建一个名为 ClientLaserManagerLocalScript

  2. 在脚本中,要求 LaserRenderer 模块。

  3. 创建一个名为 createPlayerLaser 的函数,参数为 playerWhoShottoolHandleendPosition

  4. 将该函数连接到 Events 文件夹中的 LaserFired 远程事件。

  5. 在函数中,使用 if 语句检查 playerWhoShot 是否 不等于 LocalPlayer。

  6. 在 if 语句内部,使用 toolHandleendPosition 作为参数调用 LaserRenderer 模块的 createLaser 函数。

    local Players = game:GetService("Players")
    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local LaserRenderer = require(script.Parent:WaitForChild("LaserRenderer"))
    local eventsFolder = ReplicatedStorage.Events
    -- 显示另一个玩家的激光
    local function createPlayerLaser(playerWhoShot, toolHandle, endPosition)
    if playerWhoShot ~= Players.LocalPlayer then
    LaserRenderer.createLaser(toolHandle, endPosition)
    end
    end
    eventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)
  7. 通过启动本地服务器测试激光枪,使用 2 个玩家。将每个客户端放置在显示器的不同侧,以便您可以同时看到两个窗口。当您在一个客户端上射击时,您应该在另一个客户端上看到激光。

音效

目前,射击音效仅在发射投射物的客户端播放。您需要移动播放声音的代码,以便其他玩家也能听到。

  1. ToolController 脚本中,导航到 toolActivated 函数并删除播放激活声音的行。

    local function toolActivated()
    if canShootWeapon() then
    fireWeapon()
    end
    end
  2. LaserRenderer 中的 createLaser 函数底部,声明一个名为 shootingSound 的变量,并使用 FindFirstChild() 方法检查 toolHandle 中的 Activate 声音。

  3. 使用 if 语句检查 shootingSound 是否存在;如果存在,则调用其 Play() 函数。

    laserPart.Parent = workspace
    -- 将激光束添加到 Debris 服务中以便移除和清理
    Debris:AddItem(laserPart, SHOT_DURATION)
    -- 播放武器的射击声音
    local shootingSound = toolHandle:FindFirstChild("Activate")
    if shootingSound then
    shootingSound:Play()
    end
    end

使用验证保护远程事件

如果服务器没有检查来自传入请求的数据,黑客可能会滥用远程函数和事件,并使用它们向服务器发送虚假值。使用 服务器端验证 来防止这种情况非常重要。

在当前形式下,DamageCharacter 远程事件非常容易受到攻击。黑客可以使用此事件对任何他们想要的玩家造成伤害,而无需射击他们。

验证是检查发送到服务器的值是否现实的过程。在这种情况下,服务器需要:

  • 检查玩家与激光击中位置之间的距离是否在某个边界内。
  • 在发射激光的武器和击中位置之间进行射线投射,以确保射击是可能的,并且没有穿过任何墙壁。

客户端

客户端需要将射线投射击中的位置发送给服务器,以便它可以检查距离是否现实。

  1. ToolController 中,导航到 fireWeapon 函数中触发 DamageCharacter 远程事件的行。

  2. hitPosition 作为参数添加。

    if characterModel then
    local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
    eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
    end
    end

服务器

客户端现在通过 DamageCharacter 远程事件发送了一个额外的参数,因此 ServerLaserManager 需要进行调整以接受它。

  1. ServerLaserManager 脚本中,向 damageCharacter 函数添加一个 hitPosition 参数。

    function damageCharacter(playerFired, characterToDamage, hitPosition)
    local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
    -- 从角色中移除生命值
    humanoid.Health -= LASER_DAMAGE
    end
    end
  2. getPlayerToolHandle 函数下方,创建一个名为 isHitValid 的函数,具有三个参数:playerFiredcharacterToDamagehitPosition

    end
    local function isHitValid(playerFired, characterToDamage, hitPosition)
    end

第一个检查将是击中位置与被击中角色之间的距离。

  1. 在脚本顶部声明一个名为 MAX_HIT_PROXIMITY 的变量,并将其赋值为 10。这将是击中位置与角色之间允许的最大距离。需要容差,因为角色可能在客户端发射事件后稍微移动。

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local eventsFolder = ReplicatedStorage.Events
    local LASER_DAMAGE = 10
    local MAX_HIT_PROXIMITY = 10
  2. isHitValid 函数中,计算角色与击中位置之间的距离。如果距离大于 MAX_HIT_PROXIMITY,则返回 false

    local function isHitValid(playerFired, characterToDamage, hitPosition)
    -- 验证被击中角色与击中位置之间的距离
    local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
    if characterHitProximity > MAX_HIT_PROXIMITY then
    return false
    end
    end

第二个检查将涉及在发射的武器和击中位置之间进行射线投射。如果射线投射返回的物体不是角色,则可以假设射击无效,因为有东西阻挡了射击。

  1. 复制以下代码以执行此检查。返回 true 在函数末尾:如果到达末尾,则所有检查都已通过。

    local function isHitValid(playerFired, characterToDamage, hitPosition)
    -- 验证被击中角色与击中位置之间的距离
    local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
    if characterHitProximity > 10 then
    return false
    end
    -- 检查是否穿过墙壁
    local toolHandle = getPlayerToolHandle(playerFired)
    if toolHandle then
    local rayLength = (hitPosition - toolHandle.Position).Magnitude
    local rayDirection = (hitPosition - toolHandle.Position).Unit
    local raycastParams = RaycastParams.new()
    raycastParams.FilterDescendantsInstances = {playerFired.Character}
    local rayResult = workspace:Raycast(toolHandle.Position, rayDirection * rayLength, raycastParams)
    -- 如果击中的是不是角色的实例,则忽略射击
    if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
    return false
    end
    end
    return true
    end
  2. damageCharacter 函数中声明一个名为 validShot 的变量。将其赋值为调用 isHitValid 函数的结果,传入三个参数:playerFiredcharacterToDamagehitPosition

  3. 在下面的 if 语句中,添加一个 and 运算符以检查 validShot 是否为 true

    function damageCharacter(playerFired, characterToDamage, hitPosition)
    local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")
    local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
    if humanoid and validShot then
    -- 从角色中移除生命值
    humanoid.Health -= LASER_DAMAGE
    end
    end

现在,damageCharacter 远程事件更加安全,并将防止大多数玩家滥用它。请注意,一些恶意玩家通常会找到绕过验证的方法;保持远程事件安全是一个持续的努力。

您的激光枪现在已完成,具有使用射线投射的基本命中检测系统。尝试 检测用户输入 教程,了解如何为您的激光枪添加重新装填动作,或者创建一个有趣的地图,并尝试与其他玩家一起使用您的激光枪!

最终代码

ToolController

local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
local tool = script.Parent
local eventsFolder = ReplicatedStorage.Events
local MAX_MOUSE_DISTANCE = 1000
local MAX_LASER_DISTANCE = 500
local FIRE_RATE = 0.3
local timeOfPreviousShot = 0
-- 检查自上次射击以来是否经过足够的时间
local function canShootWeapon()
local currentTime = tick()
if currentTime - timeOfPreviousShot < FIRE_RATE then
return false
end
return true
end
local function getWorldMousePosition()
local mouseLocation = UserInputService:GetMouseLocation()
-- 从 2D 鼠标位置创建一条射线
local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
-- 射线的单位方向向量乘以最大距离
local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
-- 从射线的原点朝其方向进行射线投射
local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
if raycastResult then
-- 返回交点的 3D 点
return raycastResult.Position
else
-- 没有物体被击中,因此计算射线末端的位置
return screenToWorldRay.Origin + directionVector
end
end
local function fireWeapon()
local mouseLocation = getWorldMousePosition()
-- 计算归一化的方向向量并乘以激光距离
local targetDirection = (mouseLocation - tool.Handle.Position).Unit
-- 发射武器的方向乘以最大距离
local directionVector = targetDirection * MAX_LASER_DISTANCE
-- 忽略玩家的角色以防止他们自我伤害
local weaponRaycastParams = RaycastParams.new()
weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
-- 检查在起始位置和结束位置之间是否击中了任何物体
local hitPosition
if weaponRaycastResult then
hitPosition = weaponRaycastResult.Position
-- 击中的实例将是角色模型的子级
-- 如果在模型中找到人形,则很可能是玩家的角色
local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
if characterModel then
local humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")
if humanoid then
eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
end
end
else
-- 根据最大激光距离计算结束位置
hitPosition = tool.Handle.Position + directionVector
end
timeOfPreviousShot = tick()
eventsFolder.LaserFired:FireServer(hitPosition)
LaserRenderer.createLaser(tool.Handle, hitPosition)
end
local function toolEquipped()
tool.Handle.Equip:Play()
end
local function toolActivated()
if canShootWeapon() then
fireWeapon()
end
end
tool.Equipped:Connect(toolEquipped)
tool.Activated:Connect(toolActivated)

LaserRenderer

local LaserRenderer = {}
local Debris = game:GetService("Debris")
local SHOT_DURATION = 0.15 -- 激光可见的时间
-- 从起始位置朝向结束位置创建激光束
function LaserRenderer.createLaser(toolHandle, endPosition)
local startPosition = toolHandle.Position
local laserDistance = (startPosition - endPosition).Magnitude
local laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)
local laserPart = Instance.new("Part")
laserPart.Size = Vector3.new(0.2, 0.2, laserDistance)
laserPart.CFrame = laserCFrame
laserPart.Anchored = true
laserPart.CanCollide = false
laserPart.Color = Color3.fromRGB(255, 0, 0)
laserPart.Material = Enum.Material.Neon
laserPart.Parent = workspace
-- 将激光束添加到 Debris 服务中以便移除和清理
Debris:AddItem(laserPart, SHOT_DURATION)
-- 播放武器的射击声音
local shootingSound = toolHandle:FindFirstChild("Activate")
if shootingSound then
shootingSound:Play()
end
end
return LaserRenderer

ServerLaserManager

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local eventsFolder = ReplicatedStorage.Events
local LASER_DAMAGE = 10
local MAX_HIT_PROXIMITY = 10
-- 查找玩家持有的工具的手柄
local function getPlayerToolHandle(player)
local weapon = player.Character:FindFirstChildOfClass("Tool")
if weapon then
return weapon:FindFirstChild("Handle")
end
end
local function isHitValid(playerFired, characterToDamage, hitPosition)
-- 验证被击中角色与击中位置之间的距离
local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
if characterHitProximity > MAX_HIT_PROXIMITY then
return false
end
-- 检查是否穿过墙壁
local toolHandle = getPlayerToolHandle(playerFired)
if toolHandle then
local rayLength = (hitPosition - toolHandle.Position).Magnitude
local rayDirection = (hitPosition - toolHandle.Position).Unit
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {playerFired.Character}
local rayResult = workspace:Raycast(toolHandle.Position, rayDirection * rayLength, raycastParams)
-- 如果击中的是不是角色的实例,则忽略射击
if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
return false
end
end
return true
end
-- 通知所有客户端激光已发射,以便它们可以显示激光
local function playerFiredLaser(playerFired, endPosition)
local toolHandle = getPlayerToolHandle(playerFired)
if toolHandle then
eventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)
end
end
function damageCharacter(playerFired, characterToDamage, hitPosition)
local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")
local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
if humanoid and validShot then
-- 从角色中移除生命值
humanoid.Health -= LASER_DAMAGE
end
end
-- 将事件连接到适当的函数
eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)

ClientLaserManager

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local LaserRenderer = require(Players.LocalPlayer.PlayerScripts:WaitForChild("LaserRenderer"))
local eventsFolder = ReplicatedStorage.Events
-- 显示另一个玩家的激光
local function createPlayerLaser(playerWhoShot, toolHandle, endPosition)
if playerWhoShot ~= Players.LocalPlayer then
LaserRenderer.createLaser(toolHandle, endPosition)
end
end
eventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)
©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。