使用激光进行命中检测

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

在本教程中,你将学习如何从 创建玩家工具 中发射激光,以及是否击中玩家。

投射以查找碰撞

射线投射 从起始位置向定义的方向创建一条隐形射线,长度为定义的。如果射线在其路径上碰到物体或地形,它将返回碰撞信息,例如位置和与之碰撞的对象。

从 A 向 B 发射射线碰撞墙壁

找到鼠标位置

在激光可以发射之前,你必须先知道玩家在哪里瞄准。这可以通过从屏幕上的玩家 2D 鼠标位置直接射出到游戏世界来找到,从相机射出到游戏世界。射线将与玩家用鼠标瞄准的任何东西碰撞。

  1. 创建玩家工具 中打开 Blaster 工具内的 工具控制器脚本。如果你还没有完成那个教程,你可以下载 冲击波 模型并将其插入 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游戏世界创建到3D游戏世界。

  1. 使用 XY 属性的 mouseLocation 作为 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 函数,传递 起源 属性的 screenToWorldRay 作为第一个参数和 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 ,包含有关射线和对象碰撞的信息。

射线投射结果属性描述
实例射线交叉的 BasePartTerrain 细胞。
位置交叉发生的地方;通常是部件或地形表面上的一个点。
材料碰撞点的材料。
正常交叉面的正常向量。这可以用来确定面向哪个方向。

位置 属性将是鼠标悬停的对象位置。如果鼠标没有悬停在距离 MAX_MOUSE_DISTANCE 的任何对象上, raycastResult 将是 nil

  1. 创建一个 if 声明来检查 raycastResult 是否存在。

  2. 如果 raycastResult 有值,返回其 位置 属性。

  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鼠标位置已知,它可以用作 目标位置 来发射激光。使用 射线投射 函数可以将第二射线投射到玩家的武器和目标位置之间。

  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 的函数。

  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. 使用其 单位 属性来正常化向量。这使其获得 1 的乘数,以便以后轻松乘以长度。


    local function fireWeapon()
    local mouseLocation = getWorldMousePosition()
    -- 计算普通化方向向量并乘以激光距离
    local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    end
  3. 宣言一个名为 directionVector 的变量,并将其乘以 得到的乘数。


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

一个 RaycastParams 对象可用于存储射线投射函数的额外参数。它将被用于你的激光冲击波,以确保射线投射不会意外地碰到发射武器的玩家。任何包含在 属性的射线参数对象中的部件都将在射线投射中被忽略。

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

  2. 创建包含玩家本地 角色 的表,并将其分配到 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. 使用 如果 声明来检查 weaponRaycastResult 是否有值。如果对象被击中,将 weaponRaycastResult.Position 分配给 hitPosition .


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


    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 如果 声明中,以检查是否击中了字符。


    -- 检查是否在开始和结束位置之间击中了任何对象
    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("Player hit")
    end
    end
    else
    -- 根据最大激光距离计算最终位置
    hitPosition = tool.Handle.Position + directionVector
    end

现在,激光冲击波应每次射线投射操作击中另一名玩家时打印 Player hit 到输出窗口。

与多个玩家进行测试

需要两名玩家来测试武器射线是否正在找到其他玩家,因此您需要启动本地服务器。

  1. 在 Studio 中选择 测试 标签。

  2. 确保玩家下拉菜单设置为“2 名玩家”,然后单击启动按钮以 启动 本地服务器,配备 2 个客户端。会出现三个窗口。第一个窗口将是本地服务器,其他窗口将是 Player1 和 Player2 的客户端。

  3. 在一个客户端上,通过单击其他玩家来测试武器射击另一个玩家。“玩家命中”应在每次射击玩家时显示在输出中。

您可以在这里 了解更多关于测试 选项卡 的信息

找到激光位置

爆破器应向目标发射红色光束。为此函数将在 ModuleScript 内,以便在稍后的其他脚本中重复使用。首先,脚本需要找到激光束应该渲染的位置。

  1. 创建一个名为 LaserRenderer 的模块脚本,其父辈为 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 的变量,并将 位置 属性设置为其值。这将是玩家激光发射器的位置。

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


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


    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

创建激光部件

现在你知道在哪里创建激光束了,你需要添加激光束本身。这可以用霓虹灯部件轻松完成。

  1. 宣言一个变量 laserPart 并为它分配一个新的 Part 实例。

  2. 设置 laserPart 的以下属性:

    1. 尺寸 : Vector3.new(0.2, 0.2, 激光距离)
    2. CFrame :laserCFrame
    3. 已锚定 : true
    4. 可碰撞 : 否
    5. 颜色 :Color3.fromRGB(225, 0, 0)(强烈的红色)
    6. 材质 : Enum.Material.Neon
  3. 父级 laserPart工作区

  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:AddItem(laserPart, SHOT_DURATION)
    end

现在激光束渲染的功能已完成,可以由 工具控制器 调用。

  1. 工具控制器 脚本的顶部,宣言一个名为 LaserRenderer 的变量并要求位于 PlayerScripts 中的 LaserRenderer 模块脚本。


    local UserInputService = game:GetService("UserInputService")
    local Players = game:GetService("Players")
    local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
    local tool = script.Parent
  2. fireWeapon 函数的底部,使用工具处理和 createLaser 作为参数来调用激光渲染器函数 hitPosition


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

控制武器射评分

武器需要在每次射击之间延迟,以防玩家在短时间内造成过多伤害。这可以通过检查玩家最后发射时间已经过足够时间来控制。

  1. 工具控制器 顶部声明一个变量,称为 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 的函数,没有参数。这个函数将查看从上一次射击以来已经过多少时间,然后返回真实或虚假。


    local FIRE_RATE = 0.3
    local timeOfPreviousShot = 0
    -- 检查从上一次射击发出以来已经过足够时间了吗
    local function canShootWeapon()
    end
    local function getWorldMousePosition()
  4. 在函数内声明一个名为 currentTime 的变量;将其分配给调用 tick() 函数的结果。这返回了自 1970 年 1 月 1 日以来,在秒内经过的时间,即 1970 年 1 月 1 日 (一个广泛用于计算时间的随意日期)。

  5. timeOfPreviousShot 减去 currentTime 并返回 false 如果结果小于 FIRE_RATE 否则返回 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 函数内,创建一个 如果 声明,然后调用 canShootWeapon 检查武器是否可以发射。


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

当你测试爆破器时,你应该发现,无论你多快地点按,每次射击之间总是会有短暂的 0.3 秒延迟。

伤害玩家

客户端不能直接伤害其他客户端;服务器需要在玩家被击中时发出伤害,负责发出伤害。

客户端可以使用 RemoteEvent 告诉服务器一个字符已被击中。这些应该存储在 复制存储 中,在那里它们对客户端和服务器都可见。

  1. 在 ReplicatedStorage 中创建一个名为 文件夹 的文件夹,命名为 事件

  2. 将一个远程事件插入到事件文件夹中,命名为 伤害角色 .

  3. 工具控制器 中,在脚本开始时创建变量以复制存储和事件文件夹。


    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. "Player hit" 打印声明中替换 fireWeapon 行为 Luau 发射 伤害角色 远程事件,用 characterModel 变量作为参数。


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

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

  1. 脚本 插入到服务器脚本服务中,并将其命名为 ServerLaserManager

  2. 宣言一个名为 LASER_DAMAGE 的变量并将其设置为 10 或你选择的值。

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

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

  5. 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. 远程事件 插入复制存储中的事件文件夹,并将其命名为 LaserFired

  2. fireWeapon 脚本中找到 **** 函数。在函数结束时,使用 LaserFired 远程事件发射 hitPosition 作为参数。


    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. 创建一个函数 getPlayerToolHandleplayerFiredLaser 函数上方,使用一个参数称为 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)

服务器现在可以调用 FireAllClientsLaserFired 远程事件上发送需要将激光渲染到客户端的信息。这包括发射激光的 玩家 (因此客户端对该玩家的激光不会两次渲染)、激光发射器的 手柄 (作为激光的起始位置) 和激光的最后位置。

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

  2. 如果 工具处理 存在,使用 playerFiredtoolHandleendPosition 作为参数的所有客户都会触发激光发射事件。


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

在客户端上渲染

现在 火焰所有客户端 已被调用,每个客户端都会从服务器收到一个事件来渲染激光束。每个客户端都可以从更早的时候重用 激光渲染器 模块来渲染激光束,使用服务器发送的工具手柄位置和终端位置值来渲染激光束。在第一次发射激光束的玩家应该忽略此事件,否则他们会看到 2 个激光。

  1. 在 StarterPlayerScripts 中创建一个 LocalScript 名为 ClientLaserManager 的脚本。

  2. 在脚本内,需要 LaserRenderer 模块。

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

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

  5. 在函数中,使用 如果 声明来检查是否 不等于 LocalPlayer。

  6. 在 if 声明内,使用 createLasertoolHandle 调用激光渲染器模块的 endPosition 函数作为参数。


    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. 工具控制器 脚本中,导航到 工具激活 函数,并移除播放激活声音的线。


    local function toolActivated()
    if canShootWeapon() then
    fireWeapon()
    end
    end
  2. 在 LaserRenderer 的底部,宣言一个名为 shootingSound 的变量,并使用 方法的 检查是否激活声音。

  3. 使用 如果 声明来检查是否存在 shootingSound;如果存在,调用其 播放 函数。


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

使用验证确保远程安全

如果服务器没有检查收到的请求的数据,黑客可以滥用远程函数和事件并使用它们向服务器发送假值。为了防止这种情况,需要使用 服务器端验证 来防止此情况。

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

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

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

客户

客户端需要向服务器发送射线投射所击位置,以便它可以检查距离是否合理。

  1. 工具控制器 中,导航到在 fireWeapon 函数中发射伤害角色远程事件的线。

  2. hitPosition 添加为参数。


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

服务器

客户端现在通过伤害角色远程事件发送额外参数,因此 服务器激光管理器 需要调整以接受它。

  1. ServerLaserManager 脚本中,将 hitPosition 参数添加到 damageCharacter 函数。


    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 声明中,添加 运营符以检查 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

现在,伤害角色远程事件更安全,将防止大多数玩家滥用它。请注意,一些恶意玩家会经常找到逃避验证的方法;保持远程事件安全是一项持续努力。

你的激光冲击波已完成,具有使用光线投射的基本命中检测系统。尝试检测用户输入教程来找出如何将重新加载操作添加到你的激光冲击波中,或创建有趣的游戏地图并试用你的激光冲击波与其他玩家一起!

最终代验证码

工具控制器


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
-- 从 roy 的起源向其方向发射射线
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)

激光渲染器


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:AddItem(laserPart, SHOT_DURATION)
-- 播放武器的射击声音
local shootingSound = toolHandle:FindFirstChild("Activate")
if shootingSound then
shootingSound:Play()
end
end
return LaserRenderer

服务器激光管理器


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)

客户激光管理器


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)