Was wondering if there is an better/more efficient way to handle creation of highlights when using raycasts?
Code below isnt my full code however does run within a heart beat and uses the camera CFrame. Just incase you want to recreate the script for your self.
local ray: RaycastResult = game:GetService("Workspace"):Raycast(vector, destiny, raycastParams)
if ray then
print(ray.Instance:GetFullName())
if ray.Instance:GetAttribute("IsItem") == true then
local hit = ray.Instance
local HasHighlight = hit:FindFirstChildOfClass("Highlight")
if not HasHighlight then
local highlight = Instance.new("Highlight")
highlight.Parent = hit
highlight.Enabled = true
highlight.Adornee = workspace:FindFirstChild(hit.Name)
highlight.OutlineTransparency = .5
game:GetService("Debris"):AddItem(highlight, 5) -- remove it after a short while
-- no clue how else i would write this.
-- 2:42 am
end
else
print("Nope")
end
end
I’d honestly just remove the prints and try to reduce some more function calls and that’s about it honestly, I’d also avoid destroying the objects, rather pooling them but I believe that’s quite unnecessary (To pool them)
Just to be clear first: I’m assuming what you actually intend to happen is the following, right? i.e. …
Player moves the camera
Raycast is made from the center of the camera to the world
a) If the player hits an object…
If the object that was hit has the IsItem attribute: create the highlight
Otherwise: Skip to (b)
b) If there’s no hit, remove the highlight
Repeat from (1)
Also, are the objects tagged with IsItem static? i.e. does the camera have to move for them to come into view, or can these objects move around the world and appear in front of the camera even if the camera doesn’t move?
Is this intended behaviour? What I’m trying to figure out here is whether you’re wanting:
Only 1 highlight will exist at any given time, and the object its assigned to will be whatever was last hit by the ray cast from the camera; if no objects are intersected with the camera then the last highlight will be destroyed after 5 seconds
More than 1 highlight can appear at a time, any objects hit by the ray will have a highlight created for it and they will be destroyed after 5 seconds (this is what yours does atm)
In this scenario, and as defined by your code, the highlight could appear again after those 5 seconds if the object still intersects with the ray from the camera
There’s a bunch of optimisations that can be made here depending on the answer.
Also, I’m trying to figure out if OP’s code is actually doing what they want it to do. For example, in the code OP posted: the highlight gets removed after 5 seconds but it will reappear the next frame; so why remove it? etc etc
no the object wouldn’t appear twice that is a mistake of interpretation, the snippet searches the children of the hitted instance for Highlights, it wouldn’t have two instances at the same time on the same obj
My goal is to achieve really what doors is doing. I am going to expand ontop of this with an gui but that is >>> NOT <<< important right now.
Functionality explanation:
Player is in first person → moves towards item → script checks if it has the IsItem attribute → if its an item we create a highlight (if one doesnt already exist) → after creating highlight add to debris after 5 seconds it destroys
So basically a classic highlight grabbable item script if you will? At most in my opinion you can pool your highlights and remove the prints, that would be the most performance thing in my honest opinion
If you couldnt already tell (which you hopefully should have by now seeing how youre also a scripter) the prints are meerly for debugging. Please ignore those.
You can set an existing highlight instance’s adornee to the new target when the ray hits something, and set it to nil when it doesn’t.
local highlight = script:WaitForChild("Highlight")
local target = nil
RUN_SERVICE.PreRender:Connect(function()
local raycast_results = doRaycastStuff()
if raycast_results then
if raycast_results.Instace ~= target then
target = raycast_results.Instance
end
elseif target then
target = nil
end
highlight.Adornee = target
end)
I’m typing on my phone rn so sorry for any spelling mistakes
it’s just a joke for me but yes, it’s classic as I have seen many games do it, you can get away doing it how you are doing it just fine honestly, but if you want to go an extra step, pooling like the solution just suggested would do you the job, although it may bring some consequences in the form of some maybe minor bugs when you change the object you are looking at I believe
local HasHighlight = hit:FindFirstChildOfClass("Highlight")
if not HasHighlight then
--- other stuff
However, the code then calls game:GetService('Debris'):AddItem(highlight, 5) - since OP said that the code runs within the context of RunService.Heartbeat it will be added again once the highlight is cleaned up by the Debris service
I see, thanks for explaining. I think my confusion came from the fact that you wanted it to get destroyed after 5 seconds rather than always be present if intersected +/- some animation to flash the opacity, esp. because you don’t normally interact with more than one object at one time. Never played Doors though so not sure how it all works there.
Couple of basic things:
Note: These have been implemented in the example below
Since you want multiple objects, you will need to create a cache/buffer of highlights to cycle between rather than instantiate them all the time. In general, it’s better to not instantiate new objects if you can help it
It should be noted that there is actually a limit to how many highlights you can have on the screen at once - this is even more limited on mobiles
There’s no need to set the Adornee via workspace:FindFirstChild(hit.Name) since you already know the hit object i.e. the ray.Instance
Note that it’s better to cache instances as well rather than using :FindFirstChild as there are performance implications for searching the hierarchy
See reference here under “Performance Note” - ::FindFirstChild() executes approx. 8x slower than using a reference
Heartbeat considerations:
Your objects are static so we only really need to check again if things are different when the camera itself moves; to do this, we can check if the camera has been moved/rotated via FuzzyEq before we try to cast a new ray - checking this will be negligible compared to a ray cast
Heartbeat runs at the client’s frame rate which means that:
If the client has an FPS unlocker installed and is getting 200fps then you’ll be ray casting 200 frames per second
In reality, you only need to raycast every few frames, so we can limit the update frequency of Heartbeat by capping the FPS - in the example below I chose 20fps
Example code
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')
--[!] const
local RAY_DISTANCE = 100 -- i.e. max ray distance
local UPD_FREQUENCY = 1 / 20 -- i.e. how often we perform our checks, default here as 20 fps
local MAX_HIGHLIGHTS = 5 -- i.e. the max number of inspect highlights that can exist at once
local HIGHLIGHT_CLEANUP_TIME = 5 -- i.e. cleanup the highlight after x seconds; 5 seconds in this case
--[!] decl
local camera = workspace.CurrentCamera
local player = Players.LocalPlayer
local playerGui = player:WaitForChild('PlayerGui')
local Raycast = workspace.Raycast -- declare the raycast as a local var so we don't have to index workspace
--[!] setup
-- here we're creating a hashmap to quickly check
-- if an object is already highlighted
local highlightedObjects = { }
-- here we're going to create a cache of highlight prefabs
-- that we can select from rather than creating & deleting
-- many highlights at once
--
-- where cachedHighlights = {
-- object: Highlight, -- the highlight obj
-- cleanup: Task, -- the cleanup task when applied
-- }
--
local cachedHighlights = table.create(MAX_HIGHLIGHTS)
for i = 1, MAX_HIGHLIGHTS, 1 do
local highlight = Instance.new('Highlight')
highlight.Name = 'InspectHighlight'
highlight.Enabled = false
highlight.Parent = playerGui
cachedHighlights[i] = { highlight = highlight }
end
--[!] state
local isAlive = false
local lastUpdate = 0
local lastCameraOrigin
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
--[!] handle states
-- i.e. let's watch to see when the character
-- is alive/dead so that we can toggle our
-- isAlive state
local function handleCharacter(character)
if typeof(character) ~= 'Instance' then
return
end
local humanoid = character:WaitForChild('Humanoid')
if humanoid:getState() == Enum.HumanoidStateType.Dead then
return
end
-- reset the states now that we're alive
lastCameraOrigin = nil
isAlive = true
-- add our new character to the filter
rayParams.FilterDescendantsInstances = { character }
-- watch for death
local death
death = humanoid.Died:Connect(function ()
isAlive = false
if death and death.Connected then
death:Disconnect()
end
end)
end
if player.Character then
task.spawn(handleCharacter, player.Character)
end
player.CharacterAdded:Connect(handleCharacter)
--[!] main
-- NOTE:
-- Don't forget to cleanup runtime via `runtime:Disconnect()`
--
local runtime = RunService.Heartbeat:Connect(function (dt)
-- ignore if we're dead
if not isAlive then
return
end
-- limit the update frequency to our desired fps
lastUpdate += dt
if lastUpdate < UPD_FREQUENCY then
return
end
lastUpdate = math.fmod(lastUpdate, UPD_FREQUENCY)
-- only raycast if our camera is sufficiently different from the previous frame
local origin = camera.CFrame
if lastCameraOrigin and origin:FuzzyEq(lastCameraOrigin) then
return
end
lastCameraOrigin = origin
-- perform the raycast & apply highlight
local result = Raycast(workspace, origin.Position, origin.LookVector*RAY_DISTANCE, rayParams)
local instance = result and result.Instance or nil
if instance then
if instance:GetAttribute('IsItem') then
-- i.e. ignore if we are already highlighting it and/or it was recently highlighted
if highlightedObjects[instance] then
return
end
-- pop the highlight from the front of the list
local group = table.remove(cachedHighlights, 1)
local highlight = group.highlight
-- set the object as already highlighted
highlightedObjects[instance] = true
-- if the highlight is still assigned to something
-- we need to cancel its cleanup task and remove
-- the debounce check for the current instance
local thread = group.cleanup
if thread and coroutine.status(thread) ~= 'dead' then
pcall(task.cancel, thread)
end
local adornee = highlight.Adornee
if adornee then
highlightedObjects[adornee] = nil
end
-- assign the highlight to the new object
highlight.Enabled = true
highlight.Adornee = instance
-- clean it up after x seconds
group.cleanup = task.delay(HIGHLIGHT_CLEANUP_TIME, function ()
highlight.Enabled = false
highlight.Adornee = nil
highlightedObjects[instance] = nil
end)
-- append to back of list
table.insert(cachedHighlights, group)
end
end
end)