Getting the position that originated a Spherecast result

Hello!

I have implemented some bullets whose positions are changed every frame, based off their velocity. To detect collisions I am using workspace:Shapecast, which gives me the position of the collision. With this said, using this point for effects and some other actions may look weird depending on the angle in which the bullet is sent, as shown in the video below.

External Media

I would like to have the position of the bullet that would have touched the point of collision. Any clues on how I can achieve this?

**EDIT - ** For clarification, it causes situations like this, where the trail shows the position the bullets were traveling at:

imagem

See a relevant post here where I explained how to get either the intersection or the sphere position; or see the following example that computes both the sphere and the collision point below:

Example

Note: Pressing “Q” simulates the sphere’s position during a spherecast and “E” simulates the collision point

--[!] SERVICES
local RunService = game:GetService('RunService')
local PlayerService = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local ContextActionService = game:GetService('ContextActionService')


--[!] CONST

local RAY_RADIUS = 2          -- describes the radius of the sphere we'll cast
local RAY_DISTANCE = 1000     -- describes the max magnitude/distance of the ray(s)
local RAY_OPTIONS = {         -- describes optional flags / params for simulateSphereCast
  CleanupDelay = 3,
  UpdateFrequency = 1 / 60,
  PlaybackDuration = 2,
}

local RAY_SHOW_TOUCH = true   -- whether we should a touch button to dash
local RAY_KEYBIND_CODES = {   -- the key(s) that can be used to dash
  SPHERE = {
    Enum.KeyCode.Q,
    Enum.KeyCode.ButtonB,     -- i.e. B on XBOX, Circle on PlayStation
  },
  INTERSECT = {
    Enum.KeyCode.E,
    Enum.KeyCode.ButtonX,     -- i.e. X on XBOX, Square on PlayStation
  },
}

local MOBILE_INPUTS = {       --
  [Enum.UserInputType.Gyro] = true,
  [Enum.UserInputType.Touch] = true,
  [Enum.UserInputType.Accelerometer] = true,
}


--[!] UTILS

--> 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 cleanupDisposables(disposables)
  for _, disposable in next, disposables do
    local t = typeof(disposable)
    if t == 'RBXScriptConnection' then
      pcall(disposable.Disconnect, disposable)
    elseif t == 'Instance' then
      pcall(disposable.Destroy, disposable)
    elseif t == 'function' then
      disposable()
    end
  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

--> raycast from camera to either (a) mouse position; or (b) camera centre
--  dependent on the device type
local function getTargetResults(player, camera, params, device, maxDistance)
  device = device or 'MouseAndKeyboard'
  maxDistance = maxDistance or RAY_DISTANCE

  local character = player.Character
  local isValidCharacter = isAlive(character)
  if not params then
    params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.IgnoreWater = true
    params.RespectCanCollide = false

    if isValidCharacter then
      params:AddToFilter(character)
    end
  end

  local coords
  if device == 'MouseAndKeyboard' or device == 'Gamepad' then
    coords = UserInputService:GetMouseLocation()
  else
    coords = camera.ViewportSize*0.5
  end

  local ray = camera:ViewportPointToRay(coords.X, coords.Y)
  local vec = ray.Direction*maxDistance
  local result = workspace:Raycast(ray.Origin, vec, params)

  local hit, pos, normal, material, distance
  if not result then
    hit = nil
    pos = ray.Origin + vec
    normal = Vector3.yAxis
    material = Enum.Material.Air
    distance = maxDistance
  else
    hit = result.Instance
    pos = result.Position
    normal = result.Normal
    material = result.Material
    distance = result.Distance
  end

  return {
    Normal = normal,
    Instance = hit,
    Position = pos,
    Material = material,
    Distance = distance
  }
end

local function deepCopy(orig, copies)
  copies = copies or { }

  local t = typeof(orig)
  if t == 'table' then
    if copies[orig] then
      return copies[orig]
    end

    local copy = { }
    copies[orig] = copy

    for key, val in next, orig, nil do
      copy[deepCopy(key, copies)] = deepCopy(val, copies)
    end

    setmetatable(copy, deepCopy(getmetatable(orig), copies))
    return copy
  end

  return orig
end

--> merge tables together
--    where first table = source, and following tables
--    will overwrite those beneath it
local function mergeTables(...)
  local result = { }

  local len = select('#', ...)
  for i = 1, len, 1 do
    local trg = select(i, ...)
    if typeof(trg) ~= 'table' then
      continue
    end

    for key, value in next, trg do
      local t = typeof(value)
      if t == 'table' then
        value = deepCopy(value)
      end

      if t ~= 'nil' then
        result[key] = value
      end
    end
  end

  return result
end

--> animates the sphere cast
--    will show the intersection point if `ShowIntersection` opt flag
--    is provided
local function simulateSphereCast(origin, rayDirection, rayParams, sphereRadius, opts)
  -- set up
  opts = mergeTables(RAY_OPTIONS, opts or { })
  sphereRadius = sphereRadius or RAY_RADIUS

  local scaleFactor = opts.ScaleFactor or 1
  local cleanupDelay = opts.CleanupDelay
  local updateFrequency = opts.UpdateFrequency
  local playbackDuration = opts.PlaybackDuration
  local showIntersection = opts.ShowIntersection

  local disposables = { }
  local totalElapsed = 0
  local lastFrameUpdate = 0

  -- cast our ray
  local result = workspace:Spherecast(origin, sphereRadius, rayDirection, rayParams)

  local hit, pos, normal, material, distance
  if not result then
    hit = nil
    pos = origin + rayDirection
    normal = Vector3.yAxis
    material = Enum.Material.Air
    distance = rayDirection.Magnitude
  else
    hit = result.Instance
    pos = result.Position
    normal = result.Normal
    material = result.Material
    distance = result.Distance
  end

  -- det. the desired position
  local desiredPosition
  if not showIntersection then
    desiredPosition = origin + rayDirection.Unit*distance
  else
    local displaced = pos - origin
    local magnitude = displaced.Magnitude
    local direction = displaced.Unit
    desiredPosition = origin + direction*magnitude
  end

  -- simulate it
  local scale = math.max(sphereRadius*2*scaleFactor, 0.1)
  local part = Instance.new('Part')
  part.Name = '__SPHERE_CAST'
  part.Size = Vector3.one*scale
  part.Shape = Enum.PartType.Ball
  part.Anchored = true
  part.CanQuery = false
  part.CanTouch = false
  part.Position = origin
  part.CanCollide = false
  part.CastShadow = false
  part.BrickColor = BrickColor.Random()
  part.TopSurface = 0
  part.BottomSurface = 0
  part.Transparency = 0.15
  part.Parent = workspace

  table.insert(disposables, function ()
    task.delay(cleanupDelay, pcall, part.Destroy, part)
  end)

  local runtime
  runtime = RunService.Stepped:Connect(function (gt, dt)
    lastFrameUpdate += dt

    if lastFrameUpdate < updateFrequency then
      return
    end

    local frameTime = lastFrameUpdate
    lastFrameUpdate = math.fmod(lastFrameUpdate, updateFrequency)

    -- interpolate towards our target at time t
    totalElapsed += frameTime - lastFrameUpdate

    local alpha = math.min(totalElapsed / playbackDuration, 1)
    part.Position = origin:Lerp(desiredPosition, alpha)

    if alpha >= 1 then
      -- cleanup when finished
      cleanupDisposables(disposables)
      return
    end
  end)
  table.insert(disposables, runtime)
end

--[!] MAIN
local function beginTracking(player)
  local character = tryGetCharacter(player)
  if not character then
    return
  end

  -- cleanup
  local disposables = { }

  -- set up
  local camera = game.Workspace.CurrentCamera
  local humanoid = character:FindFirstChildOfClass('Humanoid')
  local rootPart = humanoid.RootPart

  local rayParams = RaycastParams.new()
  rayParams.FilterType = Enum.RaycastFilterType.Exclude
  rayParams.IgnoreWater = true
  rayParams.RespectCanCollide = false
  rayParams:AddToFilter(character)

  -- set up listener(s)
  local died
  died = humanoid.Died:Connect(function ()
    cleanupDisposables(disposables)
  end)
  table.insert(disposables, died)

  local function handleAction(action, inputState, inputObject)
    if not isAlive(character) or inputState ~= Enum.UserInputState.Begin then
      return Enum.ContextActionResult.Pass
    end

    local origin = rootPart.Position
    local device = getDeviceName(inputObject.UserInputType)
    local target = getTargetResults(player, camera, rayParams, device)

    local displacement = target.Position - origin
    local rayDirection = displacement.Unit

    local useIntersect = action == 'ShowIntersection'
    local scalingFactor = useIntersect and 0.1 or 1
    simulateSphereCast(
      origin, rayDirection*RAY_DISTANCE,
      rayParams, RAY_RADIUS,
      {
        ScaleFactor = scalingFactor,
        ShowIntersection = useIntersect,
      }
    )

    return Enum.ContextActionResult.Sink
  end

  -- bind the keybindings +/- the touch button
  ContextActionService:BindAction('ShowSphere', handleAction, RAY_SHOW_TOUCH, table.unpack(RAY_KEYBIND_CODES.SPHERE))
  ContextActionService:BindAction('ShowIntersection', handleAction, RAY_SHOW_TOUCH, table.unpack(RAY_KEYBIND_CODES.INTERSECT))

  table.insert(disposables, function ()
    pcall(ContextActionService.UnbindAction, ContextActionService, 'ShowSphere')
    pcall(ContextActionService.UnbindAction, ContextActionService, 'ShowIntersection')
  end)
end


--[!] INIT
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.