Skip to content

Instantly share code, notes, and snippets.

@gaymeowing
Created March 21, 2025 12:26
Show Gist options
  • Save gaymeowing/7e48397ebc8175a552c40c3227fb5470 to your computer and use it in GitHub Desktop.
Save gaymeowing/7e48397ebc8175a552c40c3227fb5470 to your computer and use it in GitHub Desktop.
--!native
--[[
setup dragdetector
originally wriitten by PrinceTybalt on 10/6/2023, this is a refactored version
that is a function that can be applied to any drag detector, and doesnt require
cloning any scripts
original: https://create.roblox.com/store/asset/14988529693
]]
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
export type DragDetectorInfo = {
read exclude_descendants_instances: { Instance }?,
read x_plane_offset_min_max: NumberRange?,
read y_plane_offset_min_max: NumberRange?,
read carry_distance: number?,
}
local VECTOR_Y_AXIS = vector.create(0, 1, 0)
local PLAYER = Players.LocalPlayer
local CHARACTER = PLAYER.Character
--[[
controls how far the object can be dragged left/right or up/down,
relative to the player whilst the player is dragging it
]]
local MIN_X_PLANE_OFFSET = -10
local MIN_Y_PLANE_OFFSET = -5
local MAX_X_PLANE_OFFSET = 10
local MAX_Y_PLANE_OFFSET = 15
-- specifies how far in front of the player the object will be carried.
local CARRY_DISTANCE = 8
local EPSILON = 0.00001
local EMPTY = {}
local function GET_AXIS_DISTANCE(
offset_from_orgin: vector, axis_direction: vector,
min: number, max: number
): number
return math.clamp(vector.dot(offset_from_orgin, axis_direction), min, max)
end
local function get_cframe_from_drag_ray(
orgin: vector, direction: vector, part: BasePart,
detector: DragDetector, carry_distance: number,
params: RaycastParams
): CFrame
local normalized_direction = vector.normalize(direction)
local part_rotation = part.CFrame.Rotation
local result = Workspace:Blockcast(
part_rotation + orgin, part.Size, normalized_direction * carry_distance, params
)
local distance = carry_distance
if result then
distance = result.Distance
-- checking if its a physical drag detector
if
detector.ResponseStyle == Enum.DragDetectorResponseStyle.Physical and
not part.Anchored
then
--[[
this will attempt to place it slightly beyond the surface. in the physical case,
this helps push/slide along walls. (We don't to this in the geometric case or we'd penetrate the other objects)
]]
distance += 0.2
end
end
return part_rotation + (orgin + normalized_direction * distance)
end
--[[
returns (orgin, direction)
]]
local function get_ray_eminating_from_player(
orgin: vector, direction: vector, last_view_cframe: CFrame,
carry_distance: number, min_x_plane_offset: number,
max_x_plane_offset: number, min_y_plane_offset: number,
max_y_plane_offset: number
): (vector, vector)
local character = PLAYER.Character
local head: BasePart
local root: BasePart
if character then
head = (character :: any):FindFirstChild("Head")
root = character.PrimaryPart :: any
end
--[[
1: cannot get any ray based on the player
2: when we are in first person, the cursorRay has an origin in the center of the view and a
direction forward in the view direction. So we are shooting a direction straight ahead from our POV
]]
if
(not (head and root)) or
(head.LocalTransparencyModifier > 0.6)
then
-- any casting here because vector type still brokey :(
return orgin, direction
end
--[[
when we are not in first person, we intersect with a plane facing us,
but in front of the player's root, part to find the desired location.
then we construct a ray from the player's character directed toward the intersection with that plane.
]]
local normal_view_frame_look_at = vector.normalize(last_view_cframe.LookVector)
local root_position = root.CFrame.Position
local player_plane_orgin = root_position + normal_view_frame_look_at * carry_distance
--[[
the playerPlane is a plane in front of the player a distance of carry_instance
it should be parallel to the ground; remove the Y component just in case
]]
local level_player_forward = vector.create(normal_view_frame_look_at.x, 0, normal_view_frame_look_at.z)
local normal_player_forward = vector.normalize(level_player_forward)
local direction_normal = vector.normalize(direction)
local rate = vector.dot(direction_normal, normal_player_forward)
if math.abs(rate) < EPSILON then
-- ray is parellel to plane surface; cannot intersect
return orgin, direction
else
local plane_x_dir = vector.normalize(vector.cross(level_player_forward, VECTOR_Y_AXIS))
local plane_distance = -1 * vector.dot(normal_player_forward, player_plane_orgin)
local t = -(plane_distance + vector.dot(orgin, normal_player_forward)) / rate
local offset_from_orgin = (orgin + direction_normal * t) - player_plane_orgin
local y_distance = GET_AXIS_DISTANCE(offset_from_orgin, VECTOR_Y_AXIS, min_y_plane_offset, max_y_plane_offset)
local x_distance = GET_AXIS_DISTANCE(offset_from_orgin, plane_x_dir, min_x_plane_offset, max_x_plane_offset)
local intersection = player_plane_orgin + x_distance * plane_x_dir + y_distance * VECTOR_Y_AXIS
return root_position, intersection - root_position
end
end
local function drag_style_getter_exclude_instances(
cursor_ray: Ray, last_view_cframe: CFrame,
detector: DragDetector, carry_distance: number,
min_x_plane_offset: number, max_x_plane_offset: number,
min_y_plane_offset: number, max_y_plane_offset: number,
params: RaycastParams, exclude_descendants_instances: { Instance }
): CFrame
local part = detector.Parent :: BasePart
local new_filter = { part, unpack(exclude_descendants_instances) }
local direction = cursor_ray.Direction
local orgin = cursor_ray.Origin
if CHARACTER then
table.insert(new_filter, CHARACTER)
orgin, direction = get_ray_eminating_from_player(
orgin, direction, last_view_cframe, carry_distance,
min_x_plane_offset, max_x_plane_offset,
min_y_plane_offset, max_y_plane_offset
)
end
params.FilterDescendantsInstances = new_filter
return get_cframe_from_drag_ray(orgin, direction, part, detector, carry_distance, params)
end
local function drag_style_getter(
cursor_ray: Ray, last_view_cframe: CFrame,
detector: DragDetector, carry_distance: number,
min_x_plane_offset: number, max_x_plane_offset: number,
min_y_plane_offset: number, max_y_plane_offset: number,
params: RaycastParams
): CFrame
local part = detector.Parent :: BasePart
local new_filter = { part }
local direction = cursor_ray.Direction
local orgin = cursor_ray.Origin
if CHARACTER then
table.insert(new_filter, CHARACTER)
orgin, direction = get_ray_eminating_from_player(
orgin, direction, last_view_cframe, carry_distance,
min_x_plane_offset, max_x_plane_offset,
min_y_plane_offset, max_y_plane_offset
)
end
params.FilterDescendantsInstances = new_filter
return get_cframe_from_drag_ray(orgin, direction, part, detector, carry_distance, params)
end
--[[
Sets up a DragDetector to have a custom drag style, similar to games like LumberTycoon2.
Returns the DragDetector, and a cleanup function to stop applying the drag style.
]]
local function setup_dragdetector(detector: DragDetector, info: DragDetectorInfo?): (DragDetector, () -> ())
local exclude_descendants_instances: { Instance }?
local min_x_plane_offset = MIN_X_PLANE_OFFSET
local max_x_plane_offset = MAX_X_PLANE_OFFSET
local min_y_plane_offset = MIN_Y_PLANE_OFFSET
local max_y_plane_offset = MAX_Y_PLANE_OFFSET
local old_drag_style = detector.DragStyle
local last_view_cframe = CFrame.identity
local style_getter = drag_style_getter
local carry_distance = CARRY_DISTANCE
local params = RaycastParams.new()
if info then
local info_x_plane_offset_min_max = info.x_plane_offset_min_max
local info_y_plane_offset_min_max = info.y_plane_offset_min_max
if info_x_plane_offset_min_max then
min_x_plane_offset = info_x_plane_offset_min_max.Min
max_x_plane_offset = info_x_plane_offset_min_max.Max
end
if info_x_plane_offset_min_max then
min_x_plane_offset = info_x_plane_offset_min_max.Min
max_x_plane_offset = info_x_plane_offset_min_max.Max
end
exclude_descendants_instances = info.exclude_descendants_instances
carry_distance = info.carry_distance or carry_distance
if exclude_descendants_instances then
style_getter = drag_style_getter_exclude_instances
end
end
local continue = detector.DragContinue:Connect(function(_, _, view_frame)
last_view_cframe = view_frame
end)
detector.DragStyle = Enum.DragDetectorDragStyle.Scriptable
params.FilterType = Enum.RaycastFilterType.Exclude
detector:SetDragStyleFunction(function(cursor_ray: Ray)
return style_getter(
cursor_ray, last_view_cframe, detector, carry_distance,
min_x_plane_offset, max_x_plane_offset,
min_y_plane_offset, max_y_plane_offset,
params, exclude_descendants_instances
)
end)
return detector, function()
detector:SetDragStyleFunction(function() end)
detector.DragStyle = old_drag_style
continue:Disconnect()
end
end
do
PLAYER.CharacterAdded:Connect(function(character)
CHARACTER = character
end)
PLAYER.CharacterRemoving:Connect(function()
CHARACTER = nil
end)
end
return setup_dragdetector
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment