Accurate Projectile?

( I’m typing on mobile, bare with me.)

Ive seen quite a few posts about projectiles but none go into detail about the best method in producing them or how to create them?

I’m currently working on a Magic system of which a Player sends a projectile from PointA to PointB however on its journey it has the ability to hit objects that cross it’s path - Meaning that a simple PointA > PointB Raycast is out of the question.

Ontop of this, each Projectile will have a unique spell VFX; I’ve read that it’s better to create VFX such as travelling projectiles on the Client(s) and Raycast on the server, understandably, however how can I sync these?

This is a problem I’ve had for a year now on & off creating different methods.

Can anyone shine any light on this for myself? - Ideally I want to avoid the FastCast module as I believe it doesn’t suit me.

I cant provide code because my personal system is way to buggy and unoptimized, but the general idea is

  • move projectile
  • ray the projectile to see if it hits anything
    – if hit, handle hit
  • if not hit, repeat
1 Like

That’s how mine currently working using CFrame however does that not cause lag?
Is it sever sided?

Fast cast. 10/10
Making a combat game with ranged weapons? FastCast may be the module for you! - Resources / Community Resources - DevForum | Roblox

Creating a moving projectile and Raycasting in sync would be just creating it on the client. There is no actual way to sync it because of ping. These are the things you have to do on the client for projectiles:
Raycast,
Effects,
Projectile Movement.

Creating it on the client and detecting hits is better for performance. That’s the only way to see imitate that projectiles look in sync, However there is an issue to this. Since it’s being ray casted on the client you’d have to use Anti-Cheats. Creating one would resolve in checking positions and distances between the 2 positions (Server and Client).

I’ve used the same method when creating projectile weapons for a while now, it goes something like this:

--<< Client has clicked or pressed the keybind >>--
local Origin = CFrame.new(0, 0, 0) -- Origin reference
local Position = Vector3.new(0, 0, 0) -- Position reference (The mouse's position or wherever you want the projectile to end)

--<< Projecitle creation >>--
local Projectile = Instance.new("Part") -- However you create / reference the projectile
Projectile.CFrame = CFrame.lookAt(Origin.Position, Position) -- This basically makes the projectile face the end position

--<< Misc variables >>--
local Key = 0 -- I'll explain this later but the key can never be 0, so set to 0
local DT = 0 -- Set as 0 for the function later
local Speed = 100 -- Example speed for projectile
local MaxDistance = 200 -- How far the projectile can travel

--<< Invoke the server for the key but use task.spawn to avoid yielding >>--
task.spawn(function()
    --<< Let me elaborate on this part, because it can seem confusing >>--
    --<< The function that receives the request on the server is in charge of checks to prevent exploiters from abusing it >>--
    --<< For example, is the weapon equipped? Is the player dead? Does the weapon have ammo? Is the weapon reloading? You get the point >>--
    --<< It also is in charge of cooldowns, a key is not created if there is a cooldown, so nil would be returned >>--
    --<< If you're dealing with a weapon with ammo or cooldowns, the function on the server should be in charge of that while also returning the key needed >>--
    --<< The origin of the projectile can be referenced on the server, so a key is stored in a table with the player who initiated it and the origin's current position on the server >>--
    --<< The only reason we send the origin is for visual replication on other clients >>--
    Key = YourRemoteFunction:InvokeServer(Origin, Position)
end)

local function CastRay(Pos, Direction)
    --<< Create ray using given pos, direction, speed and DT >>--
    local Ray = workspace:Raycast(Pos, Direction * (Speed * DT))
    local Distance = (Origin.Position - Pos).Magnitude

    if (Ray) then
        --<< Ray was hit >>--
        --<< Delete the projectile >>--
        Projectile:Destroy()

        --<< Like I said earlier, the key can never equal 0 (at least in my system), so yield until the key does not equal 0 >>--
        while (Key == 0) do task.wait() end -- Probably not the most optimal way, but it gets the job done

        if (Ray.Instance ~= nil and Ray.Instance.Parent:FindFirstChild("Humanoid")) then
            --<< Ray has hit a player >>--
            --<< Invoke server to verify the hit >>--
            --<< The function that receives this request on the server does checks as well >>--
            --<< For example, is the key given valid? Does the key given belong to the player that requested verification? Does the given instance exist on the server? For round based games, is the player playing? You get the point >>--
            --<< As stated earlier, the origin's initial position upon firing is stored in a table with the key on the server >>--
            --<< In my system, I do a simple raycast check from the initial position to each of the player's limbs or whatever instance is sent to see if the hit is in LOS of the initial position >>--
            --<< That is definitely not the most optimal way for projectiles, but if you're dealing with really fast projectiles like most FPS games, it shouldn't matter >>--
            --<< If the ray hits something and the ray's instance equals the sent instance, then the hit is verified and the server deals damage >>--

            if (HitDetectionFunction:InvokeServer(Key, Ray.Instance.Parent)) then
                --<< Hit was verified: give player some sort of hit verification (damage indicator, etc) >>--
            end
        end
    elseif (Distance <= Range) then
        --<< Ray was not hit but projectile can still travel >>--
        --<< Here, we'll update the position of the projectile >>--
        Projectile.Position += Projectile.CFrame.LookVector * (Speed * DT)
        DT = game:GetService("RunService").RenderStepped:Wait() -- Update DeltaTime
        CastRay(Projectile.Position, Projectile.CFrame.LookVector) -- Run the function again
    else
        --<< Ray was not hit and the distance has maxxed out, delete the projectile >>--
        Projectile:Destroy()

        --<< Like I said earlier, the key can never equal 0 (at least in my system), so yield until the key does not equal 0 >>--
        while (Key == 0) do task.wait() end -- Probably not the most optimal way, but it gets the job done

        --<< Fire a remote to the server to delete the key stored to this projectile, as it is no longer needed >>--
        YourRemoteEvent:FireServer(Key)
    end

--<< Run the function for the first time >>--
CastRay(Projectile.Position, Projectile.CFrame.LookVector)


That about covers it! In short:

  • Create projectile on client
  • Invoke server for key exclusive to the projectile (The key is stored in a table with the origin’s initial position and the player who initiated it)
local keys = {}

keys[GivenKey] = {Player, InitialOriginPosition} -- As an example
  • In the same function that receives the invoke request on the server, deal with checks such as if the player has ammo, if the player is on a cooldown, etc. Also, reduce ammo and add a cooldown (if needed) and replicate to other clients using the origin and position.
  • Hit detection should be dealt on the client, it is the best way for most projectiles (Use raycasting)
  • If ray hits something on the client, invoke server for verification with the key earlier
  • When verifying on the server, check that the key was created by this player and if the given instance is in line of sight of the initial origin position that is stored on the server with a simple raycast. The key can now be removed from the table, as it is no longer needed. If the ray hits the same instance on the server, deal damage and return true.
  • Show the player that they hit the instance with some sort of visual (Dmg indicator, etc)

I’m sorry if this was a bit lengthy! If you have any questions, let me know! There also may be a few errors in my code that I’m not seeing (I’m tired as I’m writing this :slightly_smiling_face:)

having all bullets server sided can cause lag, might not, but if it does another idea would be to fire to all clients so each client replicates the projectile movement for themselves but the server does the raycasting