在本教程中,您将学习如何从创建玩家工具中的能量枪发射激光,并检测激光是否击中玩家。
射线投射以查找碰撞
射线投射创建一条从起始位置朝给定方向延伸到定义长度的不可见射线。如果射线与其路径上的物体或地形发生碰撞,它将返回关于碰撞的信息,例如碰撞位置和碰撞的物体。

查找鼠标位置
在射击激光之前,您必须首先知道玩家的瞄准方向。这可以通过从玩家的2D鼠标位置直接从相机向前进行射线投射到3D世界中来找到。射线将与玩家鼠标瞄准的任何对象发生碰撞。
在脚本顶部声明一个常量,命名为MAX_MOUSE_DISTANCE,其值为1000。
创建一个名为getWorldMousePosition的函数。
local tool = script.Parentlocal MAX_MOUSE_DISTANCE = 1000local function getWorldMousePosition()endlocal function toolEquipped()tool.Handle.Equip:Play()endlocal function toolActivated()tool.Handle.Activate:Play()end-- 将事件连接到适当的函数tool.Equipped:Connect(toolEquipped)tool.Activated:Connect(toolActivated)使用UserInputService的GetMouseLocation函数获取玩家在屏幕上的2D鼠标位置。将其分配给名为mouseLocation的变量。
local UserInputService = game:GetService("UserInputService")local tool = script.Parentlocal MAX_MOUSE_DISTANCE = 1000local function getWorldMousePosition()local mouseLocation = UserInputService:GetMouseLocation()end
现在已知2D鼠标位置,其X和Y属性可以用作Camera:ViewportPointToRay()函数的参数,该函数创建一个从屏幕到3D世界的Ray。
使用mouseLocation的X和Y属性作为参数调用Camera:ViewportPointToRay()函数。将其分配给名为screenToWorldRay的变量。
local function getWorldMousePosition()local mouseLocation = UserInputService:GetMouseLocation()-- 从2D鼠标位置创建射线local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)end
现在是使用WorldRoot:Raycast()函数检查射线是否击中对象的时候了。这需要一个起始位置和方向向量:在此示例中,您将使用screenToWorldRay的origin和direction属性。
方向向量的长度决定了射线将会传播多远。射线需要与MAX_MOUSE_DISTANCE一样长,因此您需要将方向向量乘以MAX_MOUSE_DISTANCE。
声明一个名为directionVector的变量,并将其值赋为screenToWorldRay.Direction乘以MAX_MOUSE_DISTANCE。
local function getWorldMousePosition()local mouseLocation = UserInputService:GetMouseLocation()-- 射线单位方向向量乘以最大距离local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE调用工作区的WorldRoot:Raycast()函数,将screenToWorldRay的Origin属性作为第一个参数,将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 属性 | 描述 |
|---|---|
| 实例 | 与射线相交的BasePart或Terrain单元。 |
| 位置 | 相交发生的位置;通常为零件或地形表面上的一个点。 |
| 材质 | 碰撞点的材质。 |
| 法向量 | 相交面的法向量。可以用于确定面朝向哪个方向。 |
位置属性将是鼠标悬停的对象的位置信息。如果鼠标没有在MAX_MOUSE_DISTANCE范围内悬停任何对象,raycastResult将为nil。
创建一个if语句来检查raycastResult是否存在。
如果raycastResult具有值,返回它的位置属性。
如果raycastResult为nil,则找到射线投射的末端。通过将screenToWorldRay.Origin和directionVector相加来计算鼠标的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
-- 返回三维交点
return raycastResult.Position
else
-- 没有对象被击中,计算射线末端的位置
return screenToWorldRay.Origin + directionVector
end
end
朝目标射击
现在已知3D鼠标位置,可以将其用作目标位置来朝该位置发射激光。可以使用射线投射函数在玩家的武器和目标位置之间进行第二次射线投射。
在脚本顶部声明一个名为MAX_LASER_DISTANCE的常量,并将其赋值为500,或您选择的激光能量枪的射程。
local UserInputService = game:GetService("UserInputService")local tool = script.Parentlocal MAX_MOUSE_DISTANCE = 1000local MAX_LASER_DISTANCE = 500在getWorldMousePosition函数下创建一个名为fireWeapon的函数。
调用getWorldMousePosition并将结果分配给名为mousePosition的变量。这将是射线投射的目标位置。
-- 没有对象被击中,计算射线末端的位置return screenToWorldRay.Origin + directionVectorendendlocal function fireWeapon()local mouseLocation = getWorldMousePosition()endlocal function toolEquipped()tool.Handle.Equip:Play()end
这次,射线投射函数的方向向量将表示从玩家的工具位置到目标位置的方向。
声明一个名为targetDirection的变量,通过从mouseLocation中减去工具位置来计算方向向量。
使用其Unit属性对向量进行归一化。这使得后续乘以长度时变得简单。
local function fireWeapon()local mouseLocation = getWorldMousePosition()-- 计算单位方向向量并乘以激光距离local targetDirection = (mouseLocation - tool.Handle.Position).Unitend声明一个名为directionVector的变量,并将其赋值为targetDirection乘以MAX_LASER_DISTANCE。
local targetDirection = (mouseLocation - tool.Handle.Position).Unit-- 发射武器的方向,乘以最大距离local directionVector = targetDirection * MAX_LASER_DISTANCEend
可以使用RaycastParams对象来存储射线投射函数的其他参数。在激光能量枪中使用它以确保射线投射不意外碰撞到开火的玩家。在RaycastParams对象的FilterDescendantsInstances属性中包含的任何部分都将在射线投射中被忽略。
继续fireWeapon函数并声明一个名为weaponRaycastParams的变量。将一个新的RaycastParams对象赋值给它。
创建一个表,包含玩家的本地character并将其分配给weaponRaycastParams.FilterDescendantsInstances属性。
从玩家的工具手柄位置最大的方向进行射线投射,记得这次将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
最后,您需要检查射线投射操作是否返回了值。如果返回了值,说明射线击中了一些对象并且可以在武器和击中位置之间创建激光。如果什么也没有返回,则需要计算最终位置以创建激光。
声明一个空变量命名为hitPosition。
使用if语句检查weaponRaycastResult是否有值。如果某个对象被击中,赋值weaponRaycastResult.Position给hitPosition。
local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)-- 检查在起止位置之间是否有对象被击中local hitPositionif weaponRaycastResult thenhitPosition = weaponRaycastResult.Positionend如果weaponRaycastResult没有值,则通过将工具手柄的位置与directionVector相加来计算射线投射的终点位置。将此赋值给hitPosition。
local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)-- 检查在起始和结束位置之间是否有对象被击中local hitPositionif weaponRaycastResult thenhitPosition = weaponRaycastResult.Positionelse-- 根据最大激光距离计算结束位置hitPosition = tool.Handle.Position + directionVectorendend导航至toolActivated函数,并调用fireWeapon函数,以便在每次激活工具时发射激光。
local function toolActivated()tool.Handle.Activate:Play()fireWeapon()end
检查命中对象
要找出激光命中的是玩家的角色的一部分,还是只是风景的某一部分,您需要寻找一个Humanoid,因为每个角色都有一个。
首先,您需要找到角色模型。如果角色的某个部位被击中,不能假设被击中对象的父对象是角色。激光可能击中了角色的身体部位、配件或工具,所有这些都位于角色层次结构的不同部分中。

您可以使用FindFirstAncestorOfClass来查找被激光击中的对象的角色模型祖先。如果找到一个模型,并且它包含一个humanoid,在大多数情况下,您可以假设这是一个角色。
在weaponRaycastResult if语句中添加高亮代码,以检查是否击中角色。
-- 检查在起始和结束位置之间是否有对象被击中local hitPositionif weaponRaycastResult thenhitPosition = weaponRaycastResult.Position-- 击中的实例将是角色模型的一个子项-- 如果在模型中找到了人形,那么它很可能是玩家的角色local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")if characterModel thenlocal humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")if humanoid thenprint("玩家被击中")endendelse-- 根据最大激光距离计算结束位置hitPosition = tool.Handle.Position + directionVectorend
现在,每次射线投射操作击中另一个玩家时,激光能量枪应在输出窗口中打印“玩家被击中”。
使用多个玩家进行测试
需要两个玩家来测试武器射线是否能够找到其他玩家,因此您需要启动一个本地服务器。有关说明,请参阅多客户端模拟。
在一个客户端上,通过点击另一个玩家来测试射击该玩家。每次点击时,输出中应显示“玩家被击中”。
查找激光位置
能量枪应用程序应在其目标上发射一束红色光束。这个功能将放在一个ModuleScript中,以便以后可以在其他脚本中重用。首先,脚本必须找到激光束应该渲染的位置。
创建一个名为LaserRenderer的ModuleScript,将其父级设置为StarterPlayerScripts下的StarterPlayer。

打开脚本并将模块表重命名为脚本的名称LaserRenderer。
声明一个名为SHOT_DURATION的变量,并将其值设置为0.15。这将是激光可见的时长(以秒为单位)。
创建一个名为createLaser的LaserRenderer函数,带有两个参数,分别为toolHandle和endPosition。
local LaserRenderer = {}local SHOT_DURATION = 0.15 -- 激光可见的时间-- 从起始位置朝向结束位置创建激光束function LaserRenderer.createLaser(toolHandle, endPosition)endreturn LaserRenderer声明一个名为startPosition的变量,并设置toolHandle的Position属性作为其值。这将是玩家的激光能量枪的位置。
声明一个名为laserDistance的变量,并将endPosition从startPosition中减去,以找出两个向量之间的差异。使用Magnitude属性获取激光束的长度。
function LaserRenderer.createLaser(toolHandle, endPosition)local startPosition = toolHandle.Positionlocal laserDistance = (startPosition - endPosition).Magnitudeend声明一个laserCFrame变量以存储激光束的位置和方向。位置需要是激光束起点和终点的中点。使用CFrame.lookAt创建一个新的CFrame,位于startPosition并面向endPosition。将其乘以一个新的CFrame,其中Z轴值为负laserDistance的一半,以获得中点。
function LaserRenderer.createLaser(toolHandle, endPosition)local startPosition = toolHandle.Positionlocal laserDistance = (startPosition - endPosition).Magnitudelocal laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)end
创建激光部件
现在您知道了在哪里创建激光束,接下来需要添加激光束本身。可以使用发光的部件轻松实现这一点。
声明一个变量laserPart并为其分配一个新的Part实例。
设置laserPart的以下属性:
- Size: Vector3.new(0.2, 0.2, laserDistance)
- CFrame: laserCFrame
- Anchored: true
- CanCollide: false
- Color: Color3.fromRGB(225, 0, 0)(一种强烈的红色)
- Material: Enum.Material.Neon
将laserPart的父级设置为工作区。
将该部件添加到Debris服务中,以便在SHOT_DURATION变量中的秒数后将其移除。
function LaserRenderer.createLaser(toolHandle, endPosition)local startPosition = toolHandle.Positionlocal laserDistance = (startPosition - endPosition).Magnitudelocal 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 = laserCFramelaserPart.Anchored = truelaserPart.CanCollide = falselaserPart.Color = Color3.fromRGB(225, 0, 0)laserPart.Material = Enum.Material.NeonlaserPart.Parent = workspace-- 将激光束添加到Debris服务中以被移除和清理Debris:AddItem(laserPart, SHOT_DURATION)end
现在用于呈现激光束的函数已完成,可以在ToolController中调用它。
在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在fireWeapon函数的末尾调用LaserRenderer的createLaser函数,使用工具手柄和hitPosition作为参数。
-- 根据激光的最大距离计算结束位置hitPosition = tool.Handle.Position + directionVectorendLaserRenderer.createLaser(tool.Handle, hitPosition)end通过点击播放按钮测试武器。当激活工具时,应可见激光束从武器发射到鼠标位置。
控制武器射击速度
武器在每次射击之间需要有延迟,以阻止玩家在短时间内造成太多伤害。这可以通过检查自上次射击以来是否经过足够的时间来控制。
在ToolController的顶部声明一个名为FIRE_RATE的变量。这将是每次射击之间的最小时间。给它一个您选择的值;这个示例使用0.3秒。
在下面声明另一个变量,命名为timeOfPreviousShot,其值为0。这存储玩家上次射击的时间,并将在每次射击时更新。
local MAX_MOUSE_DISTANCE = 1000local MAX_LASER_DISTANCE = 300local FIRE_RATE = 0.3local timeOfPreviousShot = 0创建一个名为canShootWeapon的函数,不带参数。此函数将查看自上次射击以来经过了多少时间,并返回true或false。
local FIRE_RATE = 0.3local timeOfPreviousShot = 0-- 检查自上次射击以来是否经过了足够的时间local function canShootWeapon()endlocal function getWorldMousePosition()在函数内部声明一个名为currentTime的变量;将调用tick()函数的结果赋值给它。该函数返回自1970年1月1日(一个广泛使用的计算时间的任意日期)以来经过的秒数。
从currentTime中减去timeOfPreviousShot,如果结果小于FIRE_RATE则返回false;否则返回true。
-- 检查自上次射击以来是否经过了足够的时间local function canShootWeapon()local currentTime = tick()if currentTime - timeOfPreviousShot < FIRE_RATE thenreturn falseendreturn trueend在fireWeapon函数末尾,使用tick更新每次发射武器时的timeOfPreviousShot。
hitPosition = tool.Handle.Position + directionVectorendtimeOfPreviousShot = tick()LaserRenderer.createLaser(tool.Handle, hitPosition)end在toolActivated函数内部,创建一个if语句,并调用canShootWeapon来检查是否可以射击武器。
local function toolActivated()if canShootWeapon() thentool.Handle.Activate:Play()fireWeapon()endend
当您测试能量枪时,您会发现无论您如何快速点击,每次射击之间始终会有0.3秒的短延迟。
对玩家造成伤害
客户端无法直接对其他客户端造成伤害;服务器需要负责在玩家被击中时发出伤害。
客户端可以使用RemoteEvent告诉服务器角色已被击中。这些应存储在ReplicatedStorage中,以便客户端和服务器都能看到。
在ReplicatedStorage中创建一个名为Events的文件夹。

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

在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.Parentlocal eventsFolder = ReplicatedStorage.Eventslocal MAX_MOUSE_DISTANCE = 1000local MAX_LASER_DISTANCE = 500用一行Luau代码替换fireWeapon中的“玩家被击中”打印语句,以触发DamageCharacter远程事件,将characterModel变量作为参数。
local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")if characterModel thenlocal humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")if humanoid theneventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)endendelse-- 根据最大激光距离计算结束位置hitPosition = tool.Handle.Position + directionVectorend
服务器需要在事件触发时对被击中的玩家造成伤害。
在ServerScriptService中插入一个名为ServerLaserManager的脚本。

声明一个名为LASER_DAMAGE的变量,并将其设置为10,或您选择的值。
创建一个名为damageCharacter的函数,带有两个参数,分别为playerFired和characterToDamage。
在函数内部,找到角色的Humanoid并从其生命值中减去LASER_DAMAGE。
将damageCharacter函数连接到Events文件夹中的DamageCharacter远程事件。
local ReplicatedStorage = game:GetService("ReplicatedStorage")local eventsFolder = ReplicatedStorage.Eventslocal LASER_DAMAGE = 10function damageCharacter(playerFired, characterToDamage)local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")if humanoid then-- 从角色身上移除生命值humanoid.Health -= LASER_DAMAGEendend-- 将事件连接到适当的函数eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)通过启动本地服务器,使用2个玩家测试激光能量枪。当您射击其他玩家时,他们的生命值将减少LASER_DAMAGE中指派的值。
渲染其他玩家的激光束
目前,激光束是由发射武器的客户端创建的,因此只有他们能看到激光束。
如果在服务器上创建激光束,那么每个人都能看到它。然而,会出现客户端发射武器和服务器收到射击信息之间的短暂延迟。这将意味着射击武器的客户端会看到激活武器的动作和激光束出现之间的延迟;因此,武器会感觉到卡顿。
解决此问题的方法是每个客户端将创建自己的激光束。这意味着发射武器的客户端会立即看到激光束。其他客户端在看到另一名玩家射击后,激光束会有一小段延迟。这是最好的情况:没有办法更快地将一个客户端的激光传达给其他客户端。
射手的客户端
首先,客户端需要告诉服务器它已发射激光,并提供结束位置。
在ReplicatedStorage的Events文件夹中插入一个RemoteEvent并命名为LaserFired。

在ToolController脚本中找到fireWeapon函数。在函数末尾触发LaserFired远程事件,使用hitPosition作为参数。
hitPosition = tool.Handle.Position + directionVectorendtimeOfPreviousShot = tick()eventsFolder.LaserFired:FireServer(hitPosition)LaserRenderer.createLaser(tool.Handle, hitPosition)end
服务器
服务器现在必须接收客户端发出的事件,并告知所有客户端激光束的起止位置,以便它们也能渲染它。
在ServerLaserManager脚本中,创建一个名为playerFiredLaser的函数,位于damageCharacter之上,带有两个参数,分别为playerFired和endPosition。
将该函数连接到LaserFired远程事件。
-- 通知所有客户端,激光已被发射,以便它们可以显示激光local function playerFiredLaser(playerFired, endPosition)end-- 将事件连接到适当的函数eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)
服务器需要激光的起始位置。这可以从客户端发送,但最好在可能的情况下避免信任客户端。角色的武器手柄位置将是起始位置,因此服务器可以从那里找到它。
在playerFiredLaser函数上面创建一个函数getPlayerToolHandle,带有一个参数player。
使用以下代码在玩家的角色中搜索武器,并返回手柄对象。
local LASER_DAMAGE = 10-- 找到玩家手中工具的手柄local function getPlayerToolHandle(player)local weapon = player.Character:FindFirstChildOfClass("Tool")if weapon thenreturn weapon:FindFirstChild("Handle")endend-- 通知所有客户端,激光已被发射,以便它们可以显示激光local function playerFiredLaser(playerFired, endPosition)
现在服务器可以在LaserFired远程事件上调用FireAllClients,以将渲染激光所需的信息发送给客户。这包括发射者(以便发射激光的客户端不会重复渲染激光)、手柄(作为激光的起始位置)和激光的结束位置。
在playerFiredLaser函数中,调用getPlayerToolHandle函数,参数为playerFired,并将值分配给名为toolHandle的变量。
如果toolHandle存在,则使用playerFired、toolHandle和endPosition作为参数触发LaserFired事件。
-- 通知所有客户端,激光已被发射,以便它们可以显示激光local function playerFiredLaser(playerFired, endPosition)local toolHandle = getPlayerToolHandle(playerFired)if toolHandle theneventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)endend
客户端渲染
现在FireAllClients已被调用,每个客户端将接收来自服务器的事件,以渲染激光束。每个客户端可以重用之前的LaserRenderer模块,以使用工具的手柄位置和服务器发送的结束位置值来渲染激光束。发射激光束的玩家应忽略此事件,否则他们将看到两条激光束。
在StarterPlayerScripts中创建一个名为ClientLaserManager的LocalScript。

在脚本中,要求LaserRenderer模块。
创建一个名为createPlayerLaser的函数,使用参数playerWhoShot、toolHandle和endPosition。
将此函数连接到Events文件夹中的LaserFired远程事件。
在该函数中,使用if语句检查playerWhoShot是否不等于LocalPlayer。
在if语句内,调用LaserRenderer模块中的createLaser函数,使用toolHandle和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 thenLaserRenderer.createLaser(toolHandle, endPosition)endendeventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)通过启动一个本地服务器,使用2个玩家测试激光能量枪。将每个客户端放置在显示器的不同侧,以便您可以同时看到两个窗口。当您在一个客户端上射击时,您应该在另一个客户端上看到激光束。
音效
目前,射击音效仅在发射弹丸的客户端播放。您需要将播放声音的代码移动,以便其他玩家也会听到它。
在ToolController脚本中,进入toolActivated函数并删除播放激活声音的行。
local function toolActivated()if canShootWeapon() thenfireWeapon()endend在LaserRenderer中创建激光的createLaser函数末尾,声明一个名为shootingSound的变量,并使用FindFirstChild()方法检查toolHandle中是否存在Activate声音。
使用if语句检查shootingSound是否存在;如果存在,则调用其Play()函数。
laserPart.Parent = workspace-- 将激光束添加到Debris服务中以被移除和清理Debris:AddItem(laserPart, SHOT_DURATION)-- 播放武器的射击声音local shootingSound = toolHandle:FindFirstChild("Activate")if shootingSound thenshootingSound:Play()endend
使用验证来保护远程事件
如果服务器没有检查来自传入请求的数据,黑客可能会滥用远程函数和事件,并利用它们向服务器发送虚假值。使用服务器端验证来防止这种情况非常重要。
在当前形式中,DamageCharacter远程事件非常容易被攻击。黑客可以利用此事件比较其他玩家,而无需射击他们。
验证是检查发送到服务器的值是否合理的过程。在这种情况下,服务器需要:
- 检查玩家与激光击中的位置之间的距离是否在某个边界内。
- 在发射能量枪与击中位置之间进行射线投射,以确保射击是有效的,没有穿过任何墙壁。
客户端
客户端需要向服务器发送射线投射击中的位置,以便它可以检查距离是否合理。
在ToolController中,导航到fireWeapon函数中触发DamageCharacter远程事件的行。
添加hitPosition作为参数。
if characterModel thenlocal humanoid = characterModel:FindFirstChildWhichIsA("Humanoid")if humanoid theneventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)endend
服务器
客户端现在通过DamageCharacter远程事件发送了一个额外参数,因此ServerLaserManager需要进行调整以接受它。
在ServerLaserManager脚本中,将hitPosition参数添加到damageCharacter函数中。
function damageCharacter(playerFired, characterToDamage, hitPosition)local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")if humanoid then-- 从角色身上移除生命值humanoid.Health -= LASER_DAMAGEendend在getPlayerToolHandle函数下创建一个名为isHitValid的函数,带有三个参数:playerFired、characterToDamage和hitPosition。
endlocal function isHitValid(playerFired, characterToDamage, hitPosition)end
进行的第一次检查是在击中位置和角色之间的距离。
在脚本顶部声明一个名为MAX_HIT_PROXIMITY的变量,并将其值设置为10。这将是允许在角色和击中之间的最大距离。需要容忍度,因为角色可能在客户端发出事件时略微移动。
local ReplicatedStorage = game:GetService("ReplicatedStorage")local eventsFolder = ReplicatedStorage.Eventslocal LASER_DAMAGE = 10local MAX_HIT_PROXIMITY = 10在isHitValid函数中,计算角色和击中位置之间的距离。如果距离大于MAX_HIT_PROXIMITY,则返回false。
local function isHitValid(playerFired, characterToDamage, hitPosition)-- 验证击中位置与角色之间的距离local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitudeif characterHitProximity > MAX_HIT_PROXIMITY thenreturn falseendend
第二次检查涉及在发射能量枪与击中位置之间进行射线投射。如果射线投射返回的对象不是角色,您可以假设射击无效,因为被某物体阻挡。
复制以下代码以执行此检查。返回true在函数的最后:如果到达结束,所有检查均已通过。
local function isHitValid(playerFired, characterToDamage, hitPosition)-- 验证击中位置与角色之间的距离local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitudeif characterHitProximity > 10 thenreturn falseend-- 检查是否穿过墙壁local toolHandle = getPlayerToolHandle(playerFired)if toolHandle thenlocal rayLength = (hitPosition - toolHandle.Position).Magnitudelocal rayDirection = (hitPosition - toolHandle.Position).Unitlocal raycastParams = RaycastParams.new()raycastParams.FilterDescendantsInstances = {playerFired.Character}local rayResult = workspace:Raycast(toolHandle.Position, rayDirection * rayLength, raycastParams)-- 如果撞击的实例不是角色,则忽略此射击if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) thenreturn falseendendreturn trueend在damageCharacter函数中声明一个变量为validShot。将其赋值为调用isHitValid函数的结果,参数为playerFired、characterToDamage和hitPosition。
在下方的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_DAMAGEendend
现在,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
-- 返回三维交点
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)
