How should I go about making a hitmarker for my gun system?

local UserInputService = game:GetService("UserInputService")
local RepStorage = game:GetService("ReplicatedStorage")

local idle = script.Parent.Anims.Idle
local reloadanim = script.Parent.Anims.Reload
local kickBack = script.Parent.Anims.Fire
local reloadTrack = nil
local idleTrack = nil
local kickTrack = nil
local equipped = false

local player = game.Players.LocalPlayer
local mouse = player:GetMouse()

local tool = script.Parent
local gui = tool.gui

local max_ammo = 10
local current_ammo = max_ammo
local firemode = "Semi" -- Change this to "semi" or "auto" as needed

local reload_duration = 1
local cooldown_duration = 0.9
local cooldown = false

local firing = false
local reloading = false

-- Recoil
local recoilShakerMod = require(RepStorage.CameraShaker)
local camera = workspace.CurrentCamera
local cameraShake = recoilShakerMod.new(Enum.RenderPriority.Camera.Value, function(shakeCFrame)
	camera.CFrame = camera.CFrame * shakeCFrame
end)

cameraShake:Start()

function recoil()
	cameraShake:Shake(recoilShakerMod.Presets.Bump)
end

local function reload()
	if reloading or current_ammo == max_ammo then return end
	if equipped then
		reloading = true
		reloadTrack:Play()
		tool.Sounds.Reload:Play()
		tool.gunmodel.Ammo.Transparency = 0
		gui.gun_framethingy.ammo.Text = "Reloading..."

		task.wait(tool.Sounds.Reload.TimeLength)
		tool.gunmodel.Ammo.Transparency = 1
		current_ammo = max_ammo
		gui.gun_framethingy.ammo.Text = current_ammo.."/"..max_ammo
		reloading = false
	end
end

local function fire()
	if current_ammo <= 0 then
		reload()
		return
	end
	if reloading then return end
	if cooldown then return end

	current_ammo -= 1
	gui.gun_framethingy.ammo.Text = current_ammo.."/"..max_ammo

	tool.fire:FireServer(tool.Handle.muzzle.WorldPosition, mouse.Hit.Position)
	kickTrack:Play() -- Ensure the kick animation plays each time
	recoil()

	cooldown = true
	task.wait(cooldown_duration)
	cooldown = false

	if current_ammo <= 0 then
		reload()
	end
end

local function startFiring()
	if reloading or cooldown then return end -- Prevent firing if reloading or cooldown is active

	firing = true

	if firemode == "Semi" then
		fire()
	elseif firemode == "Auto" then
		while firing and current_ammo > 0 do
			fire()
			task.wait(cooldown_duration) -- Control auto fire rate
		end
	end
end

local function stopFiring()
	firing = false
end

tool.Activated:Connect(startFiring)
tool.Deactivated:Connect(stopFiring)

UserInputService.InputBegan:Connect(function(input, processed)
	if processed then return end

	if input.KeyCode == Enum.KeyCode.R then
		reload()
	end
end)

UserInputService.TouchStarted:Connect(function(touch, processed)
	if processed then return end
	startFiring()
end)

UserInputService.TouchEnded:Connect(function(touch, processed)
	if processed then return end
	stopFiring()
end)

tool.Equipped:Connect(function()
	gui.Parent = player.PlayerGui
	mouse.Icon = "rbxassetid://12829852445"
	gui.gun_framethingy.ammo.Text = current_ammo.."/"..max_ammo
	local char = player.Character or player.CharacterAdded:Wait()
	local hum = char:WaitForChild("Humanoid")
	kickTrack = hum.Animator:LoadAnimation(kickBack)
	reloadTrack = hum.Animator:LoadAnimation(reloadanim)
	idleTrack = hum.Animator:LoadAnimation(idle)
	idleTrack:Play()
	equipped = true
end)

tool.Unequipped:Connect(function()
	gui.Parent = tool
	idleTrack:Stop()
	mouse.Icon = ""
	equipped = false
end)

So that is my code, but every time I tried to make a hitmarker, it would not work. Can somebody please make a edited version of this code? I’ve kinda ran out of ideas! The code above has no hitmarker code, so feel free to edit it and place it below.

2 Likes

hitmarker is an icon that appears when you get a succsessful shot right?

you would need to change the fire remote event into a remote function and then return true/false from the server depending on if the bullet hit

then make a UI with the hit icon and change its transparency from the client script based on the returned value from the remote function

When you mean return, do you mean at the end of the function or somewhere in the functions
and also how would i go about using the remote function, do i get the function and invoke it? Then how would I check if it hit using true or false?

This is the server script:

local RunService = game:GetService("RunService") -- used for updating the bullets
local tool = script.Parent
local bullets = {} -- table that holds all the active bullets

-- Penetration configuration
local maxPenetrationDepth = 10 -- Maximum penetration depth in studs
local materialPenetrationCosts = {
	[Enum.Material.Plastic] = 1, -- Penetration cost per material
	[Enum.Material.Wood] = 2,
	[Enum.Material.Glass] = 0.5,
}

-- Materials that completely stop penetration
local nonPenetrableMaterials = {
	[Enum.Material.Metal] = true,
	[Enum.Material.Concrete] = true,
}

-- Gravity (studs per second squared)
local gravity = Vector3.new(0, -30, 0)

-- Function to handle impacts
local function impact(raycast)
	local part = Instance.new("Part", workspace.Terrain)
	local decal = Instance.new("Decal", part)
	part.Size = Vector3.new(0.5, 0.5, 0)
	part.Massless = true
	part.Transparency = 1

	part.CanCollide = false
	part.CanQuery = false
	part.CanTouch = false

	part.CFrame = CFrame.new(raycast.Position, raycast.Position + raycast.Normal) * CFrame.Angles(0, 0, math.rad(math.random(0, 360)))
	part.Color = Color3.new()

	decal.Texture = "rbxassetid://12769915043"

	local weld = Instance.new("WeldConstraint", part)
	weld.Part0 = raycast.Instance
	weld.Part1 = part

	game.Debris:AddItem(part, 5)
end

tool.fire.OnServerEvent:Connect(function(player, origin, dir)
	local sfx = tool.Sounds.Fire:Clone()
	sfx.Parent = tool.Handle
	sfx:Play()
	game.Debris:AddItem(sfx, sfx.TimeLength)

	tool.Handle.muzzle["FlashFX[Flash]"].Enabled = true
	tool.Handle.muzzle.smoke.Enabled = true
	tool.Handle.muzzle.FIRE.Enabled = true
	tool.Handle.muzzle.SPECKS.Enabled = true
	tool.Handle.muzzle.GLOW.Enabled = true
	task.wait(0.2)
	tool.Handle.muzzle["FlashFX[Flash]"].Enabled = false
	tool.Handle.muzzle.smoke.Enabled = false
	tool.Handle.muzzle.FIRE.Enabled = false
	tool.Handle.muzzle.SPECKS.Enabled = false
	tool.Handle.muzzle.GLOW.Enabled = false

	local bullet = game.ReplicatedStorage.bullet:Clone()
	bullet.Parent = workspace.Terrain

	bullets[bullet] = {
		origin = origin,
		direction = CFrame.new(origin, dir).LookVector,
		velocity = CFrame.new(origin, dir).LookVector * 1500, -- Initial velocity
		lifetime = tick() + 2;
		firedby = tool.Parent,
		penetrationDepth = maxPenetrationDepth, -- Track remaining penetration depth
		playerPenetrations = 0, -- Track the number of players headshot
	}
end)

RunService.Heartbeat:Connect(function(dt)
	for bullet, data in pairs(bullets) do
		local params = RaycastParams.new()
		params.RespectCanCollide = false
		params.FilterDescendantsInstances = {data.firedby}

		-- Update velocity with gravity
		data.velocity += gravity * dt
		local movement = data.velocity * dt

		-- Raycast with the updated movement
		local ray = workspace:Raycast(data.origin, movement, params)
		if ray then
			local material = ray.Instance.Material

			-- Check if the material completely stops penetration
			if nonPenetrableMaterials[material] then
				impact(ray)
				bullet:Destroy()
				bullets[bullet] = nil
				return
			end

			-- Handle damage and impact
			local hum = ray.Instance.Parent:FindFirstChildOfClass("Humanoid")
			local head = ray.Instance.Parent:FindFirstChild("Head")
			if hum then
				local isHeadshot = ray.Instance == head
				if isHeadshot then
					-- Apply headshot damage and track penetrations
					hum:TakeDamage(165)
					data.playerPenetrations += 1

					-- Stop the bullet after 2 headshot penetrations
					if data.playerPenetrations >= 2 then
						bullet:Destroy()
						bullets[bullet] = nil
						return
					end
				else
					-- Apply normal damage and stop the bullet
					hum:TakeDamage(65)
					bullet:Destroy()
					bullets[bullet] = nil
					return
				end
			end

			impact(ray)

			-- Reduce penetration depth for penetrable materials
			local materialCost = materialPenetrationCosts[material] or 5
			data.penetrationDepth -= materialCost

			-- Check if the bullet can penetrate further
			if data.penetrationDepth > 0 then
				data.origin = ray.Position + (movement.Unit * 0.1) -- Move origin slightly past the impact point
			else
				bullet:Destroy()
				bullets[bullet] = nil
			end
		else
			data.origin += movement
			bullet.CFrame = CFrame.new(data.origin, data.origin + data.velocity)

			if tick() > data.lifetime then
				bullet:Destroy()
				bullets[bullet] = nil
			end
		end
	end
end)

remote function works exactly like a function, but in this case you would call the function on the client and then the server would return a value

on the server you can do

remoteFunction.onServerInvoke = function(something) -- pass arguments from client here
  return 5 -- this gets returned to the client
end

in your case you would replace tool.fire:FireServer with tool.fire:InvokeServer(tool.Handle.muzzle.WorldPosition, mouse.Hit.Position) and then keep the same code on the server but return if the hit is succsessful

hmmm, so like below that line of code, would i just do humanoid code and what not? like checking if the hit has been hit, then i do return ?

RunService.Heartbeat:Connect(function(dt)
	for bullet, data in pairs(bullets) do
		local params = RaycastParams.new()
		params.RespectCanCollide = false
		params.FilterDescendantsInstances = {data.firedby}

		-- Update velocity with gravity
		data.velocity += gravity * dt
		local movement = data.velocity * dt

		-- Raycast with the updated movement
		local ray = workspace:Raycast(data.origin, movement, params)
		if ray then
			local material = ray.Instance.Material

			-- Check if the material completely stops penetration
			if nonPenetrableMaterials[material] then
				impact(ray)
				bullet:Destroy()
				bullets[bullet] = nil
				return
			end

			-- Handle damage and impact
			local hum = ray.Instance.Parent:FindFirstChildOfClass("Humanoid")
			local head = ray.Instance.Parent:FindFirstChild("Head")
			if hum then
				local isHeadshot = ray.Instance == head
				if isHeadshot then
					-- Apply headshot damage and track penetrations
					hum:TakeDamage(165)
					data.playerPenetrations += 1

					-- Stop the bullet after 2 headshot penetrations
					if data.playerPenetrations >= 2 then
						bullet:Destroy()
						bullets[bullet] = nil
						return
					end
				else
					-- Apply normal damage and stop the bullet
					hum:TakeDamage(65)
					bullet:Destroy()
					bullets[bullet] = nil
					return
				end
			end

			impact(ray)

			-- Reduce penetration depth for penetrable materials
			local materialCost = materialPenetrationCosts[material] or 5
			data.penetrationDepth -= materialCost

			-- Check if the bullet can penetrate further
			if data.penetrationDepth > 0 then
				data.origin = ray.Position + (movement.Unit * 0.1) -- Move origin slightly past the impact point
			else
				bullet:Destroy()
				bullets[bullet] = nil
			end
		else
			data.origin += movement
			bullet.CFrame = CFrame.new(data.origin, data.origin + data.velocity)

			if tick() > data.lifetime then
				bullet:Destroy()
				bullets[bullet] = nil
			end
		end
	end
end)

so where would i put the returns? like at the damage code? so if i do that, on the client i check if its true then i do the tweening and transparency?

you shouldn’t check hitmarkers on the server or the response to hitting a target will feel sluggish

you should fire a raycast (on the client) and check if it hit a player model

hm that could work, i will check it out, but last time i did that, the bullet tracer did not show up

1 Like