Dynamic Raycast Cursor for SPH Guns

True-to-Bore Crosshair Integration for SPH Gun System

:white_check_mark:This tutorial should work for any version for Spearhead/SPH
(I am using 1.2.2)

:warning: Important Note:

This tutorial is written this way because SPH does not currently have a dedicated modding system. Any modifications or changes, including this true-to-bore crosshair, must be manually integrated into the CharacterClient LocalScript provided by SPH. SPH’s client code is not fully modular yet, hence direct edits to the script are necessary.

:rotating_light:Disclaimer:
I did not create the DynamicCrosshair Module or the Custom ShiftLock Module. Credit for these modules goes entirely to their original creators listed in the resource section below.

Resources

  • SPH GitHub (Installation & Setup):
    SPH GitHub Repository

  • RBXL File (Full Demo Game):
    crossHairTestingGrounds.rbxl (8.5 MB)
    (Note: This includes additional content and may be difficult to dig through and separate all the systems I have. Follow this tutorial carefully if you just want the crosshair.)

(If you want to use the demo place, publish to roblox, turn on API and Http in settings and set to R6. And you may have to re-upload animations for a gun to see the cursor work properly.)

  • SmoothShiftLock Module (RBXM):
    CustomShiftLock.rbxm (14.8 KB)
    (By CT1)

  • DynamicCrosshair Module (RBXM & Dev Forum Post):
    Dev Forum Post | DynamicCrosshair.rbxm (6.0 KB)
    (Note: For this tutorial to work only use the rbxm file, it has been modified to work with SPH and the “true to bore” nature of this sytem. The post was linked for credit)

Media

Video


What You Need to Do:

1. Ensure SPH is Set Up Properly:

  • SPH Setup: Make sure SPH is set up and the CharacterClient script is located under SPH_Character in StarterCharacterScripts.

2. Add Required Files:

  • Custom Shiftlock Module:
    • Action: Place the custom shiftlock RBXM file into StarterPlayerScripts.
  • Dynamic Crosshair Module:
    • Action: Put the DynamicCrosshair module into a folder called MiscModules in ReplicatedStorage.

3. Create Required Folders & Events:

  • Folder for Bindable Event:
    • Location: In ReplicatedStorage (or your preferred location).
    • Action: Create a folder (or container) for the bindable event named Test2Fire.
    • Purpose: This event tracks camera offset for custom shiftlock.

4. Setup ScreenGui:

  • ScreenGui Name: “CrosshairGui”
  • Location: In StarterGui
  • Action: Add a ScreenGui called “CrosshairGui”. This will hold the crosshair UI.

5. Insert Crosshair Code into SPH Client Script:

  • Action: Place the following code snippets in the exact spots as described below.

Code Integration

:warning: ALL CODE INTEGRATIONS ARE DONE IN CHARACTERCLIENT

A. Build and Initialize the Cursor

  • Location: At the top of your SPH client script (Anywhere below muzzleAttachment is declared)
  • Folder: Ensure DynamicCrosshair module is located in ReplicatedStorage/MiscModules.
  • Code:
local crosshairUI = game.Players.LocalPlayer.PlayerGui:WaitForChild("CrosshairGui")  -- Ensure you have this ScreenGui
local cursor = nil
local DynamicCrosshair = require(game.ReplicatedStorage.MiscModules.DynamicCrosshair)

local function BuildCursor(muzzleAttachment)
    print("BUILDING CURSOR")
    cursor = DynamicCrosshair.New(crosshairUI, 20, 60, 40, 30, false)
    cursor:Size(7, 2)
    cursor:Display({
        BackgroundTransparency = 0.4,
        Image = nil,
        ImageTransparency = 0,
        BackgroundColor3 = Color3.new(0.729412, 0.729412, 0.729412)
    })
    cursor:Enable()
    -- Use the muzzle for bore-based positioning
    if muzzleAttachment then
        cursor:UseMuzzleAttach(muzzleAttachment, 600, rayParams)
    end
end
local function DestroyCursor()
    if cursor then
        cursor:Destroy()
        cursor = nil
    end
end

B. Toggle Cursor Visibility in Aiming

  • Where: Inside the ToggleAiming function.
  • Code:
if toggle then
    if cursor then
        cursor:ToggleVisible(false)
    end
else
    if cursor then
        cursor:ToggleVisible(true)
    end
end

C. Cleanup on Player Death

  • Where: Inside the humanoid.Died event handler.
  • Code:
humanoid.Died:Connect(function()
    -- Other death-related cleanup code...
    DestroyCursor()  -- Clean up the cursor on death
    -- Continue with remaining cleanup...
end)

D. Build Cursor When Equipping a Weapon

  • Where: Inside the character.ChildAdded event where a new weapon is added.
  • Note: Place this code right above the ToggleSprint call.
  • Code:
character.ChildAdded:Connect(function(newChild)
    if newChild:FindFirstChild("SPH_Weapon") and not dead then
        -- Setup new weapon and other variables...
        BuildCursor(gun.Grip:WaitForChild("Muzzle"))  -- Build the cursor using the gun's muzzle attachment
        ToggleSprint(userInputService:IsKeyDown(config.keySprint))
        -- Continue with weapon equip code...
    end
end)

E. Destroy Cursor When Unequipping a Weapon

  • Where: Inside the character.ChildRemoved event handler.
  • Note: If oldChild equals equipped, call DestroyCursor right above setting wepStats = nil.
  • Code:
character.ChildRemoved:Connect(function(oldChild)
    if equipped and oldChild:FindFirstChild("SPH_Weapon") then
        -- Other cleanup code...
        if oldChild == equipped then
            DestroyCursor()  -- Destroy the cursor upon unequipping
            equipped = nil
            wepStats = nil
        end
        -- Continue with remaining cleanup...
    end
end)

F. Update Cursor During Firing (Heartbeat)

  • Where: Inside the Heartbeat function, immediately after playerFire:Fire(curModel.Grip.Muzzle.WorldCFrame).
  • Code:
if cursor then
    cursor:Shove(Vector3.new(
        recoilStats.vertical ,
        math.random(-recoilStats.horizontal, recoilStats.horizontal) ,
        recoilStats.punchMultiplier) * dt * 60)
end

G. Update Cursor Position (RenderStepped)

  • Where: Inside the RenderStepped function, just above the block that checks for first-person body offset.
  • Code:
if cursor then
    local curModel = weaponRig.Weapon:FindFirstChildWhichIsA("Model")
    if curModel then
        local laserPoint = curModel.Grip.Muzzle
        local rayResult = workspace:Raycast(laserPoint.WorldPosition, laserPoint.WorldCFrame.LookVector * 600, rayParams)
        if rayResult then
            cursor:SetWorldPosition(rayResult.Position)
        else
            cursor:SetWorldPosition(laserPoint.WorldPosition + laserPoint.WorldCFrame.LookVector * 600)
        end
    end
end

Use these resources as references or starting points for your integration. Make sure you carefully follow the tutorial above if you’re only adding the crosshair to an existing setup.

5 Likes