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.
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
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)