For your use case, you could probably do something like this with RunService to figure out the velocity:
local part = --part being cframed
-- doesn't have to be heartbeat
local lastPosition
local connection
connection = RunService.Heartbeat:Connect(function(dt)
local part.Position = --whatever you want the position to be
if NeedsToBeThrown and lastPosition then
--disengage part
part.Velocity = (part.Position - lastPosition)/dt
connection:Disconnect()
end
lastPosition = part.Position
end)
This might throw the item backwards, and if so, reverse the sign on the velocity assignment.
You can also use a while loop to do this:
while true do
local dt = task.wait()
part.Position = --input
if NeedsToBeThrown and lastPosition then
--disengage
part.Velocity = (part.Position - lastPosition)/dt
break
end
lastPosition = part.Position
end
What makes CollectionService complicated? Using CollectionService means one instance less for each object that can be picked up, potentially amounting to thousands of less instances used. (less memory used)
You might also want to detect where to position the brick differently then. Maybe you are not calculating the position(s) correctly then?
I can’t show you any math equation(s) / “Position-calculator-code”, because I am not familiar with VR, and I am not the person to ask, and this is not a request catagory.
What I would do to correctly position the part is use a WeldConstraint (not a Weld, as that changes the part’s position unrealistically, I think), and just destroy the weld when you want to throw it. That way, it looks like you’re holding it in your hand and the physics engine does the holding (and throwing!) for you.
I should’ve guessed this since the hands are not simulated by the physics engine, I guess you should try that loop I was talking about earlier then, but just recording the last position instead of setting it.
I always recommend ContextActionService over UserInputService for cases where you only to an event to fire whenever a specific key is pressed. InputBegan fires whenever an input is began, so it will fire when not needed. ContextActionService also accounts for if the player is selecting a textbox so you don’t need to use GetFocusedTextBox.
This would create a lot of lag with lots of instances under workspace, so using GetTagged could give you a significant performance gain if you have a lot of parts under workspace.
RenderStepped should be used instead of heartbeat for the positioning. This will prevent the movement from becoming choppy and set the part’s position every frame.
Also, if RenderStepped is used then there is no need to anchor the part. The part will be in the correct position every frame. Anchoring it may prevent the physics engine from doing any extra though.
The average weighted change of the last few positions should be used. This is because if the hand position changes at the last moment perhaps due to an artifact of position precision, the object would be thrown in a very different direction. Not too many positions should be used otherwise it would be unresponsive. The changes should also be weighted according to how close they are to the release since changes closer to the release probably mean more to the desired direction.
I get that, but the reason I anchor it is because whenever I do throw it, it just bounces back fast. The way it gets thrown is fine with the solution that was used. Plus, I already am using RenderStepped.
So, implementation of your suggestion into code, using an array:
local lastPositions = {}
connection = RunService.RenderStepped:Connect(function(dt)
part.Position = --input
if NeedsToBeThrown and #lastPositions > 0 then
--disengage
local sumVelocity = part.Position - lastPositions[1]
part.Velocity = sumVelocity / (#lastPositions * dt)
connection:Disconnect()
end
table.insert(lastPositions,part.Position)
if #lastPositions > 5 then --really a variable
table.remove(lastPositions,1)
end
end)
Also, reasoning for not taking the smaller instantaneous velocities for each iteration and taking the “true” average is shown here, sort of:
local sumVelocity = Vector3.new()
for i = 1, #lastPositions - 1 do
sumVelocity = sumVelocity + lastPositions[i+1] - lastPositions[i]
end
-- this becomes ([2] - [1]) + ([3] - [2]) + ([4] - [3]) + ([5] - [4])
-- which evaluates to [2] - [1] + [3] - [2] + [4] - [3] + [5] - [4]
-- which is resorted into [5] - [4] + [4] - [3] + [3] - [2] + [2] - [1]
-- which makes everything cancel out except [5] and -[1]
-- this becomes the next line:
sumVelocity = sumVelocity + lastPositions[#lastPositions] - lastPositions[1]
sumVelocity = sumVelocity + part.Position - lastPositions[#lastPositions]
-- this makes lastPositions[#lastPositions] cancel out too!
-- which creates [current] - [1]
I suggest tuning how large the length of the array can be, which is dictated by this snippet at the end:
if #lastPositions > 5 then
table.remove(lastPositions,1)
end
Please reply if you think taking the sum of dt is important too!
The simplification of the average depends on not weighting the positions according to their closeness to the release event. It also requires you to disregard the spacing of the positions in time (assumes they are all the same).
Here is a full implementation of weighting the velocities and keeping track of the spacing:
local RunService = game:GetService 'RunService'
-- << INTERFACE >>
local function getHandCF()
return CFrame.new() -- TODO: edit to get the hand position
end
local function didLetGo()
return false -- TODO: edit to return true when the hand let go
end
local function setCF(cf)
-- TODO: edit to set the part's CFrame
end
local function setV(v)
-- TODO: edit to set the part's Velocity
end
-- << FIFO stack >>
-- implemented as a linked list
local push, clean, toArray, reset
do
local first, last
function push(t, v)
local record = {
t = t;
v = t;
}
if first then
first = record
last = record
else
last.next = record
last = record
end
end
function clean(t, freshness)
while first and t - first.t > freshness do
first = first.next
end
if first == nil then
last = nil
end
end
function toArray()
local arr = {}
local cur = first
while cur do
arr[#arr + 1] = cur.v
cur = cur.next
end
return arr
end
function reset()
first = nil
last = nil
end
end
-- Custom reduce function. Goes through the array
-- accumilating the results of the given callback.
-- Call the callback with the previous and current
-- values in the array. Both values must be defined
local function reduce(arr, func, acc)
local i = 1
local prv, cur = nil, arr[1]
while cur do
acc = func(acc, prv, cur)
prv = cur
i = i + 1
cur = arr[i]
end
return acc
end
-- Calculate the area under the curve of sqrt
-- multiplied by 3/2 to make total area between
-- 0 and 1 equal to 1. This makes the percent of
-- the total area equal to the partial area.
-- The integral was used to calculate this formula.
local p = 3 / 2
local function AUC(x)
return x ^ p
end
local t = 0
local freshness = 0.1
-- Calculate the weight for the given time period
local function getWeight(ct, dt)
-- percent of time at start of period since start
local a = 1 - ((t - ct) / freshness)
-- percent of time at end of period since start
local b = a + dt / freshness
return AUC(b) - AUC(a)
end
local function toVelocity(curV, prv, cur)
local prv_ct
if prv then
prv_ct = prv.ct
else
prv_ct = t - freshness
end
return curV + getWeight(prv_ct, cur.ct - prv_ct) * cur.cv
end
local connection
local lastClean = t
local cleanPeriod = 5 -- clean cache every 5 seconds
local lastP = Vector3.new()
-- TODO: keep track of last CF to calculate rotational velocity
local function onRenderStepped(dt)
t = t + dt
local cf = getHandCF()
setCF(cf)
push(t, {
ct = t; -- absolute time of cf start
cv = cf.p - lastP;
})
lastP = cf.P
if didLetGo() then
clean(t, freshness)
connection:Disconnect()
connection = nil
setV(reduce(
toArray(),
toVelocity,
Vector3.new()
))
reset()
elseif t - lastClean > cleanPeriod then
clean(t, freshness)
end
end
connection = RunService.RenderStepped:Connect(onRenderStepped)
Since my VR headset isn’t detected by Roblox, I’ll leave testing to the OP. It uses 3/2 * sqrt(x) as the weight function, meaning that positions closer to the moment of release all have a reasonable influence on the velocity, quickly dropping off near the maximum freshness (0.1 seconds ago in this case).