PcoWSkbVqDnWTu_dm2ix
Hit Detection with Lasers
Part 5 - Securing Remotes using Validation
Hit Detection with Lasers
Part 5 - 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.
        if characterModel then
        local humanoid = characterModel:FindFirstChild("Humanoid")
        if humanoid then
            eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
        end
    end

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.
    function damageCharacter(playerFired, characterToDamage, hitPosition)
        local humanoid = characterToDamage:FindFirstChild("Humanoid")
        if humanoid then
            -- Remove health from character
            humanoid.Health -= LASER_DAMAGE
        end
    end
  2. Below the getPlayerToolHandle function, create a function named isHitValid with three parameters: playerFired, characterToDamage and hitPosition.
    end
    
    local function isHitValid(playerFired, characterToDamage, hitPosition)
     
    end

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.
    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local eventsFolder = ReplicatedStorage.Events
    local LASER_DAMAGE = 10
    local MAX_HIT_PROXIMITY = 10
  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.
    local function isHitValid(playerFired, characterToDamage, hitPosition)
        -- Validate distance between the character hit and the hit position
        local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
        if characterHitProximity > MAX_HIT_PROXIMITY then
            return false
        end
    end

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.
    local function isHitValid(playerFired, characterToDamage, hitPosition)
        -- Validate distance between the character hit and the hit position
        local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
        if characterHitProximity > 10 then
            return false
        end
    
        -- Check if shooting through walls
        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 an instance was hit that was not the character then ignore the shot
            if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
                return false
            end
        end
         
        return true
    end
  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.
    function damageCharacter(playerFired, characterToDamage, hitPosition)
        local humanoid = characterToDamage:FindFirstChild("Humanoid")
        local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
        if humanoid and validShot then
            -- Remove health from character
            humanoid.Health -= LASER_DAMAGE
        end
    end

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 Player Input course 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!

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

-- Check if enough time has pissed since previous shot was fired
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()
	
	-- Create a ray from the 2D mouse location
	local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
	
	-- The unit direction vector of the ray multiplied by a maxiumum distance
	local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
	
	-- Raycast from the roy's origin towards its direction
	local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
	
	if raycastResult then
		-- Return the 3D point of intersection
		return raycastResult.Position
	else
		-- No object was hit so calculate the position at the end of the ray
		return screenToWorldRay.Origin + directionVector
	end
end

local function fireWeapon()
	local mouseLocation = getWorldMousePosition()
	
	-- Calculate a normalised direction vector and multiply by laser distance
	local targetDirection = (mouseLocation - tool.Handle.Position).Unit
	
	-- The direction to fire the weapon, multiplied by a maximum distance
	local directionVector = targetDirection * MAX_LASER_DISTANCE
	
	-- Ignore the player's character to prevent them from damaging themselves
	local weaponRaycastParams = RaycastParams.new()
	weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character}
	local weaponRaycastResult = workspace:Raycast(tool.Handle.Position, directionVector, weaponRaycastParams)
	
	-- Check if any objects were hit between the start and end position
	local hitPosition
	if weaponRaycastResult then
		hitPosition = weaponRaycastResult.Position
		
		-- The instance hit will be a child of a character model
		-- If a humanoid is found in the model then it's likely a player's character
		local characterModel = weaponRaycastResult.Instance:FindFirstAncestorOfClass("Model")
		if characterModel then
			local humanoid = characterModel:FindFirstChild("Humanoid")
			if humanoid then
				eventsFolder.DamageCharacter:FireServer(characterModel, hitPosition)
			end
		end
	else
		-- Calculate the end position based on maxiumum laser distance
		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 SHOT_DURATION = 0.15 -- Time that the laser is visible for

-- Create a laser beam from a start position towards an end position
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
	
	-- Add laser beam to the Debris service to be removed & cleaned up
	game.Debris:AddItem(laserPart, SHOT_DURATION)
	
	-- Play the weapon's shooting sound
	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

-- Find the handle of the tool the player is holding
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)
	-- Validate distance between the character hit and the hit position
	local characterHitProximity = (characterToDamage.HumanoidRootPart.Position - hitPosition).Magnitude
	if characterHitProximity > MAX_HIT_PROXIMITY then
		return false
	end
	
	-- Check if shooting through walls
	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 an instance was hit that was not the character then ignore the shot
		if rayResult and not rayResult.Instance:IsDescendantOf(characterToDamage) then
			return false
		end
	end

	return true
end

-- Notify all clients that a laser has been fired so they can display the laser
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:FindFirstChild("Humanoid")
	local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
	if humanoid and validShot then
		-- Remove health from character
		humanoid.Health -= LASER_DAMAGE
	end
end

-- Connect events to appropriate functions
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

-- Display another player's laser
local function createPlayerLaser(playerWhoShot, toolHandle, endPosition)
	if playerWhoShot ~= Players.LocalPlayer then
		LaserRenderer.createLaser(toolHandle, endPosition)
	end
end

eventsFolder.LaserFired.OnClientEvent:Connect(createPlayerLaser)


Previous Page Client to Server Communication