A way to improve projectile sanity

I’m currently attempting to program server-based sanity checking regarding projectiles into a class-based fighter I’m making. The projectiles I use aren’t hitscan - they’re comparitively slow-moving physics-based projectiles that register hits via a clientsided Touched event. It’s sloppy, I know - but the projectiles we use are large and definitely too big to be visually accurate to a raycasted hitcheck. Thus I’m forced to give the client permissions and simply send the server details regarding the projectile when a hit connects for the server to leaf through and determine whether or not the hit was legal.

What I have as of now though is incredibly barebones - it simply assesses the magnitude between two unit vectors.

function chkBullet(o, p, v)
	local s = (p - o).unit; local d = v.unit
	return (s - d).magnitude <= 1
end

‘o’ is the place the shot originated from, ‘p’ is the position of the shot at the time of contact, and ‘v’ is the direction the shot was originally fired from. This check doesn’t account for travel time (not that I know if it should, none of our projectiles change directions and homing shots skip this check entirely) so I’m sure there’s definitive room for improvement. If anyone has any suggestions, I’m open to hearing them.

Thanks for reading.

I would suggest looking into this module.

I’ve worked with FastCast before and it’s a great module - but like already mentioned, it simply uses a server-synched raycasting loop and can’t be used for large parts with an intended wide area of collision (unless it’s been updated to do so, I haven’t checked).

Have you looked into using Region3’s? EgoMoose has a great module:

I think you could definitely lessen the gap by a bit for your chkBullet function, like maybe comparing <= 0.5 instead. Your current check allows for, at maximum, a 60 degree difference in direction, if my calculations are correct.

I was inspired enough by this problem to make a cool vector renderer for this on Desmos by the way, I think it looks extra cool, and hopefully it’s not too confusing:

You can also use the dot product between s and d to compare angles between (p - o) and v, if that helps. Using this method, you would be able to strictly define the maximum difference in angle that can be made between the s and d. i.e. This would be an equivalent function:

function chkBullet(o, p, v)
	local s, d = (p - o).Unit, v.Unit
	
	local maxAngle = 60 -- in degrees
	return math.acos(s:Dot(d)) <= math.rad(maxAngle)
end

You can check the time of arrival dupe by creating a timestamp for each projectile.

It starts with this:

  • for each projectile, a table is created (well call it projectile data) and stored in a big database (a table called currentprojectiles or something)
  • give each rocket a timestamp of tick() as well as a custom id, and a marker telling which player shot it.
  • when the projectile hits something, the server is given a position. The server calculates the time of arrival between the two points and the speed of the projectile (distance/speed).
  • it checks the time between when the rocket was fired (projectiledata.Time or something.) and the current timestamp (tick()) and then campares that with the calculated time of arrival. Given a margin of error to account for latency (1 is usually enough) it reliably prevents people from making their projectiles hit/explode instantly, aka they gotta wait the full time.

I really liked what @goldenstein64 proposed with the whole angle check, I tried to do roughly the same thing but it got really overcomplicated with me using ray:ClosestPoint() and such. The main flaw with my angle system is that it didn’t measure the angle difference, but rather the position distance of where it should end be vs where it did end. This results in false positives at large distances because at 700 studs a 0.5 degree angle difference can mean hundreds of positional difference, as opposed to close up.

I’ll share my snippet of sanity checks that I described for anyone who wants it.

Code
local Raynew = Ray.new
local Closest = Ray.new().ClosestPoint
local v3_naught = Vector3.new()

local function FindClosest(line_start, line_end, point)
	local delta = line_end - line_start
	local mag = delta.magnitude
	local norm = mag > 9e-22 and delta.unit or v3_naught
	local ray1 = Closest(Raynew(line_start, norm), point)
	if (ray1 - line_start).magnitude < mag then
		return ray1
	else
		return Closest(Raynew(line_end, norm), point)
	end
end


local function findrocket(player, id)
	for i,v in pairs(currentrockets) do
		if v.Player == player and id == v.ID then
			return v, i
		end
	end
end

game.ReplicatedStorage.Projectile.ShootRocket.OnServerEvent:connect(function(player, command, arg, arg2)
	if command == "Start" then
		local airplane = player.Character.Aircraft.Value
		local set = require(airplane.AircraftModule)
		local rocketdata = {Player = player, Start = airplane.Hull.CFrame, Time = tick(), ID = set.rockets}
		set.rockets = set.rockets - 1
		table.insert(currentrockets, rocketdata)
		for i,v in pairs(game.Players:GetPlayers()) do
			if v ~= player then
			game.ReplicatedStorage.Render:FireClient(v, "Rocket", airplane.Hull.CFrame)
			end
		end
	elseif command == "Finish" then
		local rocketdata, place = findrocket(player, arg2) -- get our rocket data to compare times
		if rocketdata then
			local timeofarrival = (rocketdata.Start.Position - arg).Magnitude/200 -- distance/speed
			local difference = getdifference(rocketdata.Time, tick(), timeofarrival)
			if difference < -TimeMOE or difference > TimeMOE then
				warn("Rocket request ignored because time of arrival does not match. Time difference: "..difference)
			else 
				local closestpoint = FindClosest(rocketdata.Start.Position, rocketdata.Start.Position + rocketdata.Start.LookVector*(rocketdata.Start.Position - arg).Magnitude, arg)
				local distance = (closestpoint - arg).Magnitude
				print(distance)
				if distance <= PositionMOE then
					gf.explode(arg, 100, 200, 0, 0, 1000)
				else
					warn("Rocket request ignored because endposition does not allign. Position Difference: "..distance)
				end
			end
		table.remove(currentrockets, place)
		rocketdata = {}
		else
			warn("no rocket data found")
		end
	end
end)
4 Likes