I always wonder how people make the projectiles so smooth, like this game with throwing snowballs: Snowman Simulator ☃️ - Roblox
Is this Velocity based?
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.
If you move this to #development-support:scripting-support, you may be able to get an answer from @Banj0man directly.
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.
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.
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.