# Calculating Bullet Drop and Trajectories

I was wondering how games like phantom forces are able to calculate bullet drop. I know that you can get a part on a ray cast but that is a straight line. Do you have to do multiple ray casts forming a parabolic curve with low resolution so to make computation less stressful? Also, if I have a slower moving object like a ball, is it less computationally stressful to do raycasting of trajectory with a delay between each individual raycast in the parabola instead of a touch event server side?

15 Likes

Probably intense math. Having a formula that basically moves the bullet across a specific route. Take a look at this:

http://hyperphysics.phy-astr.gsu.edu/hbase/grav.html#bul

1 Like

I already understand the physics behind trajectories and bullet drop; the issue is calculating a collision on a trajectory with another part (ex. bullet hitting playerâ€™s head). Would doing multiple raycasts be the best way of doing this?
So like each ray cast starts where the previous ray cast ended, and recursively calculating getPartOnRay() for each of these ray casts until some base case. You can probably define the resolution (how many ray casts to do) as well as the delay between each ray cast (based on bullet speed or object speed). Iâ€™m just wondering if thereâ€™s is already a built in method that would make things easier, and if I should just use touch events for slower moving objects like snowballs.

1 Like

If you know the end position, you could spawn a part at that position and use that with a touch event to deal the damage. There are tons of ways to do this, so this particular one may not be the best.

For bullets, I just use one ray per step, stepping 60 times per second.

Because bullets move so fast, the amount of ground that they cover in relation to how quickly they are effected by gravity doesnâ€™t result in an intense arc. That combined with the sample rate make it fine to approximate with straight lines for each segment.

6 Likes
1 Like

I donâ€™t have access to those links because I donâ€™t have full member status. Can you copy and paste the information here?

You actually donâ€™t have to use a low resolution, either. The overhead of raycasting has a lot more to do with the length of the ray(s), rather than how many rays there are. So you can cast a whole bunch as long as theyâ€™re not any longer than the one single ray wouldâ€™ve been.

Of course, there is still a fair amount of overhead from invoking the API. I would definitely benchmark this and see how it affects your game, though. Odds are that it wonâ€™t really be that bad.

Iâ€™ve actually done this myself a few times, too. My normal approach is something like this:

``````local function fireBullet(originPoint, velocity)
local bulletPos, bulletVelocity = originPoint, velocity

local startTime = tick()
while true do
if tick() - startTime > 3 then break end
local dt = RunService.RenderStepped:Wait()

local bulletRay = Ray.new(bulletPos, bulletVelocity)
local hitPart, hitPoint, hitNormal = workspace:FindPartOnRay(bulletRay)
if hitPart then
handleBulletHit(hitPart, hitPoint, hitNormal)
break
else
bulletVelocity = bulletVelocity - (Vector3.new(0, workspace.Gravity, 0) * dt)
bulletPos = bulletPos + bulletVelocity * dt
end
end
end``````
50 Likes

Oh RIP

Hereâ€™s two decent ones, I think

Raycasting hit-detection

On the client;
Anchored part, ray-based detection.
If youâ€™re looking at shooting in a straight line, make sure you have your direction to begin with, keep note of the bullets â€śCFrameâ€ť but this can be stored as data, and a seperate CFrame can be applied onto this to make a 3d model work using it.
Then, using the heartbeat/stepped method, you can iterate the projectile at any speed youâ€™d like, using delta/tick() equations.
Essentially, how far do you want a projectile to travel per second? How long has it been since the last update? Multiply together, you get the distance the projectile should have travelled in that update.

For then using hit detection, you can do a couple of things.
Stagger the raycasts, so theyâ€™re only called every couple of updates.
Do them per-update, but have a minimal amount of projectiles as to not slow the system down.
In terms of sending the data, rather than sending a CFrame, send the Starting position, and the position of the start CFrame moved 1 stud in the lookVector. You can then re-create the CFrame on the clients using this information.
In order to make a bullet â€śhitâ€ť, youâ€™ll need to give it a unique ID and tell each client that ID. Once the client has hit something with that bullet, you just send the ID to the clients, and theyâ€™ll cease activity of the bullet.

Thats how Iâ€™d go about setting that up anywho.

Bullet Drop

Alright, did my best to try and comment anything thatâ€™s important in how I do my bullets with rays. I didnâ€™t really optimize a lot of this, I aimed more to keep it as simple to read as possible (no clue how well I did on that though). Theres a lot of redundant bits that could be cleaned up to boost performance as well as a few API calls you could localize.

It more or less runs until it either hits something, or it hits a distance cap. All of the distance calculations it does are all influenced by the time between iterations, so youâ€™ll get consistent velocities 99% of the time but your ray distance wont be the same number all the time. But that doesnâ€™t really matter if you are trying to aim for a â€śsmoothâ€ť flight.

Getting the distance and the bullet drop amount are all just simple Newtonian physics put to use, Iâ€™m sure if you wanted to read more into them theâ€™re are million of pages online about them.

As per your old code Iâ€™d avoid using a for loop for this type of thing, they can aid in that stuttering if your trying to be accurate and using the values from loop. And copying a bullet per loop iteration puts a really big load on the script, especially when you fire lots of bullets. Itâ€™s much better to copy a single bullet per function call and re-position it accordingly.

And as A final note, in my example I used wait(0), but Iâ€™d recommend switching that over to one of the RunServiceâ€™s step events and just waiting for that to fire, they just sync a lot nicer and wait() can be a little inconsistent under heavy load.

(sorry about any typos, itâ€™s late and I figured Iâ€™d draft this up before I signed off for the night.)

Click this for a better look without all the page scrunching on the forums. (http://hastebin.com/odijuyaniw.lua)

``````local bullet_Base = Instance.new("Part")
bullet_Base.FormFactor = "Custom"
bullet_Base.Size = Vector3.new(0.2, 0.2, 4)
bullet_Base.FormFactor = Enum.FormFactor.Custom
bullet_Base.BrickColor = BrickColor.new("Cool yellow")
bullet_Base.Material = Enum.Material.SmoothPlastic
bullet_Base.BottomSurface = Enum.SurfaceType.Smooth
bullet_Base.TopSurface = Enum.SurfaceType.Smooth
bullet_Base.Name = "Bullet"
bullet_Base.CanCollide = false
bullet_Base.Anchored = true
function fireBullet(start, target, maxLength)
--settings some constants
local directionVector = (target - start).unit
local direcitonCFrame = CFrame.new(start, start + directionVector)
--settings values that we're going to update throughout
local delta, lastTick, startTime = 0, tick(), tick()
local hit, position, lastPosition = nil, target, start
local totalDistance = 0
--copying a bullet to use as a visual for the rays
local bullet = bullet_Base:Clone()
bullet.Parent = workspace --???
local function loop()
while not hit and totalDistance < maxLength do --basically stop if we hit our max or we hit something
delta = tick() - lastTick --get the time delta, aka time between last iteration and now
lastTick = tick() --set this to keep out delta calculations accurate
local travelDistance = delta * stats.muzzleVelocity --getting our distance for this iteration
--velocity being in studs per second
--Just a simple physics equation, distance = time * velocity
--Doing this makes for constant velocity, but not consistent ray distance
--all this does it get rid of potential lag influences really
travelDistance = travelDistance < 999 and travelDistance or 999 --just limiting our distance, this way is faster than math.min
totalDistance = totalDistance + travelDistance --just adding on to our total
local dropAmount = 9.81/2 * (tick() - startTime)^2 --gravity/2 * the square of our total time to get our bullet drop amount
local incremented = --see break down of what transformations are done below
direcitonCFrame * --start from our directionCFrame constant
CFrame.new(0, 0, -totalDistance) - --move forward our total distance
Vector3.new(0, dropAmount, 0) --move down based on our bullet drop amount
--doing this should give us an idea of where we want to be based on our calculations
local direction = CFrame.new(lastPosition, incremented.p).lookVector --getting a look vector from where we are, to where we want to be
--the rest you more or less had, fairly basic raycasting code
local ray = Ray.new(lastPosition, direction * travelDistance)
hit, position = workspace:FindPartOnRayWithIgnoreList(ray, {bullet}) --{workspace.Weapons, character, camera})
bullet.Size = Vector3.new(0.2, 0.2, (lastPosition - position).magnitude)
bullet.CFrame = CFrame.new(lastPosition, position) * CFrame.new(0, 0, -bullet.Size.Z/2) --lazy here
if hit then
print(hit)
break
end
lastPosition = position
wait(0) --would recommend waiting for one of the runservice events instead
end
if not hit then
print("Distance maxed out, we hit nothing")
end
end
--gun.Barrel.Flash.Range = math.random(6,9)
--gun.Barrel.Flash.Enabled = true
--Delay(0.025, function()
--	gun.Barrel.Flash.Enabled = false
--end)
coroutine.wrap(loop)()
end
fireBullet(Vector3.new(0, 5, 0), Vector3.new(0, 10, 100), 2500)
``````
8 Likes

That is incredibly useful to know, Iâ€™ve been wondering why Iâ€™ve been able to get a few hundred working!

7 Likes

Wow thatâ€™s really helpful information, thanks. Do you think a ray casting approach for slower moving objects would be more effective than a touch event?

1 Like

No, I donâ€™t think so. You should just use the physics engine because itâ€™s already pretty good at doing exactly that. Just make sure you listen to the Touched events in the right place. Set the network owner to the client that shot the missile/bullet/whatever, then do your Touched stuff locally on their client.

3 Likes

If I do the touch event locally on the client, how would I do proper sanity checks on the server side? I know I can check rate of fire of the event, but I canâ€™t think of any ways to check if the collision was actually valid. Is that just the downside of optimizing this?

Yeah, it is. Youâ€™ll always have to make some compromises here and try to patch them up another way. Itâ€™s just an unfortunate part of making networked games that hide the inherent network latency from the player.

Fortunately, Iâ€™m working on a module that will help streamline this process that will be open-sourced some time in the future.

For now, if youâ€™re feeling up to it, you can emulate its functionality. It logs up to 20 positions of every player on the server, sampling every 1/10th of a second. Thereâ€™s another part that estimates the network latency by measuring how long it takes for a RemoteFunction:InvokeClient() for each player to happen, which gives you a rough idea of the roundtrip time.

Now, when someone shoots someone else, they will tell the server where they shot and who they think they shot. The server can then rewind the targetâ€™s position by the shooterâ€™s one-way network latency time and see if that person was actually there when the shot was fired.

4 Likes

Thatâ€™s a great idea! You could probably decrease the amount of network traffic (assuming this is a multiplayer game) by having clients report to the server if they suspect another client to be hacking. When the amount of reports reaches a certain threshold the server can audit the player who might be hacking (start logging playerâ€™s position and sample it every 1/10th of a second), and if the server audit shows that the player is moving too fast, then it can moderate the player and/or log the incident.

This might decrease the computational stress on the server so that it doesnâ€™t need to constantly check every client position every tenth of a second, but rather have clients check each other.

I wouldnâ€™t, otherwise youâ€™d probably risk having some people find out about this and start ruining the game for others. Plus, you canâ€™t effectively check this because the server still needs to tell you everyoneâ€™s roundtrip times, and now youâ€™ll be dealing with their roundtrip time PLUS your own.

Checking it on the server isnâ€™t just for cheat-detection, but also for reconciling the game state. If I shoot at someone and it takes a while for the shot to reach the server, the player I shot at has probably already moved clear of my shot in the serverâ€™s perspective. You still need to rewind so that you can properly apply the damage to the shot player. You could trust the shooter, but that just makes it way too easy to cheat. Always assume that hackers can and will send whatever they want through your RemoteEvents/Functions. If you donâ€™t trust the shooter, but also donâ€™t rewind, people with faster internet connections get a huge advantage because they donâ€™t have to lead their shots as much.

1 Like

If you cast a ray 60 times a second, how do you replicate it on the server?
You might wish to check out my another thread for the code.

Bruh you donâ€™t fire the remoteEvent 60 times, you just tell the server you fired, and when you impact. You detect impacts client side lol

I would use something object oriented like this.

Would work something like this

Usage

``````local trajectory = Trajectory.new(handlePosition, FireDirectionAndVelocity, partsListToIgnore)

-- connect to the impacted event, BEFORE running any :updates()

trajectory.impacted:connect(function(hit, pos)
--find player character from hit part
end)

--every RunService.Heartbeat, run trajectory:update() until you have found your hit
``````

Module

``````local Trajectory = {}

function Trajectory.new(initial, velocity, ignorelist)
local self 				= setmetatable({},{__index = Trajectory})

--Properties
self.position			= initial
self.velocity			= velocity
self.ignorelist			= {} or ignorelist

--Hidden variables
self.impactedobject 	= Instance.new("BindableEvent")

--Events
self.impacted			= self.impactedobject.Event

self.start				= tick()
self.t					= self.start

return self
end

function Trajectory:update()
local dt 				= tick() - self.t
self.t 					= tick()

local oldPosition 		= self.position
self.position 			= oldPosition + (self.velocity * dt)
self.velocity			= (self.velocity + .5 * Vector3.new(0,-1,0) * dt)

local UpdateRay = Ray.new(oldPosition, (self.Position - oldPosition).unit * 300)
local hit, hitpos, normal, material = workspace:FindPartOnRayWithIgnoreList(UpdateRay, self.ignorelist, false, true)
if hit then
if (oldPosition - self.position).magnitude >= (oldPosition - hitpos).magnitude then
self.impactedobject:Fire(hit, hitpos, normal, material)
end
end
end

function Trajectory:reflect(normal)
self.trajectory			= -2 * self.velocity.unit:Dot(normal.unit) * normal.unit + self.velocity.unit;
end
``````

iâ€™ve also provided you with my reflect method if you want to get into bouncing these things off of walls and stuff. This should have overpentration for collateralâ€™s built in as long as you donâ€™t stop running :update() after the first impact.

Security

Fire a remote event once to let the server and everyone else know that someone shot and in what direction so that they can emulate bullet effects and sounds, and then fire the server again on impacted. This is what i would recommend with any system, even without bullet drop/travel time (except it would be one request, combining the data), with some good server side shot verification. Shot verification is something that will always require tuning, and it becomes increasingly difficult with more complex shots (aka bullet drop.)

Itâ€™s more straightforward on games that use single raycast, as you can just check the positions of players relative to the shot, check the validity of the shot with a single ray on the server, and a few other things.

6 Likes