Better way to handle highlights using raycasts?

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.

  1. Player moves the camera

  2. 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
  3. 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?

1 Like

I don’t get why you would need so much information to infer that it’s just a raycast on -Z as direction (Forward)

The highlight is added to the debris service and removed after 5 seconds.

But you are correct

1 Like

Is this intended behaviour? What I’m trying to figure out here is whether you’re wanting:

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

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

2 Likes

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

2 Likes

Ok so let me simplify it for you.

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

1 Like

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

1 Like

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.

And I suppose its classic?

1 Like

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

1 Like

I was thinking doing this. Wrote the original code at like 2am - 3am so looks a bit stupid.

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

I’ve swapped from rays to mouse instead just to make my life a bit easier.

Thanks to you and many others for their help.

Yes, it does check whether the highlight exists:

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

  1. 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
    • See issue tracker here and here
  2. 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
  3. 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)

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.