Pushing object as far as possible

Hello, so I am trying to push an object as far as possible from my camera without going through a wall. This is to achieve a forced perspective effect. But I don’t know how to calculate that. I was thinking about shapecast but I don’t understand how to use it

local function UpdatePos()
	target.Position = mouse.Hit.Position	

	local params = RaycastParams.new()
	params.FilterDescendantsInstances = {CastObject}

	local Dir = (mouse.Hit.Position - target.Position).Unit

	local Result = workspace:Shapecast(
		target,
		Dir * maxRayDistance,
		params
	)

	print(Result)
	CastObject.CFrame = Result
		and target.CFrame * CFrame.new(0, -Result.Distance, 0)
		or target.CFrame * (Dir * maxRayDistance)
end

Here is what happen:

And this is what I’d like to achieve (script by FederalNando):

You could create an invisible part with an attachment when the player spawns. Keep that part at the end of a regular raycast, and when the object you’re pushing gets clicked on, parent a second attachment and a couple constraints to the object being pushed (AlignPosition and AlignOrientation/BodyGyro).

Adjusting the Max Velocity of the AlignPosition should help with any clipping issues

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
  1. Player presses button / action to pick up object

  2. 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
  3. 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
  4. 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
  5. 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)

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.