How to implement smart hooking logic?

Hi, I’m currently developing an AOT game with heavy inspiration from AOT:R.
I’m trying to understand the logic behind to how they detect the nearest surface’s edge

For example (the > and < are the nearest hookable points):

I’ve tried everything but none really achieve the desired result I was looking for.
My theory is that they fired a single ray with multiple rays firing from both sides, but that doesn’t tell me how they got the precise position.

Please help :"

1 Like

I’d be tempted to say that each of these models have invisible nodes in them, using ScreenPointToRay or some sort of function to detect what nodes are closest to each side of the mouse

While it could work I feel like it’s unethical, it should be dynamic

bumpingbumpingbumpingbumpingbumpingbumpingbumping

Why so?
That a pretty good idea to prevent heavy computation.
You can try making sort of plugin to pre compile poses inside studio.

Individually putting “invisible nodes” in all models requires heavier computation than simply casting a ray to find the nearest hookable surface.

???
What are you talking about, and where did you get this untrue statistic?

Otherwise, make certain materials hookable, like wood, and make others not hookable if you want it to be universal.

I don’t think you’re understanding my question

Have you found a solution to this?

Before I tell you my approach, I would like to vouch for @westside216’s node suggestion as for level geometry like trees that can be difficult for raycasting to detect otherwise. I would actually recommend a hybrid system if you insist on a dynamic algorithm as they work as fallbacks for one another.

Dynamically checking for geometry in a given direction will require raycasting, and depending on distance, you’ll have to use quite a bit if you want to ensure reliability, but raycasting is a pretty fast operation. Spherecasting is something I considered, but it’s far more expensive, and rays scale much better while offering better coverage in my opinion.

This script I wrote a will cast in a flat cone directed towards the mouse.
It casts 72 rays every 0.05 seconds within a 200 stud range. The green dots indicate a hook point.


While it gives you an array of valid positions, going about narrowing down the most appropriate is probably something I need more gameplay insight on in order to give the best advice

Demo Script
--[[ Variables ]]--
-- Services --
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")

-- User --
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local mouse = player:GetMouse()

-- Tool --
local tool = script.Parent

-- Constants --
local HOOK_SEARCH_RANGE = 200
local HOOK_SEARCH_ANGLE = math.rad(45)
local HOOK_SEARCH_ITERATIONS = 72

--[[ Functions ]]--
local function debug_clearPreviewPoints() : ()
	for _, v in CollectionService:GetTagged("HookPreview") do
		v:Destroy()
	end
end

local function debug_makePreviewPoint() : Part
	local part = Instance.new("Part")
	part.Shape = Enum.PartType.Ball
	part.Anchored = true
	part.CanCollide = false
	part.CanQuery = false
	part.Transparency = 0.6
	part.Size = Vector3.new(1, 1, 1)
	part.Material = Enum.Material.Neon
	part.Color = Color3.new(0, 1, 0)
	part.Parent = workspace
	part:AddTag("HookPreview")
	
	return part
end

local function getSearchCastDirections(aimMatrix : CFrame) : {Vector3}
	local castDirections = {
		aimMatrix.LookVector * HOOK_SEARCH_RANGE
	}
	
	for i = 1, HOOK_SEARCH_ITERATIONS do
		local theta = HOOK_SEARCH_ANGLE * (i / HOOK_SEARCH_ITERATIONS) - HOOK_SEARCH_ANGLE / 2
		local rotated = aimMatrix * CFrame.Angles(0, theta, 0)

		table.insert(castDirections, rotated.LookVector * HOOK_SEARCH_RANGE)
	end

	return castDirections
end

local function findHookPoints(aimMatrix : CFrame) : {Vector3}
	local hookPoints = {}
	
	local castParams = RaycastParams.new()
	castParams.FilterDescendantsInstances = {player.Character}
	
	local castOrigin = aimMatrix.Position
	for _, castDirection in getSearchCastDirections(aimMatrix) do
		local castResult = workspace:Raycast(castOrigin, castDirection, castParams)
		if castResult ~= nil then
			table.insert(hookPoints, castResult.Position)
		end
	end
	
	return hookPoints
end

local function getAimMatrix() : CFrame
	local origin = player.Character.Head.Position
	
	local look = (mouse.Hit.Position - origin).Unit
	local right = camera.CFrame.RightVector
	local up = -look:Cross(right)
	
	return CFrame.fromMatrix(origin, right, up, -look)
end

local function onEquipped() : ()
	local parts = {}
	for i = 1, HOOK_SEARCH_ITERATIONS do
		table.insert(parts, debug_makePreviewPoint())
	end
	
	while tool.Parent == player.Character do
		local hookPoints = findHookPoints(getAimMatrix())
		for i, v in parts do
			v.Position = hookPoints[i] or Vector3.zero
		end
		
		task.wait(0.05)
	end
end
tool.Equipped:Connect(onEquipped)

local function onUnequipped() : ()
	debug_clearPreviewPoints()
end
tool.Unequipped:Connect(onUnequipped)
2 Likes