Hit Detection with Lasers

In this tutorial, you'll learn how to cast a laser from the blaster in Creating Player Tools and detect whether or not it hits a player.

Raycasting to Find Collisions

Raycasting creates an invisible ray from a start position towards a given direction with a defined length. If the ray collides with objects or terrain in its path, it will return information on the collision such as the position and the object it collided with.

Raycast from A towards B colliding with a wall

Finding the Mouse Location

Before a laser can be shot, you must first know where the player is aiming. This can be found by raycasting from the player's 2D mouse location on the screen directly forwards from the camera into the game world. The ray will collide with whatever the player is aiming at with the mouse.

  1. Open the ToolController script inside the Blaster tool from Creating Player Tools. If you haven't completed that tutorial yet you can download the Blaster model and insert it into StarterPack.

  2. At the top of the script, declare a constant named MAX_MOUSE_DISTANCE with a value of 1000.

  3. Create a function called getWorldMousePosition.


    1local tool = script.Parent
    2
    3local MAX_MOUSE_DISTANCE = 1000
    4
    5local function getWorldMousePosition()
    6
    7end
    8
    9local function toolEquipped()
    10 tool.Handle.Equip:Play()
    11end
    12
    13local function toolActivated()
    14 tool.Handle.Activate:Play()
    15end
    16
    17-- Connect events to appropriate functions
    18tool.Equipped:Connect(toolEquipped)
    19tool.Activated:Connect(toolActivated)
    20
  4. Use the GetMouseLocation function of UserInputService to get the player's 2D mouse location on the screen. Assign this to a variable named mouseLocation.


    1local UserInputService = game:GetService("UserInputService")
    2
    3local tool = script.Parent
    4
    5local MAX_MOUSE_DISTANCE = 1000
    6
    7local function getWorldMousePosition()
    8 local mouseLocation = UserInputService:GetMouseLocation()
    9end
    10

Now the 2D mouse location is known, its X and Y properties can be used as parameters for the Camera:ViewportPointToRay() function, which creates a Ray from the screen into the 3D game world.

  1. Use the X and Y properties of mouseLocation as arguments for the ViewportPointToRay() function. Assign this to a variable named screenToWorldRay.


    1local function getWorldMousePosition()
    2 local mouseLocation = UserInputService:GetMouseLocation()
    3
    4 -- Create a ray from the 2D mouse location
    5 local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    6end
    7

It's time to use the Raycast function to check if the ray hits an object. This requires a start position and direction vector: in this example, you will use the origin and direction properties of screenToWorldRay.

The length of the direction vector determines how far the ray will travel. The ray needs to be as long as the MAX_MOUSE_DISTANCE, so you'll have to multiply the direction vector by MAX_MOUSE_DISTANCE.

  1. Declare a variable named directionVector and assign it the value of screenToWorldRay.Direction multiplied by MAX_MOUSE_DISTANCE.


    1local function getWorldMousePosition()
    2 local mouseLocation = UserInputService:GetMouseLocation()
    3
    4 -- Create a ray from the 2D mouseLocation
    5 local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    6
    7 -- The unit direction vector of the ray multiplied by a maximum distance
    8 local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
    9
  2. Call the Raycast function of workspace, passing the Origin property of screenToWorldRay as the first argument and directionVector as the second. Assign this to a variable named raycastResult.


    1local function getWorldMousePosition()
    2 local mouseLocation = UserInputService:GetMouseLocation()
    3
    4 -- Create a ray from the 2D mouseLocation
    5 local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
    6
    7 -- The unit direction vector of the ray multiplied by a maximum distance
    8 local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
    9
    10 -- Raycast from the ray's origin towards its direction
    11 local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
    12

Collision Information

If the raycast operation finds an object hit by the ray, it will return a RaycastResult, which contains information about the collision between the ray and object.

RaycastResult Property Description
Instance The BasePart or Terrain cell that the ray intersected.
Position Where the intersection occurred; usually a point directly on the surface of a part or terrain.
Material The material at the collision point.
Normal The normal vector of the intersected face. This can be used to determine which way the face is pointing.

The Position property will be the position of the object that the mouse is hovering over. If the mouse isn't hovering over any object within a distance of MAX_MOUSE_DISTANCE, raycastResult will be nil.

  1. Create an if statement to check whether raycastResult exists.

  2. If raycastResult has a value, return its Position property.

  3. If raycastResult is nil then find the end of the raycast. Calculate the 3D position of the mouse by adding screenToWorldRay.Origin and directionVector together.


1local function getWorldMousePosition()
2 local mouseLocation = UserInputService:GetMouseLocation()
3
4 -- Create a ray from the 2D mouseLocation
5 local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
6
7 -- The unit direction vector of the ray multiplied by a maximum distance
8 local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
9
10 -- Raycast from the ray's origin towards its direction
11 local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
12
13 if raycastResult then
14 -- Return the 3D point of intersection
15 return raycastResult.Position
16 else
17 -- No object was hit so calculate the position at the end of the ray
18 return screenToWorldRay.Origin + directionVector
19 end
20end
21

Firing Towards the Target

Now that the 3D mouse position is known, it can be used as a target position to fire a laser towards. A second ray can be cast between the player's weapon and the target position using the Raycast function.

  1. Declare a constant called MAX_LASER_DISTANCE at the top of the script and assign it to 500, or your chosen range for the laser blaster.


    1local UserInputService = game:GetService("UserInputService")
    2
    3local tool = script.Parent
    4
    5local MAX_MOUSE_DISTANCE = 1000
    6local MAX_LASER_DISTANCE = 500
    7
  2. Create a function called fireWeapon under the getWorldMousePosition function.

  3. Call getWorldMousePosition and assign the result to a variable named mousePosition. This will be the target position for raycast.


    1 -- No object was hit so calculate the position at the end of the ray
    2 return screenToWorldRay.Origin + directionVector
    3 end
    4end
    5
    6local function fireWeapon()
    7 local mouseLocation = getWorldMousePosition()
    8end
    9
    10local function toolEquipped()
    11 tool.Handle.Equip:Play()
    12end
    13

This time, the direction vector for the raycast function will represent the direction from the player's tool position to the target location.

  1. Declare a variable named targetDirection and calculate the direction vector by subtracting the tool position from mouseLocation.

  2. Normalise the vector by using its Unit property. This gives it a magnitude of 1, which makes it easy to multiply by a length later.


    1local function fireWeapon()
    2 local mouseLocation = getWorldMousePosition()
    3
    4 -- Calculate a normalised direction vector and multiply by laser distance
    5 local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    6end
    7
  3. Declare a variable named directionVector and assign to it the targetDirection multiplied by the MAX_LASER_DISTANCE.


    1 local targetDirection = (mouseLocation - tool.Handle.Position).Unit
    2
    3 -- The direction to fire the weapon, multiplied by a maximum distance
    4 local directionVector = targetDirection * MAX_LASER_DISTANCE
    5end
    6

A RaycastParams object can be used to store additional parameters for the raycast function. It will be used in your laser blaster to make sure the raycast doesn't accidentally collide with the player firing the weapon. Any parts included in the FilterDescendantsInstances property of a RaycastParams object will be ignored in the raycast.

  1. Continue the fireWeapon function and declare a variable called weaponRaycastParams. Assign a new RaycastParams object to it.

  2. Create a table containing the player's local character and assign it to the weaponRaycastParams.FilterDescendantsInstances property.

  3. Raycast from the player's tool handle position, in a direction towards the directionVector. Remember to add weaponRaycastParams as an argument this time. Assign this to a variable named weaponRaycastResult.


1local UserInputService = game:GetService("UserInputService")
2local Players = game:GetService("Players")
3
4local tool = script.Parent
5
6local MAX_MOUSE_DISTANCE = 1000
7local MAX_LASER_DISTANCE = 500
8
9local function getWorldMousePosition()
10

1local function fireWeapon()
2 local mouseLocation = getWorldMousePosition()
3
4 -- Calculate a normalised direction vector and multiply by laser distance
5 local targetDirection = (mouseLocation - tool.Handle.Position).Unit
6
7 -- The direction to fire the weapon multiplied by a maximum distance
8 local directionVector = targetDirection * MAX_LASER_DISTANCE
9
10 -- Ignore the player's character to prevent them from damaging themselves
11 local weaponRaycastParams = RaycastParams.new()
12 weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
13 local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
14end
15

Finally, you'll need to check that the raycast operation returned a value. If a value is returned, an object was hit by the ray and a laser can be created between the weapon and hit location. If nothing was returned, the final position needs to be calculated in order to create the laser.

  1. Declare an empty variable named hitPosition.

  2. Use an if statement to check whether weaponRaycastResult has a value. If an object was hit, assign weaponRaycastResult.Position to hitPosition.


    1 local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
    2
    3 -- Check if any objects were hit between the start and end position
    4 local hitPosition
    5 if weaponRaycastResult then
    6 hitPosition = weaponRaycastResult.Position
    7 end
    8
  3. If weaponRaycastResult has no value, calculate the end position of the raycast by adding together the position of the tool handle with the directionVector. Assign this to hitPosition.


    1 local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
    2
    3 -- Check if any objects were hit between the start and end position
    4 local hitPosition
    5 if weaponRaycastResult then
    6 hitPosition = weaponRaycastResult.Position
    7 else
    8 -- Calculate the end position based on maximum laser distance
    9 hitPosition = tool.Handle.Position + directionVector
    10 end
    11end
    12
  4. Navigate to the toolActivated function and call the fireWeapon function so that the laser fires each time the tool is activated.


    1local function toolActivated()
    2 tool.Handle.Activate:Play()
    3 fireWeapon()
    4end
    5

Checking the Object Hit

To find if the object hit by the laser is part of a player's character or just a piece of scenery, you'll need to look for a Humanoid, as every character has one.

First, you'll need to find the character model. If a part of the character was hit, you cannot assume the parent of the object hit would be the character. The laser could have hit a body part, an accessory, or a tool, all of which are located in different parts of the character's hierarchy.

You can use FindFirstAncestorOfClass to find a character model ancestor of the object hit by the laser, if one exists. If you find a model and it contains a humanoid, in most cases you can assume it's a character.

  1. Add the highlighted code below to the weaponRaycastResult if statement to check if a character was hit.


    1 -- Check if any objects were hit between the start and end position
    2 local hitPosition
    3 if weaponRaycastResult then
    4 hitPosition = weaponRaycastResult.Position
    5
    6 -- The instance hit will be a child of a character model
    7 -- If a humanoid is found in the model then it's likely a player's character
    8 local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
    9 if characterModel then
    10 local humanoid = characterModel:FindFirstChild("Humanoid")
    11 if humanoid then
    12 print("Player hit")
    13 end
    14 end
    15 else
    16 -- Calculate the end position based on maximum laser distance
    17 hitPosition = tool.Handle.Position + directionVector
    18 end
    19

Now the laser blaster should print Player hit to the output window every time the raycast operation hits another player.

Testing with Multiple Players

Two players are needed to test if the weapon raycast is finding other players, so you need to start a local server.

  1. Select the Test tab in Studio.

  2. Make sure the players dropdown is set to '2 Players' and click the Start button to start a local server with 2 clients. Three windows will appear. The first window will be the local server, the other windows will be the clients for Player1 and Player2.

  3. On one client, test shooting the other player with the weapon by clicking on them. "Player hit" should be displayed in the output each time a player is shot.

You can find out more about the Test tab here.

Finding the Laser Position

The blaster should fire a red beam of light at its target. The function for this will be inside a ModuleScript so it can be reused in other scripts later on. First, the script will need to find the position where the laser beam should be rendered.

  1. Create a ModuleScript named LaserRenderer, parented to StarterPlayerScripts under StarterPlayer.

  2. Open the script and rename the module table to the name of the script LaserRenderer.

  3. Declare a variable named SHOT_DURATION with a value of 0.15. This will be the amount of time (in seconds) the laser is visible for.

  4. Create a function of LaserRenderer named createLaser with two parameters called toolHandle and endPosition.


    1local LaserRenderer = {}
    2
    3local SHOT_DURATION = 0.15 -- Time that the laser is visible for
    4
    5-- Create a laser beam from a start position towards an end position
    6function LaserRenderer.createLaser(toolHandle, endPosition)
    7
    8end
    9
    10return LaserRenderer
    11
  5. Declare a variable named startPosition and set the Position property of toolHandle as its value. This will be the position of the player's laser blaster.

  6. Declare a variable named laserDistance and subtract endPosition from startPosition to find the difference between the two vectors. Use the Magnitude property of this to get the length of the laser beam.


    1function LaserRenderer.createLaser(toolHandle, endPosition)
    2 local startPosition = toolHandle.Position
    3
    4 local laserDistance = (startPosition - endPosition).Magnitude
    5end
    6
  7. Declare a laserCFrame variable to store the position and orientation of the laser beam. The position needs to be the midpoint of the start and end of the beam. Use CFrame.lookAt to create a new CFrame located at startPosition and facing towards endPosition. Multiply this by a new CFrame with a Z axis value of half of negative laserDistance to get the midpoint.


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

Creating the Laser Part

Now that you know where to create a laser beam, you need to add the beam itself. This can be done easily with a Neon part.

  1. Declare a variable laserPart and assign to it a new Part instance.

  2. Set the following properties of 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) (a strong red color)
    6. Material: Enum.Material.Neon
  3. Parent laserPart to Workspace.

  4. Add the part to the Debris service so that it gets removed after the amount of seconds in the SHOT_DURATION variable.


    1function LaserRenderer.createLaser(toolHandle, endPosition)
    2 local startPosition = toolHandle.Position
    3
    4 local laserDistance = (startPosition - endPosition).Magnitude
    5 local laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)
    6
    7 local laserPart = Instance.new("Part")
    8 laserPart.Size = Vector3.new(0.2, 0.2, laserDistance)
    9 laserPart.CFrame = laserCFrame
    10 laserPart.Anchored = true
    11 laserPart.CanCollide = false
    12 laserPart.Color = Color3.fromRGB(225, 0, 0)
    13 laserPart.Material = Enum.Material.Neon
    14 laserPart.Parent = workspace
    15
    16 -- Add laser beam to the Debris service to be removed & cleaned up
    17 game.Debris:AddItem(laserPart, SHOT_DURATION)
    18end
    19

Now the function to render the laser beam is complete, it can be called by the ToolController.

  1. At the top of the ToolController script, declare a variable named LaserRenderer and require the LaserRenderer ModuleScript located in PlayerScripts.


    1local UserInputService = game:GetService("UserInputService")
    2local Players = game:GetService("Players")
    3
    4local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
    5
    6local tool = script.Parent
    7
  2. At the bottom of the fireWeapon function, call the LaserRenderer createLaser function using the tool handle and hitPosition as arguments.


    1 -- Calculate the end position based on maximum laser distance
    2 hitPosition = tool.Handle.Position + directionVector
    3 end
    4
    5 LaserRenderer.createLaser(tool.Handle, hitPosition)
    6end
    7
  3. Test the weapon by clicking the Play button. A laser beam should be visible between the weapon and the mouse when the tool is activated.

Controlling Weapon Fire Rate

Weapons need a delay between each shot to stop players from dealing too much damage in a short amount of time. This can be controlled by checking if enough time has passed since a player last fired.

  1. Declare a variable at the top of the ToolController called FIRE_RATE. This will be the minimum time between each shot. Give it a value of your choosing; this example uses 0.3 seconds.

  2. Declare another variable underneath called timeOfPreviousShot with a value of 0. This stores the last time the player fired and will be updated with each shot.


    1local MAX_MOUSE_DISTANCE = 1000
    2local MAX_LASER_DISTANCE = 300
    3local FIRE_RATE = 0.3
    4local timeOfPreviousShot = 0
    5
  3. Create a function called canShootWeapon with no parameters. This function will look at how much time has passed since the previous shot and return true or false.


    1local FIRE_RATE = 0.3
    2local timeOfPreviousShot = 0
    3
    4-- Check if enough time has passed since previous shot was fired
    5local function canShootWeapon()
    6
    7end
    8
    9local function getWorldMousePosition()
    10
  4. Inside the function declare a variable named currentTime; assign to it the result of calling the tick() function. This returns how much time has elapsed, in seconds, since the 1st of January 1970 (an arbitrary date widely used to calculate time).

  5. Subtract the timeOfPreviousShot from currentTime and return false if the result is smaller than FIRE_RATE; otherwise, return true.


    1-- Check if enough time has passed since previous shot was fired
    2local function canShootWeapon()
    3 local currentTime = tick()
    4 if currentTime - timeOfPreviousShot < FIRE_RATE then
    5 return false
    6 end
    7 return true
    8end
    9
  6. At the end of the fireWeapon function, update timeOfPreviousShot each time the weapon is fired using tick.


    1 hitPosition = tool.Handle.Position + directionVector
    2 end
    3
    4 timeOfPreviousShot = tick()
    5
    6 LaserRenderer.createLaser(tool.Handle, hitPosition)
    7end
    8
  7. Inside the toolActivated function, create an if statement and call canShootWeapon to check if the weapon can be fired.


    1local function toolActivated()
    2 if canShootWeapon() then
    3 tool.Handle.Activate:Play()
    4 fireWeapon()
    5 end
    6end
    7

When you test the blaster you should find that no matter how fast you click, there will always be a short 0.3 seconds delay between each shot.

Damaging the Player

Clients cannot damage other clients directly; the server needs to be responsible for issuing damage when a player is hit.

Clients can use a RemoteEvent to tell the server that a character has been hit. These should be stored in ReplicatedStorage, where they are visible to both client and server.

  1. Create a Folder in ReplicatedStorage named Events.

  2. Insert a RemoteEvent into the Events folder and name it DamageCharacter.

  3. In ToolController, create variables at the start of the script for ReplicatedStorage and the Events folder.


    1local UserInputService = game:GetService("UserInputService")
    2local Players = game:GetService("Players")
    3local ReplicatedStorage = game:GetService("ReplicatedStorage")
    4
    5local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
    6
    7local tool = script.Parent
    8local eventsFolder = ReplicatedStorage.Events
    9
    10local MAX_MOUSE_DISTANCE = 1000
    11local MAX_LASER_DISTANCE = 500
    12
  4. Replace the "Player hit" print statement in fireWeapon with a line of Lua to fire the DamageCharacter remote event with the characterModel variable as an argument.


    1 local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
    2 if characterModel then
    3 local humanoid = characterModel:FindFirstChild("Humanoid")
    4 if humanoid then
    5 eventsFolder.DamageCharacter:FireServer(characterModel)
    6 end
    7 end
    8else
    9 -- Calculate the end position based on maximum laser distance
    10 hitPosition = tool.Handle.Position + directionVector
    11end
    12

The server needs to deal damage to the player who has been hit when the event is fired.

  1. Insert a Script into ServerScriptService and name it ServerLaserManager.

  2. Declare a variable named LASER_DAMAGE and set it to 10, or a value of your choice.

  3. Create a function named damageCharacter with two parameters called playerFired and characterToDamage.

  4. Inside the function, find the character's Humanoid and subtract LASER_DAMAGE from its health.

  5. Connect the damageCharacter function to the DamageCharacter remote event in the Events folder.


    1local ReplicatedStorage = game:GetService("ReplicatedStorage")
    2local eventsFolder = ReplicatedStorage.Events
    3local LASER_DAMAGE = 10
    4
    5function damageCharacter(playerFired, characterToDamage)
    6 local humanoid = characterToDamage:FindFirstChild("Humanoid")
    7 if humanoid then
    8 -- Remove health from character
    9 humanoid.Health -= LASER_DAMAGE
    10 end
    11end
    12
    13-- Connect events to appropriate functions
    14eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
    15
  6. Test the blaster with 2 players by starting a local server. When you shoot the other player, their health will decrease by the number assigned to LASER_DAMAGE.

Rendering Other Player's Laser Beams

Currently, the laser beam is created by the client firing the weapon, so only they will be able to see the laser beam.

If the laser beam was created on the server then everyone would be able to see it. However, there would be a small delay between the client shooting the weapon and the server receiving the information about the shot. This would mean the client shooting the weapon would see a delay between when they activate the weapon and when they see the laser beam; the weapon would feel laggy as a result.

To solve this issue, every client will create their own laser beams. This means the client shooting the weapon will see the laser beam instantly. Other clients will experience a small delay between when another player shoots and a beam appears. This is the best case scenario: there is no way to communicate one client's laser to other clients any faster.

Shooter's Client

First, the client needs to tell the server it has fired a laser and provide the end position.

  1. Insert a RemoteEvent into the Events folder in ReplicatedStorage and name it LaserFired.

  2. Locate the fireWeapon function in the ToolController script. At the end of the function, fire the LaserFired remote event using hitPosition as an argument.


    1 hitPosition = tool.Handle.Position + directionVector
    2 end
    3
    4 timeOfPreviousShot = tick()
    5
    6 eventsFolder.LaserFired:FireServer(hitPosition)
    7 LaserRenderer.createLaser(tool.Handle, hitPosition)
    8end
    9

The Server

The server now must receive the event that the client has fired and tell all clients the start and end position of the laser beam so they can also render it.

  1. In the ServerLaserManager script, create a function named playerFiredLaser above damageCharacter with two parameters called playerFired and endPosition.

  2. Connect the function to the LaserFired remote event.


    1-- Notify all clients that a laser has been fired so they can display the laser
    2local function playerFiredLaser(playerFired, endPosition)
    3
    4end
    5

    1-- Connect events to appropriate functions
    2eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
    3eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)
    4

The server needs the start position of the laser. This could be sent from the client, but it's best to avoid trusting the client where possible. The character's weapon handle position will be the start position, so the server can find it from there.

  1. Create a function getPlayerToolHandle above the playerFiredLaser function with a parameter called player.

  2. Use the following code to search the player's character for the weapon and return the handle object.


    1local LASER_DAMAGE = 10
    2
    3-- Find the handle of the tool the player is holding
    4local function getPlayerToolHandle(player)
    5 local weapon = player.Character:FindFirstChildOfClass("Tool")
    6 if weapon then
    7 return weapon:FindFirstChild("Handle")
    8 end
    9end
    10
    11-- Notify all clients that a laser has been fired so they can display the laser
    12local function playerFiredLaser(playerFired, endPosition)
    13

The server can now call FireAllClients on the LaserFired remote event to send the information required to render the laser to the clients. This includes the player who fired the laser (so the client for that player does not render the laser twice), the handle of the blaster (which acts as a starting position for the laser) and the end position of the laser.

  1. In the playerFiredLaser function, call the getPlayerToolHandle function with playerFired as an argument and assign the value to a variable named toolHandle.

  2. If toolHandle exists, fire the LaserFired event for all clients using playerFired, toolHandle and endPosition as arguments.


    1-- Notify all clients that a laser has been fired so they can display the laser
    2local function playerFiredLaser(playerFired, endPosition)
    3 local toolHandle = getPlayerToolHandle(playerFired)
    4 if toolHandle then
    5 eventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)
    6 end
    7end
    8

Rendering on the Clients

Now FireAllClients has been called, each client will receive an event from the server to render a laser beam. Each client can reuse the LaserRenderer module from earlier to render the laser beam using the tool's handle position and end position value sent by the server. The player that fired the laser beam in the first place should ignore this event otherwise they'll see 2 lasers.

  1. Create a LocalScript in StarterPlayerScripts called ClientLaserManager.

  2. Inside the script, require the LaserRenderer module.

  3. Create a function named createPlayerLaser with the parameters playerWhoShot, toolHandle and endPosition.

  4. Connect the function to the LaserFired remote event in the Events folder.

  5. In the function, use an if statement to check if playerWhoShot does not equal the LocalPlayer.

  6. Inside the if statement, call the createLaser function from the LaserRenderer module using toolHandle and endPosition as arguments.


    1local Players = game:GetService("Players")
    2local ReplicatedStorage = game:GetService("ReplicatedStorage")
    3
    4local LaserRenderer = require(script.Parent:WaitForChild("LaserRenderer"))
    5
    6local eventsFolder = ReplicatedStorage.Events
    7
    8-- Display another player's laser
    9local function createPlayerLaser(playerWhoShot, toolHandle, endPosition)
    10 if playerWhoShot ~= Players.LocalPlayer then
    11 LaserRenderer.createLaser(toolHandle, endPosition)
    12 end
    13end
    14
    15eventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)
    16
  7. Test the blaster with 2 players by starting a local server. Position each client on different sides of your monitor so you can see both windows at once. When you shoot on one client you should see the laser on the other client.

Sound Effects

The shooting sound effect currently only plays on the client that's shooting the projectile. You'll need to move the code to play the sound so that other players will hear it too.

  1. In the ToolController script, navigate to the toolActivated function and remove the line which plays the Activate sound.


    1 local function toolActivated()
    2 if canShootWeapon() then
    3
    4 fireWeapon()
    5 end
    6 end
    7
  2. At the bottom of the createLaser function in LaserRenderer, declare a variable named shootingSound and use the FindFirstChild() method of toolHandle to check for the Activate sound.

  3. Use an if statement to check if shootingSound exists; if it does, call its Play function.


    1 laserPart.Parent = workspace
    2
    3 -- Add laser beam to the Debris service to be removed & cleaned up
    4 game.Debris:AddItem(laserPart, SHOT_DURATION)
    5
    6 -- Play the weapon's shooting sound
    7 local shootingSound = toolHandle:FindFirstChild("Activate")
    8 if shootingSound then
    9 shootingSound:Play()
    10 end
    11end
    12

Securing Remotes using Validation

If the server isn't checking data from incoming requests, a hacker can abuse remote functions and events and use them to send fake values to the server. It's important to use server-side validation to prevent this.

In its current form, the DamageCharacter remote event is very vulnerable to attack. Hackers could use this event to damage any player they want in the game without shooting them.

Validation is the process of checking that the values being sent to the server are realistic. In this case, the server will need to:

  • Check if the distance between the player and the position hit by the laser is within a certain boundary.
  • Raycast between the weapon that fired the laser and the hit position to make sure the shot was possible and didn't go through any walls.

Client

The client needs to send the server the position hit by the raycast so it can check the distance is realistic.

  1. In ToolController, navigate to the line where the DamageCharacter remote event is fired in the fireWeapon function.

  2. Add hitPosition as an argument.


    1 if characterModel then
    2 local humanoid = characterModel:FindFirstChild("Humanoid")
    3 if humanoid then
    4 eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
    5 end
    6end
    7

Server

The client is now sending an extra parameter through the DamageCharacter remote event, so the ServerLaserManager needs to be adjusted to accept it.

  1. In the ServerLaserManager script, add a hitPosition parameter to the damageCharacter function.


    1function damageCharacter(playerFired, characterToDamage, hitPosition)
    2 local humanoid = characterToDamage:FindFirstChild("Humanoid")
    3 if humanoid then
    4 -- Remove health from character
    5 humanoid.Health -= LASER_DAMAGE
    6 end
    7end
    8
  2. Below the getPlayerToolHandle function, create a function named isHitValid with three parameters: playerFired, characterToDamage and hitPosition.


    1end
    2
    3local function isHitValid(playerFired, characterToDamage, hitPosition)
    4
    5end
    6

The first check will be the distance between the hit position and the character hit.

  1. Declare a variable named MAX_HIT_PROXIMITY at the top of the script and assign it a value of 10. This will be the maximum distance allowed between the hit and character. A tolerance is needed because the character may have moved slightly since the client fired the event.


    1local ReplicatedStorage = game:GetService("ReplicatedStorage")
    2local eventsFolder = ReplicatedStorage.Events
    3local LASER_DAMAGE = 10
    4local MAX_HIT_PROXIMITY = 10
    5
  2. In the isHitValid function, calculate the distance between the character and the hit position. If the distance is larger than MAX_HIT_PROXIMITY then return false.


    1local function isHitValid(playerFired, characterToDamage, hitPosition)
    2 -- Validate distance between the character hit and the hit position
    3 local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
    4 if characterHitProximity > MAX_HIT_PROXIMITY then
    5 return false
    6 end
    7end
    8

The second check will involve a raycast between the weapon fired and the hit position. If the raycast returns an object that isn't the character, you can assume the shot wasn't valid since something was blocking the shot.

  1. Copy the code below to perform this check. Return true at the end of the function: if it reaches the end, all checks have passed.


    1local function isHitValid(playerFired, characterToDamage, hitPosition)
    2 -- Validate distance between the character hit and the hit position
    3 local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
    4 if characterHitProximity > 10 then
    5 return false
    6 end
    7
    8 -- Check if shooting through walls
    9 local toolHandle = getPlayerToolHandle(playerFired)
    10 if toolHandle then
    11 local rayLength = (hitPosition - toolHandle.Position).Magnitude
    12 local rayDirection = (hitPosition - toolHandle.Position).Unit
    13 local raycastParams = RaycastParams.new()
    14 raycastParams.FilterDescendantsInstances = {playerFired.Character}
    15 local rayResult = workspace:Raycast(toolHandle.Position, rayDirection * rayLength, raycastParams)
    16
    17 -- If an instance was hit that was not the character then ignore the shot
    18 if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
    19 return false
    20 end
    21 end
    22
    23 return true
    24end
    25
  2. Declare a variable in the damageCharacter function called validShot. Assign to it the result of a call to the isHitValid function with three arguments: playerFired, characterToDamage and hitPosition.

  3. In the below if statement, add an and operator to check if validShot is true.


    1function damageCharacter(playerFired, characterToDamage, hitPosition)
    2 local humanoid = characterToDamage:FindFirstChild("Humanoid")
    3 local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
    4 if humanoid and validShot then
    5 -- Remove health from character
    6 humanoid.Health -= LASER_DAMAGE
    7 end
    8end
    9

Now the damageCharacter remote event is more secure and will prevent most players from abusing it. Note that some malicious players will often find ways around validation; keeping remote events secure is a continuous effort.

Your laser blaster is now complete, with a basic hit detection system using raycasting. Try the Detecting User Input tutorial to find out how you can add a reloading action to your laser blaster, or create a fun game map and try your laser blaster out with other players!

Final Code

ToolController


1local UserInputService = game:GetService("UserInputService")
2local Players = game:GetService("Players")
3local ReplicatedStorage = game:GetService("ReplicatedStorage")
4
5local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer)
6
7local tool = script.Parent
8local eventsFolder = ReplicatedStorage.Events
9
10local MAX_MOUSE_DISTANCE = 1000
11local MAX_LASER_DISTANCE = 500
12local FIRE_RATE = 0.3
13local timeOfPreviousShot = 0
14
15-- Check if enough time has pissed since previous shot was fired
16local function canShootWeapon()
17 local currentTime = tick()
18 if currentTime - timeOfPreviousShot < FIRE_RATE then
19 return false
20 end
21 return true
22end
23
24local function getWorldMousePosition()
25 local mouseLocation = UserInputService:GetMouseLocation()
26
27 -- Create a ray from the 2D mouse location
28 local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
29
30 -- The unit direction vector of the ray multiplied by a maxiumum distance
31 local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
32
33 -- Raycast from the roy's origin towards its direction
34 local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
35
36 if raycastResult then
37 -- Return the 3D point of intersection
38 return raycastResult.Position
39 else
40 -- No object was hit so calculate the position at the end of the ray
41 return screenToWorldRay.Origin + directionVector
42 end
43end
44
45local function fireWeapon()
46 local mouseLocation = getWorldMousePosition()
47
48 -- Calculate a normalised direction vector and multiply by laser distance
49 local targetDirection = (mouseLocation - tool.Handle.Position).Unit
50
51 -- The direction to fire the weapon, multiplied by a maximum distance
52 local directionVector = targetDirection * MAX_LASER_DISTANCE
53
54 -- Ignore the player's character to prevent them from damaging themselves
55 local weaponRaycastParams = RaycastParams.new()
56 weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
57 local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
58
59 -- Check if any objects were hit between the start and end position
60 local hitPosition
61 if weaponRaycastResult then
62 hitPosition = weaponRaycastResult.Position
63
64 -- The instance hit will be a child of a character model
65 -- If a humanoid is found in the model then it's likely a player's character
66 local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
67 if characterModel then
68 local humanoid = characterModel:FindFirstChild("Humanoid")
69 if humanoid then
70 eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
71 end
72 end
73 else
74 -- Calculate the end position based on maxiumum laser distance
75 hitPosition = tool.Handle.Position + directionVector
76 end
77
78 timeOfPreviousShot = tick()
79
80 eventsFolder.LaserFired:FireServer(hitPosition)
81 LaserRenderer.createLaser(tool.Handle, hitPosition)
82end
83
84local function toolEquipped()
85 tool.Handle.Equip:Play()
86end
87
88local function toolActivated()
89 if canShootWeapon() then
90 fireWeapon()
91 end
92end
93
94tool.Equipped:Connect(toolEquipped)
95tool.Activated:Connect(toolActivated)
96

LaserRenderer


1local LaserRenderer = {}
2
3local SHOT_DURATION = 0.15 -- Time that the laser is visible for
4
5-- Create a laser beam from a start position towards an end position
6function LaserRenderer.createLaser(toolHandle, endPosition)
7 local startPosition = toolHandle.Position
8
9 local laserDistance = (startPosition - endPosition).Magnitude
10 local laserCFrame = CFrame.lookAt(startPosition, endPosition) * CFrame.new(0, 0, -laserDistance / 2)
11
12 local laserPart = Instance.new("Part")
13 laserPart.Size = Vector3.new(0.2, 0.2, laserDistance)
14 laserPart.CFrame = laserCFrame
15 laserPart.Anchored = true
16 laserPart.CanCollide = false
17 laserPart.Color = Color3.fromRGB(255, 0, 0)
18 laserPart.Material = Enum.Material.Neon
19 laserPart.Parent = workspace
20
21 -- Add laser beam to the Debris service to be removed & cleaned up
22 game.Debris:AddItem(laserPart, SHOT_DURATION)
23
24 -- Play the weapon's shooting sound
25 local shootingSound = toolHandle:FindFirstChild("Activate")
26 if shootingSound then
27 shootingSound:Play()
28 end
29end
30
31return LaserRenderer
32
33

ServerLaserManager


1local ReplicatedStorage = game:GetService("ReplicatedStorage")
2local eventsFolder = ReplicatedStorage.Events
3local LASER_DAMAGE = 10
4local MAX_HIT_PROXIMITY = 10
5
6-- Find the handle of the tool the player is holding
7local function getPlayerToolHandle(player)
8 local weapon = player.Character:FindFirstChildOfClass("Tool")
9 if weapon then
10 return weapon:FindFirstChild("Handle")
11 end
12end
13
14local function isHitValid(playerFired, characterToDamage, hitPosition)
15 -- Validate distance between the character hit and the hit position
16 local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
17 if characterHitProximity > MAX_HIT_PROXIMITY then
18 return false
19 end
20
21 -- Check if shooting through walls
22 local toolHandle = getPlayerToolHandle(playerFired)
23 if toolHandle then
24 local rayLength = (hitPosition - toolHandle.Position).Magnitude
25 local rayDirection = (hitPosition - toolHandle.Position).Unit
26 local raycastParams = RaycastParams.new()
27 raycastParams.FilterDescendantsInstances = {playerFired.Character}
28 local rayResult = workspace:Raycast(toolHandle.Position, rayDirection * rayLength, raycastParams)
29
30 -- If an instance was hit that was not the character then ignore the shot
31 if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
32 return false
33 end
34 end
35
36 return true
37end
38
39-- Notify all clients that a laser has been fired so they can display the laser
40local function playerFiredLaser(playerFired, endPosition)
41 local toolHandle = getPlayerToolHandle(playerFired)
42 if toolHandle then
43 eventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)
44 end
45end
46
47function damageCharacter(playerFired, characterToDamage, hitPosition)
48 local humanoid = characterToDamage:FindFirstChild("Humanoid")
49 local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
50 if humanoid and validShot then
51 -- Remove health from character
52 humanoid.Health -= LASER_DAMAGE
53 end
54end
55
56-- Connect events to appropriate functions
57eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
58eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)
59

ClientLaserManager


1local Players = game:GetService("Players")
2local ReplicatedStorage = game:GetService("ReplicatedStorage")
3
4local LaserRenderer = require(Players.LocalPlayer.PlayerScripts:WaitForChild("LaserRenderer"))
5
6local eventsFolder = ReplicatedStorage.Events
7
8-- Display another player's laser
9local function createPlayerLaser(playerWhoShot, toolHandle, endPosition)
10 if playerWhoShot ~= Players.LocalPlayer then
11 LaserRenderer.createLaser(toolHandle, endPosition)
12 end
13end
14
15eventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)
16