I like this idea so probably spent a bit more time on this than I should have so bare with me:
Your current script’s issues
To answer your initial question: the reason you’re experiencing the part suddenly clip backwards and forwards is because you haven’t added yourself alongside the CastObject
to the RaycastParams
filter - you can do this using :AddToFilter
. Learn more about this here, but essentially, you need to add it to the exclusion filter so that the ray isn’t intersecting with itself when you cast it to the find the intersection position.
The other problem with your code, assuming that you’ve reorientated the part to the camera’s CFrame
is that CFrame.new(0, -Result.Distance, 0)
should look like CFrame.new(0, 0, -Result.Distance)
because you want the object to travel along its LookVector
. Learn more about CFrame and its LookVector here and here.
Edit: For some reason I read target.CFrame
as Camera.CFrame
- I’m assuming you wanted the object to appear on top of the surface if there was a hit? To do that you would use the normal vector of the target surface via result.Normal
- learn about normal here
How does Shapecasting work?
I answered a question about Shapecast/Blockcast/Spherecast yesterday, you can read that here - it includes some example code to show you how to recalculate the position of the object as well as the intersection position.
Putting it all together
You seem to be going along the right track already, so I’m doubtful you need this, but to put this all together to figure out what you should be doing next:
How this should work
-
Player presses button / action to pick up object
-
While the player is holding the object we either:
- Put the object inside a ViewportFrame so that it can’t clip with the world
-
OR; we continuously update the object’s position so that it’s positioned in front of their camera - this can be done by taking the
LookVector
of the Camera’s CFrame and multiplying it by the sum of the NearPlaneZ
and the extents of the object. Though, sadly, this one does have some issues because the object will clip other object(s) in front of it
-
Once the player releases the action:
- We raycast from the Camera’s origin to the Mouse and/or centre of the screen - this depends on what device they’re using as well as whether you expect them to be first person or not
- Before repositioning the object, we need to take the intersection (or final ray position) and offset it so that we’re not colliding with the object we may or may not have hit
- Note that you could shape/blockcast/spherecast here to avoid clipping here, or you can also do depenetration if you look at the object(s) that would collide with our object if we positioned it there
-
Now we need to resize our object, to do that we could…
- When we first pick up the object we can calculate the distance from the camera to the object
- Then when released, we calculate the distance again but this time using the position we calculated during the raycast step
- We can divide the current distance by the original distance to get a scale factor, and then can multiply that with our original size to get the newly scaled size
-
Finally…
- If we used a
ViewportFrame
then we need to reparent the object back into the world;
- otherwise, if we’ve just updated the position, we just need to release the item and reset our state(s)
NOTE: Before you continue, I really do recommend you keep trying yourself first because you actually seem to be on your way to being able to do it. I only put this together because I thought it would be fun to do myself, so I’m happy to put it here for you to look at but if you just copy/paste it you’ll never learn, so make sure you do try yourself!
Example (don't look until you try yourself!)
Note: The following code is expected to be executed from a LocalScript
from within StarterPlayerScripts
--[!] SERVICES
-- i.e. any services we need
local Lighting = game:GetService('Lighting')
local RunService = game:GetService('RunService')
local PlayerService = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local ContextActionService = game:GetService('ContextActionService')
--[!] CONST
local PICK_DISTANCE = 25 -- i.e. distance we raycast from cam->world to find object(s) to pick up
local DROP_DISTANCE = 100 -- i.e. max distance we cast until we drop the object regardless
local RECALC_DEFER_TIME = 1 / 20 -- i.e. we'll wait 1/20th of a second before updating lighting
local PICK_TOUCH_BTN = true -- whether to show pickup button
local PICK_KEYBINDINGS = {
Enum.KeyCode.Q,
Enum.KeyCode.ButtonB, -- i.e. B on XBOX, Circle on PlayStation
}
local MOBILE_INPUTS = { -- i.e. input types associated with a mobile device
[Enum.UserInputType.Gyro] = true,
[Enum.UserInputType.Touch] = true,
[Enum.UserInputType.Accelerometer] = true,
}
--[!] UTILS
-- i.e. any utility methods & helper functions
--> used to check whether a player is alive
-- i.e. one that's alive and has both a humanoid + a root part
local function isAlive(character)
if typeof(character) ~= 'Instance' or not character:IsA('Model') or not character:IsDescendantOf(workspace) then
return false
end
local humanoid = character:FindFirstChildOfClass('Humanoid')
local humanoidState = humanoid and humanoid:GetState() or Enum.HumanoidStateType.Dead
local humanoidRootPart = humanoid and humanoid.RootPart or nil
if humanoidState == Enum.HumanoidStateType.Dead or not humanoidRootPart then
return false
end
return true
end
--> used to await a player's character
-- i.e. one that exists + is alive
local function tryGetCharacter(player)
if typeof(player) ~= 'Instance' or not player:IsA('Player') then
return nil
end
local character
while not character do
if not player or not player:IsDescendantOf(PlayerService) then
break
end
local char = player.Character
if not char then
player.CharacterAdded:Wait()
continue
end
if not isAlive(char) then
RunService.Stepped:Wait()
continue
end
character = char
end
return character
end
--> used to cleanup any connections/instances
local function cleanupDisposableInstance(disposable)
local t = typeof(disposable)
if t == 'RBXScriptConnection' then
pcall(disposable.Disconnect, disposable)
elseif t == 'Instance' then
pcall(disposable.Destroy, disposable)
elseif t == 'function' then
pcall(disposable)
elseif t == 'thread' and coroutine.status(disposable) ~= 'dead' then
pcall(task.defer, task.cancel, disposable)
end
end
local function cleanupDisposables(disposables)
for _, disposable in next, disposables do
cleanupDisposableInstance(disposable)
end
table.clear(disposables)
end
--> a non-perfect method of quickly deriving the device type
-- from the given input type, _e.g._ UserInputService::GetLastInputType
local function getDeviceName(inputType)
if typeof(inputType) == 'EnumItem' then
if MOBILE_INPUTS[inputType] then
return 'Touch'
elseif inputType.Name:match('Gamepad') then
return 'Gamepad'
end
end
return 'MouseAndKeyboard'
end
--[!] MAIN
-- i.e. our main code that performs all the action(s)
local function beginTracking(player)
local character = tryGetCharacter(player)
if not character then
return
end
-- cleanup
-- i.e. keep a table containing all the things
-- we need to cleanup once we're done
local disposables = { }
-- state
local pickupItem
local pickupOffset
local pickupDistance
-- set up our constants
local camera = game.Workspace.CurrentCamera
local humanoid = character:FindFirstChildOfClass('Humanoid')
local playerGui = player:FindFirstChild('PlayerGui')
-- set up our viewport to store the object
local screen = Instance.new('ScreenGui')
screen.Name = 'PickupScreen'
screen.IgnoreGuiInset = true
screen.Parent = playerGui
table.insert(disposables, screen)
local viewCam = camera:Clone()
viewCam.Name = 'ViewportCamera'
viewCam.Parent = screen
local viewport = Instance.new('ViewportFrame')
viewport.Size = UDim2.fromScale(1, 1)
viewport.Position = UDim2.fromScale(0.5, 0.5)
viewport.AnchorPoint = Vector2.one*0.5
viewport.CurrentCamera = viewCam
viewport.BackgroundTransparency = 1
-- note: we're trying to match the lighting of the
-- world here but you may need to work on this
local h, s, v = Lighting.OutdoorAmbient:ToHSV()
viewport.Ambient = Lighting.OutdoorAmbient
viewport.LightColor = Color3.fromHSV(h, s, math.max(1e-3, v)*Lighting.Brightness)
viewport.LightDirection = Lighting:GetSunDirection()
viewport.Parent = screen
local world = Instance.new('WorldModel')
world.Parent = viewport
-- set up our RaycastParameters to ignore
-- our own character when raycasting
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = { character }
-- set up listener(s)
local died
died = humanoid.Died:Connect(function ()
cleanupDisposables(disposables)
end)
table.insert(disposables, died)
local function handleAction(_, inputState, inputObject)
-- get a hint at what device we're using
local device = getDeviceName(inputObject.UserInputType)
-- make sure we're not already dead
if not isAlive(character) then
return Enum.ContextActionResult.Pass
end
-- get the cursor position appropriate for our device
-- note: if you're first person you can skip this and just use the
-- centre of the viewport frame
local pos
if device == 'MouseAndKeyboard' or device == 'Gamepad' then
pos = UserInputService:GetMouseLocation()
else
pos = camera.ViewportSize*0.5
end
-- det. whether we need to try to pick up or drop it ...
if inputState == Enum.UserInputState.Begin then
-- we want to pickup, so let's start by casting
-- a ray from the centre of our screen to the world
-- to find any objects that we can pick up
--
local ray = camera:ViewportPointToRay(pos.X, pos.Y)
local result = workspace:Raycast(ray.Origin, ray.Direction*PICK_DISTANCE, rayParams)
local instance = result and result.Instance or nil
if not instance or instance.Locked then
return Enum.ContextActionResult.Pass
end
-- update our viewport camera's transform so that it
-- reflects the current camera's transform so that the
-- object will be visible to the player in the same
-- manner that it is now
--
local transform = camera.CFrame
viewCam.CFrame = transform
-- compute the distance from our camera the object
-- we picked up so that we can use it to scale the
-- object later
--
local distance = (transform.Position - instance.Position).Magnitude
-- set our state & put the object inside our viewport
pickupItem = instance
pickupOffset = instance.CFrame.Rotation
pickupDistance = distance
pickupItem.Parent = world
-- update our lighting whilst we hold the object
--
-- note: we've put this in a deferred thread
-- so that we're not constantly updating the
-- lighting to reduce lighting recomputation
-- event(s)
--
local offset = transform:ToObjectSpace(pickupOffset)
viewport.LightDirection = -Lighting:GetSunDirection()
disposables._runtime = camera:GetPropertyChangedSignal('CFrame'):Connect(function ()
local thread = disposables._deferredThread
if thread and coroutine.status(thread) ~= 'dead' then
return
end
thread = task.delay(RECALC_DEFER_TIME, function ()
viewport.LightDirection = camera.CFrame:ToWorldSpace(offset):VectorToObjectSpace(-Lighting:GetSunDirection())
if disposables._deferredThread == thread then
disposables._deferredThread = nil
end
end)
disposables._deferredThread = thread
end)
-- add a disposable to cleanup in case we die etc
disposables._dropItem = function ()
-- wrapped in conditional & a pcall
-- in case our object was destroyed
-- if we don't do this it might throw an error
--
if pickupItem then
pcall(function ()
pickupItem.Parent = workspace
end)
end
end
elseif inputState ~= Enum.UserInputState.Change then
-- let's check to see if we have anything to drop first...
if not pickupItem then
return Enum.ContextActionResult.Pass
end
-- get our camera's current transform
local transform = camera.CFrame
-- once again, cast a ray from centre of our screen
local ray = camera:ViewportPointToRay(pos.X, pos.Y)
local dir = ray.Direction*DROP_DISTANCE
-- let's raycast because Blockcast may not work
--
-- note: the reason for this is because our object might be so
-- large that we're already intersecting an object in the
-- world, and thus, roblox may not return a valid result
--
local result = workspace:Raycast(ray.Origin, dir, rayParams)
-- compute the resulting normal & displacement
-- even if we didn't find an object to hit so
-- that we can still drop the item
--
local normal, displacement
if result then
normal = result.Normal
local proj = ray.Direction:Cross(normal)
normal = normal:Cross(proj)
displacement = ray.Direction.Unit*result.Distance
else
normal = Vector3.zero
displacement = dir
end
-- compute a transform that is...
--
-- 1) at our camera's current position
-- AND; 2) orientated in a manner that reflects the new object's orientation
-- relative to the new camera transform
--
local projection = viewCam.CFrame:ToObjectSpace(pickupOffset).Rotation
projection = transform:ToWorldSpace(projection)
-- compute the final object position and rescale it
--
-- size: resize based on the the difference between the last
-- pickup distance and the new distance
--
-- position: you might want to consider doing a depenetration
-- offset here, i.e. compute the amount the object
-- penetrates any objects in the final position area
-- so that it's not colliding with anything
--
local position = ray.Origin + displacement
local distance = (transform.Position - position).Magnitude
local size = pickupItem.Size*(distance / pickupDistance)
position = position + normal*(transform.Rotation*size) - ray.Direction*(transform.Rotation*size)
projection = projection.Rotation
-- transform the object's position so that it's
-- in its new location and back in the same orientation
-- as when we picked it up
pickupItem.CFrame = CFrame.new(position)*projection
-- set the item scale
pickupItem.Size = size
-- put it back into the world
pickupItem.Parent = workspace
-- reset our state
pickupItem = nil
pickupOffset = nil
pickupDistance = nil
-- resolve any cleanup disposable(s)
disposables._dropItem = nil
local thread = disposables._deferredThread
disposables._deferredThread = nil
if thread then
cleanupDisposableInstance(thread)
end
local runtime = disposables._runtime
disposables._runtime = nil
if runtime then
cleanupDisposableInstance(runtime)
end
end
return Enum.ContextActionResult.Sink
end
-- bind the pickup keybindings +/- the pickup touch button
ContextActionService:BindAction('PickupAction', handleAction, PICK_TOUCH_BTN, table.unpack(PICK_KEYBINDINGS))
-- add the action to our disposables
-- so we can clean it up later
table.insert(disposables, function ()
pcall(ContextActionService.UnbindAction, ContextActionService, 'PickupAction')
end)
end
--[!] INIT
-- i.e. when the script first initialises on the client
local player = PlayerService.LocalPlayer
if typeof(player.Character) ~= 'nil' then
beginTracking(player)
end
player.CharacterAdded:Connect(function ()
return beginTracking(player)
end)