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

https://devforum.roblox.com/t/bullet-physics/6683

https://devforum.roblox.com/t/slow-projectile-simulation-and-hit-detection/107254/4

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