Velocity based projectiles?

I always wonder how people make the projectiles so smooth, like this game with throwing snowballs: Snowman Simulator ☃️ - Roblox
Is this Velocity based?

1 Like

If I had to implement this, I would move the part on Heartbeat over the trajectory, raycasting every frame or every few frames until it hits something. This is done immediately on the client that fired it and other clients may run that simulation as well on their own end separately from the client that fired it (the server propagates the launch position, direction, magnitude, etc). Effects are played immediately in the simulation on each client for instant feedback.

It could be physics-based too, but I think moving them manually would give more consistent behavior and makes it easier to find hits (just what you get back from the raycast).

The rest of what you are seeing is just a trail attached to the snowball. Simple and looks great.

4 Likes

If you move this to #development-support:scripting-support, you may be able to get an answer from @Banj0man directly.

1 Like

Moved it, thanks

So basically like this?

But mine has the simulation done on the server, moving a part on it looks laggy.
if thats what you mean, I can make the simulation local

Yeah, you’ll want to not even animate it on the server (the snowball doesn’t even have to exist on the server). Just run the simulation on each client. If you anticipate a lot of throws then you could add some logic so that clients don’t simulate throws that are too far away from them / too far away + aimed away from them.

1 Like

Awesome, I will try that instead.
Thanks!

Another question,

function FireArrow(player, startPoint, movementSpeed, gravityForce) -- movementSpeed 15, GravityForce -50
	
	local lastCFrame = startPoint
	local firstTick = tick()
	local length = 15
	
	while (tick() - firstTick) < 5 do
		local CF = lastCFrame + lastCFrame.lookVector * length
		local newCF = CFrame.new(lastCFrame.p, CF.p + Vector3.new(0, gravityForce * 0.05, 0))
		local currentCFrame = newCF + newCF.lookVector * length
		local rayStart, rayDirection = newCF.p, newCF.lookVector * length
		local ray = Ray.new(rayStart, rayDirection)
		local hit, pos = Workspace:FindPartOnRayWithIgnoreList(ray,{player.Character,workspace.Ignore})
		
		drawVisualRays(rayStart, pos) 
		

		wait()
		
		lastCFrame = currentCFrame
	end
end

This is what I use to calculate the trajectory, how would you smoothly move a projectile over it? Tweenservice, lerp?

Instead of wait(), you would use Heartbeat:Wait(), and you catch the delta time from that, then use that to update the projectile over a distance relative to that time passed. Like so (left out some parts for brevity):

local Heartbeat = game:GetService("RunService").Heartbeat

function FireArrow(player, startPoint, movementSpeed, gravityForce) -- movementSpeed 15, GravityForce -50
	
	local cframe = startPoint
	local timeLeft = 5
	local length = 15
	local hit, pos
	
	while timeLeft > 0 and not hit do
		local dt = Heartbeat:Wait()
		timeLeft = timeLeft - dt
		
		local lastCFrame = cframe
		-- TODO: move 'cframe' by a factor relative to dt (passed time)
		
		local ray = Ray.new(lastCFrame.p, cframe.p - lastCFrame.p)
		hit, pos = Workspace:FindPartOnRayWithIgnoreList(ray, {player.Character, workspace.Ignore})

		-- TODO: update appearance of projectile
	end
	
	if hit then
		-- TODO: do the thing, all the things
	end
end

I might not be thinking very clearly but this makes no sense to me at the moment, move 'cframe' by a factor relative to dt (passed time)
Doesnt it get a new position everytime from the ray?

No, if it is moving through the air without obstacles, the raycast call will return nothing since there is no hit. (Assuming the projectile itself is part of the ignore list) You also need to compute the next position so that you know where to raycast towards in the first place.

Ah I see, mine goes at max length of 15 every step so I can give it gravity.

drawVisualRays(RayStart, pos)

Which gives this result:
https://gyazo.com/247ab200404e7f1856c53111f42af6de

But I am useless at the actual moving of things. Those are just non moving parts, like you explained with the delta.

If it’s not much work and if you have time, would you mind if I gave you the file so you could have a look?

I’m confused what the problem is, do you mean that you are drawing a lot of parts there instead of just one with a Trail object in it?

The problem is, I want a part to move over it, like the snowball in that game.
But I have no idea how to do that.

The gif I showed you shows the debug rays

Does this help describe the idea better?

local Heartbeat = game:GetService("RunService").Heartbeat

local GRAV_FORCE_MULTIPLIER = 0.05 -- magic constant from original code

function FireArrow(player, startPoint, movementSpeed, gravityForce)
	-- Creating a projectile that resembles a snowball for illustration:
	local part = Instance.new("Part")
	part.Anchored = true
	part.Shape = Enum.PartType.Ball
	part.Size = Vector3.new(1,1,1)
	part.CanCollide = false
	part.Color = Color3.new(1,1,1)
	-- TODO: insert trail into part
	
	local cframe = startPoint
	local timeLeft = 5
	local hit, pos
	
	part.CFrame = cframe
	part.Parent = workspace.Ignore -- I guess that goes there?
	
	while timeLeft > 0 and not hit do
		local dt = Heartbeat:Wait()
		timeLeft = timeLeft - dt
		
		local lastCFrame = cframe
		-- Direction is based on amount of time passed and movement speed:
		local dir = dt * movementSpeed
		-- Get the next position on the trajectory:
		local newPos = cframe.p + dir
		-- Construct new cframe based on newPos, where we tilt the direction downward for gravity:
		-- (gravity tilt also based on the time passed and some magic constant)
		cframe = CFrame.new(newPos, newPos + dir + dt * gravityForce * GRAV_FORCE_MULTIPLIER)
		
		local ray = Ray.new(lastCFrame.p, cframe.p - lastCFrame.p)
		hit, pos = Workspace:FindPartOnRayWithIgnoreList(ray, {player.Character, workspace.Ignore})

		part.CFrame = cframe
	end
	
	part:Destroy()
	
	if hit then
		-- TODO: play hit effect at pos
		-- TODO: do the thing, all the things
	else
		-- TODO: play disappear effects at cframe.p, if any
	end
end

You do dt * ... to ensure that the distance you travel / tilt down is relative to the time that the past frame took to execute, that way you get the smoothest trajectory. So if the speed is 5 studs and the past frame took 0.05 seconds, we would move 0.25 studs, and if the past frame took 0.1 seconds instead (twice as long), we would move 0.5 studs (twice as long).

Please note I didn’t test the code, it’s just to convey the idea, consider it pseudocode. It may need a couple tweaks to work proper.

6 Likes

That is exactly what I needed thanks!
The only issue that is occuring now is that dir is a number, not a vector.
it errors on that

Right, that should be

local dir = dt * movementSpeed * cframe.LookVector
		local dir = dt * movementSpeed * cframe.LookVector
		-- Get the next position on the trajectory:
		local newPos = cframe.p + dir
		-- Construct new cframe based on newPos, where we tilt the direction downward for gravity:
		-- (gravity tilt also based on the time passed and some magic constant)
		cframe = CFrame.new(newPos, newPos * dir * dt * gravityForce * GRAV_FORCE_MULTIPLIER)

like so? also edited the last line of the snippet

Yeah but that last line is not correct like this, it should be what it was initially AFAIK.