Rate this laser gun tool

I just got back into Roblox programming after a long hiatus. These last few days I’ve spent a good amount of work and time to make this laser gun tool.

This laser gun has a starting bullet of 5 bullets, each shot delay is 1 second, and you must reload by pressing “R” on your keyboard. It is a very simple mechanism.

The only challenging part is my take on making it more secure so that it is harder for exploiters to take advantage and mess up the settings of the laser gun. The way on how I did this is by doing checks on the server side including the player’s laser gun’s bullet count, reload state and accuracy. I learnt all of these from the tutorial made by Roblox on the developer hub website.

Here are the scripts involved:

CLIENT

local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local LaserRenderer = require(Players.LocalPlayer.PlayerScripts.LaserRenderer) -- a module designed to render the laser part itself
local eventsFolder = ReplicatedStorage:WaitForChild("Events")

local tool = script.Parent

local RELOAD_ACTION = "ReloadWeapon"
local RELOAD_TIME = 2
local RELOAD_DEBOUNCE = false
local MAX_MOUSE_DISTANCE = 100
local MAX_LASER_DISTANCE = 100
local FIRE_RATE = 1

local timeOfPreviousShot = 0

local function onAction(actionName, inputState, inputObject)
	if not RELOAD_DEBOUNCE then 
		if actionName == RELOAD_ACTION and inputState == Enum.UserInputState.Begin then
			RELOAD_DEBOUNCE = true
			eventsFolder.ReloadWeapon:FireServer(tool)
			tool.TextureId = "rbxassetid://6593020923"
			task.wait(RELOAD_TIME)
			tool.TextureId = "rbxassetid://92628145"
			RELOAD_DEBOUNCE = false
		end
	end
end

local function canShootWeapon()
	-- check timing
	local currentTime = tick()
	if currentTime - timeOfPreviousShot <= FIRE_RATE then
		return false
	end
	-- check bullets
	if tool:GetAttribute("Ammo") <= 0 then
		return false
	end

	return true
end

local function getWorldMousePosition()
	local mousePosition = UserInputService:GetMouseLocation()
	local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
	local directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE

	local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)

	if raycastResult then
		return raycastResult.Position
	else
		return tool.Handle.Position + directionVector
	end
end

local function fireWeapon()
	local mousePosition = getWorldMousePosition()

	local targetPosition = (mousePosition - tool.Handle.Position).Unit
	local directionVector = targetPosition * MAX_LASER_DISTANCE

	local weaponRaycastParams = RaycastParams.new()
	weaponRaycastParams.FilterDescendantsInstances = {Players.LocalPlayer.Character:GetDescendants()}
	weaponRaycastParams.FilterType = Enum.RaycastFilterType.Blacklist

	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()
	
	if not RELOAD_DEBOUNCE then
		LaserRenderer.RenderLaser(tool.Handle, hitPosition)
	end
	eventsFolder.LaserFired:FireServer(hitPosition)
end

local function toolEquipped()
	tool.Handle.Equip:Play()
	ContextActionService:BindAction(RELOAD_ACTION, onAction, false, Enum.KeyCode.R)
end

local function toolActivated()
	if canShootWeapon() then
		fireWeapon()
	else
		
	end
end

local function toolUnequipped()
	ContextActionService:UnbindAction(RELOAD_ACTION)
end

tool.Equipped:Connect(toolEquipped)
tool.Unequipped:Connect(toolUnequipped)
tool.Activated:Connect(toolActivated)

SERVER

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local eventsFolder = ReplicatedStorage.Events

local LASER_DAMAGE = 10
local MAX_AMMO = 5
local MAX_HIT_PROXIMITY = 10
local FIRE_RATE = 1
local playerInfoTable = {}

local function onPlayerAdded(player)
	local key = player.UserId
	local backpack
	local character
	local toolsLoaded = false
	
	-- table format
	playerInfoTable[key] = {
		["IsLoaded"] = toolsLoaded,
		["LastShot"] = 0,
		["Reloading"] = false
	}
	
	-- set events
	player.CharacterAdded:Connect(function(char)
		backpack = player:WaitForChild("Backpack")
		character = char
		
		local blaster = backpack.Blaster
		blaster:SetAttribute("Ammo", 5)
		toolsLoaded = true
		playerInfoTable[key]["IsLoaded"] = true
		
	end)
	
	repeat task.wait() until character
	character:FindFirstChild("Humanoid").Died:Connect(function(char)
		if playerInfoTable[key] then
			
			playerInfoTable[key]["IsLoaded"] = false
		end
	end)
	
	-- setup table
	repeat task.wait() until toolsLoaded
	playerInfoTable[key]["Ammo"] = backpack.Blaster:GetAttribute("Ammo")

end

local function onReloadingWeapon(playerReloaded, tool)
	
	local key = playerReloaded.UserId
	
	if not playerInfoTable[key]["Reloading"] then
		playerInfoTable[key]["Reloading"] = true
		task.wait(2)
		
		tool:SetAttribute("Ammo", MAX_AMMO)
		
		playerInfoTable[key]["Reloading"] = false
	end
end

local function canShootWeapon(playerInfo, tool)
	-- check timing
	local currentTime = tick()
	local timeDifference = currentTime - playerInfo["LastShot"]
	
	if timeDifference <= FIRE_RATE then
		return false
	end
	-- check bullets
	if tool:GetAttribute("Ammo") <= 0 then
		return false
	end
	
	playerInfo["LastShot"] = tick()
	return true
end

local function getPlayerToolHandle(player)
	local character = player.Character
	if character ~= nil then
		local tool = character:FindFirstChild("Blaster")
		if tool then
			return tool.Handle
		end
	end
	return nil
end

local function isHitValid(playerFired, characterToDamage, hitPosition)
	-- check distance
	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 = (toolHandle.Position - hitPosition).Magnitude
		local rayDirection = (toolHandle.Position - hitPosition).Unit
		local raycastParams = RaycastParams.new()
		raycastParams.FilterDescendantsInstances = {playerFired.Character}
		raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
		local raycastResult = workspace:Raycast(toolHandle.Position, rayDirection*rayLength, raycastParams)
		if raycastResult and raycastResult.Instance:IsDescendantOf(playerFired.Character) then
			return false
		end
	end
	
	return true
end

local function playerFiredLaser(playerFired, endPosition)
	local key = playerFired.UserId
	local playerFiredCharacter = playerFired.Character
	local toolHandle = getPlayerToolHandle(playerFired)
	local canShoot = canShootWeapon(playerInfoTable[key], playerFiredCharacter.Blaster)
	local isReloading = playerInfoTable[key]["Reloading"]
	if (toolHandle and canShoot) and not isReloading then
		eventsFolder.LaserFired:FireAllClients(playerFired, toolHandle, endPosition)
		playerInfoTable[key]["Ammo"] -= 1
		toolHandle.Parent:SetAttribute("Ammo", (toolHandle.Parent:GetAttribute("Ammo") - 1))
	end
end

local function damageCharacter(playerFired, characterToDamage, hitPosition)
	local validShot = isHitValid(playerFired, characterToDamage, hitPosition)
	local humanoid = characterToDamage:FindFirstChildWhichIsA("Humanoid")
	if validShot and humanoid then
		humanoid.Health -= 10
	end
end

eventsFolder.DamageCharacter.OnServerEvent:Connect(damageCharacter)
eventsFolder.LaserFired.OnServerEvent:Connect(playerFiredLaser)
eventsFolder.ReloadWeapon.OnServerEvent:Connect(onReloadingWeapon)
Players.PlayerAdded:Connect(onPlayerAdded)

If there are any mistakes or blunders I’ve made in the scripts, please let me know so I can be more cautious and careful in the future when making large systems. Thanks.

1 Like

This might be a good reference for you

Your code looks good and decently
optimised.

Perhaps you could introduce sanity checks on the server to determine if exploiters are trying to fire remotes

You could improve structure potentially implementing OOP however that’s just a preference and would just make it more readable

Your code looks good nevertheless!

isHitValid defaults to true if the weapon isn’t in the character. Exploiters can directly fire the Damage Character remote event (with the hitPosition set to the target character’s root part position) while having the gun unequipped to damage any specific character in the entire map without any restrictions on

  • current available bullets
  • fire range
  • walls between shooter and target
  • the shooter being alive and in the map
  • the shooter having a gun