Location Marker System

Location Marker System
Hello again developers! Recently I’ve been working on a really cool project: a GUI based location marking system! This system helps players find their way around the map, especially things off screen. I had a ton of fun designing the algorithm to do this :happy3:. I’ll explain how it works below for anyone interested.

Showcase
Without further ado, the GIF!
ezgif.com-gif-maker (23)
(Location Marker System (Game) - Roblox)

Details
For everyone interested, here is a detailed guide on how the algorithm works.

How The Algorithm Works

To start, lets look at what the algorithm does. It has two parts: positioning the GUI and getting the direction the arrow should point.

When the location is on screen, the algorithm is very simple: there isn’t an arrow, and the position is based directly on Camera:WorldToScreenPoint(Vector3 worldPoint).

The complexity begins when the location becomes off screen, or in this case, off the “buffer” screen (screen size with a bit of each side cut off).

The first goal here is to get the 2D direction the camera should move to look directly towards the location. We can do this by taking the offset between the location’s position and the camera’s position, then changing that Vector3 offset (which is currently relative to the world) to be relative to the camera. We can do this with cameraCFrame:VectorToObjectSpace(offset), where cameraCFrame is the camera’s CFrame and offset is the offset between the location and the camera (location - cameraCFrame.Position). Now we have something like this:
Example 23980243

(Note the length of the red vector can be any value)

Now, how does this help us find what we want to find? Well, we want to find the green arrow in this picture:

Example 23980243

The green arrow is the 2D direction we want the camera to move to get to the direction of the red vector. Here is a 2D picture of this set up looking at it head on:

Example 8972343243

So how do we do this? Well, all we need to do is take out the forward part of our 3D direction to the object. We can do this with: Vector2.new(relativeDirection.X, relativeDirection.Y), where relativeDirection is the 3D red vector in the pictures above. Now we have that green arrow, but we want it to be normalized: have a magnitude/length of 1. To do this, we just get the .Unit property of the Vector2: Vector2.new(relativeDirection.X, relativeDirection.Y).Unit
And we did it: that’s the direction the arrow should face!

Now we need to get the position the GUI should be at. First, we get the longest distance of the buffer screen:

Example 923432

Using this length, we can conceptually make a circle around our buffer screen with a radius of this length:

Example 908238432

Now if we take our direction (the 2D green vector) and multiply it by the longest distance, we get something like this:

Example 349234234

Notice that blue dot? That’s the position were trying to find. To find it, we need to split this into two cases: when the end point of the green line is past the sides of the lime box (the buffer screen) and when the end of the green line is past the bottom or top of the lime box.

To do this, we simply check if the absolute value of the Y component of the green line is greater than half the height of the box. If it is, the blue dot falls on the top or bottom of our screen. If it’s not, then the blue dot falls on the sides of our screens.

In both of the cases below, we are trying to find a number to multiply the direction vector (the green vector but with a magnitude of 1) so that the resulting vector ends on the side of the lime box (buffer screen).

Case 1, The dot falls on the top or bottom of the screen:
In this case, we want the y component of the position of the blue dot to equal plus or minus half the height of the screen. We can use trig for this:

Example 2342323432

(height refers to the height of the buffer screen(the lime box))

This means all we need to do to get the position of the blue dot is relativeDirection2D * math.abs(maxBoundsY/2/relativeDirection2D.Y)
where relaitveDirection2d is the green vector with a length of one, maxBoundsY is the height of the lime box, the buffer screen. (If you’re wondering why there are absolute values, it’s in case a is negative. If a is negative then it cancels out the negative direction of the direction vector (think -1 * -1 = 1).)

And that’s everything! We know the direction the arrow should face and the position the UI should be in!

Features

  • Customizability: the color, image, and visibility of the indicators can all be adjusted easily.
  • Automatic updating: when a player’s team or an Indicator’s Attributes are changed the GUI responds.
  • Automatic clean up: indicators can be destroyed without causing problems

Instructions

  1. Get the model here: Location Marker System - Roblox.
  2. Insert the model into your game.
  3. (Optional) Move “IndicatorsClient” from “Loader” to StarterPlayerScripts (StarterPlayer > StarterPlayerScripts) and remove “Loader”
  4. That’s it!

Want to use this for a game with StreamingEnabled? Look at this reply.

Creating Indicators
I’d recommend just copying existing indicators and changing the settings, but if you want to create a new one:

  1. Create an Attachment and name it “Indicator”
  2. Add an attribute “Color” of type Color3
  3. Add an attribute “Enabled” of type boolean
  4. Add an attribute “Image” of type string
  5. Add an attribute “Team” of type BrickColor

Changing the Color and Image

  1. Open properties
  2. Select the Indicator
  3. Change the Color and Image attributes:
    image

Changing the Visibility
There are two ways to change the visibility: you can change the Enabled attribute or set the Team attribute. The Enabled attribute turns the indicator on and off. The Team attribute sets which team can see the indicator (set to the team’s BrickColor). If you want all teams to see the indicator, set the Team attribute to the White BrickColor (neutral team color).

(Advanced) Changing the Size of All Indicators
To change the size of all indicators, go into the CreateNewIndicator module (IndicatorsClient > CreateNewIndicator) and look at the first line it should look like: local DEFAULT_SIZE = UDim2.new(0.038, 0, 0.038, 0). Change the values to what ever you want (example: local DEFAULT_SIZE = UDim2.new(0.041, 0, 0.041, 0)).

Last Bit
Let me know if you have any feedback! I hope you all find this helpful :happy1:
Model: Location Marker System (Model) - Roblox
Uncopylocked game: Location Marker System (Game) - Roblox

218 Likes

Just in time ! A very good work, thanks for sharing

4 Likes

Wow, that looks very interesting. I’ve been working with a minimap tool that can display blips on the border and for some games that works really well. Now you have taken this bordersnapping concept in a whole new direction. I definitely like the look of it and will try it out. Thank you for releasing this.

5 Likes

Fastest bookmark in the west.

Amazing system, I know a lot of people who have needed something like this in the past and I finally show them a decent resource that does it.

6 Likes

I was hoping the following code would work, but it doesn’t appear to work on either my laptop or my android phone. Is it possible to tag players and/or NPCs?

local Players = game:GetService("Players")

game.Players.CharacterAutoLoads = false

-- Tag all players except the LocalPlayer.
local function onPlayerAdded(player)
	if player ~= Players.LocalPlayer then
		player.CharacterAdded:Connect(function(character)
			local indicator = Instance.new("Attachment")
			indicator:SetAttribute("Color", BrickColor.new("Lime green").Color)
			indicator:SetAttribute("Enabled", true)
			indicator:SetAttribute("Image", "rbxassetid://8239527343")
			indicator:SetAttribute("Team", BrickColor.new("White"))
			indicator.Parent = character:WaitForChild("HumanoidRootPart")
		end)
	end
end

for _, player in pairs(Players:GetPlayers()) do
	onPlayerAdded(player)
end
Players.PlayerAdded:Connect(onPlayerAdded)
3 Likes

I’ve got another situation that isn’t working the way I would like. In short, I have an Egg Hunt game where I do a lot of the egg setup in code. When I added your system I get the following error:

16:58:39.586  Players.JaxxonJargon.PlayerScripts.IndicatorsClient:85: attempt to index nil with number  -  Client - IndicatorsClient:85
  16:58:39.587  Stack Begin  -  Studio
  16:58:39.587  Script 'Players.JaxxonJargon.PlayerScripts.IndicatorsClient', Line 85 - function updateIndicatorPositions  -  Studio - IndicatorsClient:85
  16:58:39.587  Stack End  -  Studio

The line that is referenced in the error is the following:

	local bufferSize = indicatorTable[1][2].AbsoluteSize.X

And here are the relevant pieces of my LocalScript:

local function getIndicator()
	local indicator = Instance.new("Attachment")
	indicator:SetAttribute("Color", BrickColor.new("Lime green").Color)
	indicator:SetAttribute("Enabled", true)
	indicator:SetAttribute("Image", "rbxassetid://8239527343") -- Target.
	indicator:SetAttribute("Team", BrickColor.new("White"))
	return indicator
end

for _, egg in ipairs(workspace.HiddenEggs:GetChildren()) do
	egg.Anchored = true
	-- Assign a random color to the egg so it is different each time the game is played.
	egg.BrickColor = BrickColor.random()
	-- Update the location indicator to match the egg color.
	local indicator = getIndicator()
	indicator:SetAttribute("Color", egg.BrickColor.Color)
	indicator.Parent = egg
end
3 Likes

Whoops! I didn’t handle the case where there weren’t any indicators. That error shouldn’t cause any problems. Let me know if it did.(Edit: I might not have mentioned this, new indicators need to be named “Indicator”. Sorry about that!) To fix that error:

Replace:

local bufferSize = indicatorTable[1][2].AbsoluteSize.X

With:

-- Edited:
local firstIndicatorData = indicatorTable[1]
if not firstIndicatorData then return end
local bufferSize = firstIndicatorData[2].AbsoluteSize.X
--
	
-- Removed: local bufferSize = indicatorTable[1][2].AbsoluteSize.X

I’m looking into your first problem. Did you try resetting the other characters? I think the problem is that you start the connection when their characters are already added. Either way, like what you did with the player added, you should run the character code on already existing character in addition to making the connection for new ones. (Edit: Probably the same problem as above. My bad, sorry about that!)


Thanks for the feedback! Nice catch with that first bug. (Edit: I updated the topic to have that info.)

4 Likes

I made your changes and changed my code to the following:

local function getIndicator()
	local indicator = Instance.new("Attachment")
	indicator.Name = "Indicator"
	indicator.Position = Vector3.new(0, 3, 0)
	indicator:SetAttribute("Color", BrickColor.new("Lime green").Color)
	indicator:SetAttribute("Enabled", true)
	indicator:SetAttribute("Image", "rbxassetid://8239527343") -- Target.
	indicator:SetAttribute("Team", BrickColor.new("White"))
	return indicator
end

I also Destroy the indicator when the egg is collected. As far as I can see everything is working the way I would expect. However, I am getting a bunch of these errors:

18:34:13.334  ArrowFrame is not a valid member of Frame "MainFrame"  -  Client - IndicatorsClient:125
  18:34:13.335  Stack Begin  -  Studio
  18:34:13.335  Script 'Players.JaxxonJargon.PlayerScripts.IndicatorsClient', Line 125 - function updateIndicatorPositions  -  Studio - IndicatorsClient:125
  18:34:13.335  Stack End  -  Studio
3 Likes

I’m still having trouble with the first example and I’m not sure how to proceed. If you want to see my Egg Hunt game with Location Markers the game is Uncopylocked and Open Source:

I’m not sure if location markers make sense for this game in the long run as it kind of makes hunting for the eggs too easy. So I’m tempted to make a new game that fits well with your tool. If I do I will let you know. :slight_smile:

4 Likes

Would it make sense to have a MaxFalloffDistance similar to a Sound within which the transparency of the Location Marker goes from 0 to 1? (Or something similar.) I’m trying to deal with the fact that I might have a lot of objects tagged and cluttering up the UI. Close markers should stand out from distant ones (at least in the contexts I have in mind).

2 Likes

Thanks again! (I really should have tested this more :doh:)

I updated the model and the uncopylocked game. Here is the IndicatorClient code:

New code for: "IndicatorClient"
-- v.1.2

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")


local createNewIndicator = require(script:WaitForChild("CreateNewIndicator"))
local updateIndicatorUI = require(script:WaitForChild("UpdateIndicatorUI"))


local camera = Workspace.CurrentCamera

local player = Players.LocalPlayer

local screenGui = Instance.new("ScreenGui")
screenGui.Name = "IndicatorGui"
screenGui.IgnoreGuiInset = true
screenGui.ResetOnSpawn = false
screenGui.Parent = player:WaitForChild("PlayerGui")


local indicatorTable = {}
-- {attachment, ui, connections}


local function removeIndicator(attachment)
	for index, data in ipairs(indicatorTable) do
		if data[1] == attachment then
			data[2]:Destroy()
			for _, connection in ipairs(data[3]) do
				if connection then
					connection:Disconnect()
				end
			end
			table.remove(indicatorTable, index)
		end
	end
end

local function processWorkspaceDescendantAdded(descendant)
	if descendant:IsA("Attachment") and descendant.Name == "Indicator" then
		removeIndicator(descendant)
		
		local newIndicator = createNewIndicator()
		local connections = {}
		
		local attributeChanged = descendant.AttributeChanged:Connect(function(name)
			if (name == "Image") or (name == "Team") or (name == "Color") or (name == "Enabled") then
				updateIndicatorUI(descendant, newIndicator)
			end
		end)
		table.insert(connections, attributeChanged)
		
		local data = {}
		data[1] = descendant
		data[2] = newIndicator
		data[3] = connections
		table.insert(indicatorTable, data)
		
		updateIndicatorUI(descendant, newIndicator)
		newIndicator.Parent = screenGui
	end
end

for _, descendant in ipairs(Workspace:GetDescendants()) do
	processWorkspaceDescendantAdded(descendant)
end

Workspace.DescendantAdded:Connect(processWorkspaceDescendantAdded)


local function processWorkspaceDescendantRemoving(descendant)
	if descendant:IsA("Attachment") and descendant.Name == "Indicator" then
		removeIndicator(descendant)
	end
end

Workspace.DescendantRemoving:Connect(processWorkspaceDescendantAdded)

local function updateIndicatorPositions()
	local viewportX = camera.ViewportSize.X
	local viewportY = camera.ViewportSize.Y
	
	if not indicatorTable[1] then return end
	local bufferSize = indicatorTable[1][2].AbsoluteSize.X
	
	
	local maxBoundsX = viewportX - (bufferSize * 2)
	local maxBoundsY = viewportY - (bufferSize * 2)
	
	
	local camCFrame = camera.CFrame
	local screenHypotenuse = math.sqrt((maxBoundsX/2)^2+(maxBoundsY/2)^2)
	
	
	local cameraForward
	do
		local cameraForward3D = camera.CFrame.LookVector
		cameraForward = Vector2.new(cameraForward3D.X, cameraForward3D.Z).Unit
	end

	for _, data in ipairs(indicatorTable) do
		local indicatorUI = data[2]
		if indicatorUI.Visible == false then
			continue
		end

		local position = data[1].WorldPosition

		local screenPosition3d, onScreen = camera:WorldToViewportPoint(position)

		local xPosition = math.clamp(screenPosition3d.X, bufferSize, viewportX - bufferSize)
		local yPosition = math.clamp(screenPosition3d.Y, bufferSize, viewportY - bufferSize)



		if (xPosition == screenPosition3d.X) and (yPosition == screenPosition3d.Y) and onScreen then
			indicatorUI.ArrowFrame.Visible = false
		else
			indicatorUI.ArrowFrame.Visible = true
			
			local worldDirection = position - camCFrame.Position
			local relativeDirection = camCFrame:VectorToObjectSpace(worldDirection)
			local relativeDirection2D = Vector2.new(relativeDirection.X, relativeDirection.Y).Unit

			
			local testScreenPoint = relativeDirection2D * screenHypotenuse
			
			local angle = math.atan2(relativeDirection2D.X, relativeDirection2D.Y)
			
			local screenPoint
			if math.abs(testScreenPoint.Y) > maxBoundsY/2 then
				screenPoint = relativeDirection2D * math.abs(maxBoundsY/2/relativeDirection2D.Y)
			else
				screenPoint = relativeDirection2D * math.abs(maxBoundsX/2/relativeDirection2D.X) -- TODO Try flip sin cos
			end
			
			xPosition = viewportX / 2 + screenPoint.X
			yPosition = viewportY / 2 - screenPoint.Y
			
			
			indicatorUI.ArrowFrame.Rotation = math.deg(angle)
		end
		

		indicatorUI.Position = UDim2.fromOffset(xPosition, yPosition)
	end
end

RunService.RenderStepped:Connect(updateIndicatorPositions)

player:GetPropertyChangedSignal("TeamColor"):Connect(function()
	for _, data in ipairs(indicatorTable) do
		updateIndicatorUI(data[1], data[2])
	end
end)

It was a pretty quick fix (I’d forgotten to remove the data table when cleaning up markers). The problem you had is fixed :+1:


To make the indicators fade based on distance, place this code

--
local maxDistance = 100
local minDistance = 10
		
local distance = (position - camCFrame.Position).Magnitude
		
local transparency = 0
if distance > minDistance then
	transparency = math.clamp((distance-minDistance)/(maxDistance-minDistance), 0, 1)
end
		
data[2].Transparency = transparency
data[2].ArrowFrame.ArrowImage.ImageTransparency = transparency
data[2].IconImage.ImageTransparency = transparency
data[2].LayoutOrder = math.round(distance/(maxDistance - minDistance)*10)
--

under

for _, data in ipairs(indicatorTable) do
	local indicatorUI = data[2]
	if indicatorUI.Visible == false then
		continue
	end

	local position = data[1].WorldPosition
Example code
local function updateIndicatorPositions()
	local viewportX = camera.ViewportSize.X
	local viewportY = camera.ViewportSize.Y
	
	if not indicatorTable[1] then return end
	local bufferSize = indicatorTable[1][2].AbsoluteSize.X
	
	
	local maxBoundsX = viewportX - (bufferSize * 2)
	local maxBoundsY = viewportY - (bufferSize * 2)
	
	
	local camCFrame = camera.CFrame
	local screenHypotenuse = math.sqrt((maxBoundsX/2)^2+(maxBoundsY/2)^2)
	
	
	local cameraForward
	do
		local cameraForward3D = camera.CFrame.LookVector
		cameraForward = Vector2.new(cameraForward3D.X, cameraForward3D.Z).Unit
	end

	for _, data in ipairs(indicatorTable) do
		local indicatorUI = data[2]
		if indicatorUI.Visible == false then
			continue
		end

		local position = data[1].WorldPosition
		
		--
		local maxDistance = 100
		local minDistance = 10

		local distance = (position - camCFrame.Position).Magnitude

		local transparency = 0
		if distance > minDistance then
			transparency = math.clamp((distance-minDistance)/(maxDistance-minDistance), 0, 1)
		end

		data[2].Transparency = transparency
		data[2].ArrowFrame.ArrowImage.ImageTransparency = transparency
		data[2].IconImage.ImageTransparency = transparency
		data[2].LayoutOrder = math.round(distance/(maxDistance - minDistance)*10)
		--

		local screenPosition3d, onScreen = camera:WorldToViewportPoint(position)

		local xPosition = math.clamp(screenPosition3d.X, bufferSize, viewportX - bufferSize)
		local yPosition = math.clamp(screenPosition3d.Y, bufferSize, viewportY - bufferSize)

		

		if (xPosition == screenPosition3d.X) and (yPosition == screenPosition3d.Y) and onScreen then
			indicatorUI.ArrowFrame.Visible = false
		else
			indicatorUI.ArrowFrame.Visible = true
			
			local worldDirection = position - camCFrame.Position
			local relativeDirection = camCFrame:VectorToObjectSpace(worldDirection)
			local relativeDirection2D = Vector2.new(relativeDirection.X, relativeDirection.Y).Unit

			
			local testScreenPoint = relativeDirection2D * screenHypotenuse
			
			local angle = math.atan2(relativeDirection2D.X, relativeDirection2D.Y)
			
			local screenPoint
			if math.abs(testScreenPoint.Y) > maxBoundsY/2 then
				screenPoint = relativeDirection2D * math.abs(maxBoundsY/2/relativeDirection2D.Y)
			else
				screenPoint = relativeDirection2D * math.abs(maxBoundsX/2/relativeDirection2D.X) -- TODO Try flip sin cos
			end
			
			xPosition = viewportX / 2 + screenPoint.X
			yPosition = viewportY / 2 - screenPoint.Y
			
			
			indicatorUI.ArrowFrame.Rotation = math.deg(angle)
		end
		

		indicatorUI.Position = UDim2.fromOffset(xPosition, yPosition)
	end
end

Fading Markers

minDistance is the distance where all markers will be fully visible. maxDistance is the distance where the markers will be fully invisible.


Thanks again for helping me fix some bugs :happy2:


Edit:

Yep :+1:

If there is a new version, you should just be able to copy that code back into the same place (probably).

4 Likes

Awesome support. I’m now a big fan. :slight_smile:

And here is the game that I’d really like to flesh out with Location Markers. The game is only a few days old so it works but doesn’t have any real gameplay other than the fact that it is a two-player game on an island with weapons. In any event, I added location markers for the significant features and will continue to refine it using the suggestions you’ve given above. TYVM

1 Like

Thank you for the example code. Am I correct that this would change all the location markers? I might want more control so that I could have different rules for different types of features. And I’m leery of making changes to your source code because that would make it difficult to maintain with any updates you put out.

I know I’m asking for the moon, but why not? :wink:

In the mean time I’ll try out the code you posted to see how it works on Treasure Island.

1 Like

I was literally just thinking of asking how to make one of these. Creepy.

But really cool and smooth, nice job!

2 Likes

OMG I am so in love with this! I added different distances for the Egg Hunt game vs. Treasure Island and the results are terrific, IMNSHO. No more cluttered UI and the transparency value tells you how far away the object is. I love it! I will definitely be using this in both games. I’ll probably make it a powerup or reward but it makes such a difference in navigating a big map looking for things. I even like it combined with the minimap system that I’ve been using. Awesome work. And your code looks very clean and readable. Good job.

1 Like

I believe that processWorkspaceDescendantAdded is being called whenever a workspace descendant is removed. It looks like you’re connecting the wrong function.

Great module though. I’m definitely using this in my game

1 Like

Good catch! You’re totally right: it should be processWorkspaceDescendantRemoving, not processWorkspaceDescendantAdded! Thanks a bunch :happy2:

I updated the game and the model :+1:

2 Likes

Sorry but this post makes no sense, put what in what? it’s not working for me whatever i do. could you elaborate? currently monkey-brained without my ADHD medication

1 Like

No worries. If you want to make the indicators fade based on distance, place the code block above under where it says “local position = data[1].WorldPosition”. You can also just replace the entire “updateIndicatorPositions” function with the code under the “Example code” drop down.

You can also replace all the code inside “IndicatorClient” with the code below:

-- v.1.2

local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")


local createNewIndicator = require(script:WaitForChild("CreateNewIndicator"))
local updateIndicatorUI = require(script:WaitForChild("UpdateIndicatorUI"))


local camera = Workspace.CurrentCamera

local player = Players.LocalPlayer

local screenGui = Instance.new("ScreenGui")
screenGui.Name = "IndicatorGui"
screenGui.IgnoreGuiInset = true
screenGui.ResetOnSpawn = false
screenGui.Parent = player:WaitForChild("PlayerGui")


local indicatorTable = {}
-- {attachment, ui, connections}


local function removeIndicator(attachment)
	for index, data in ipairs(indicatorTable) do
		if data[1] == attachment then
			data[2]:Destroy()
			for _, connection in ipairs(data[3]) do
				if connection then
					connection:Disconnect()
				end
			end
			table.remove(indicatorTable, index)
		end
	end
end

local function processWorkspaceDescendantAdded(descendant)
	if descendant:IsA("Attachment") and descendant.Name == "Indicator" then
		removeIndicator(descendant)
		
		local newIndicator = createNewIndicator()
		local connections = {}
		
		local attributeChanged = descendant.AttributeChanged:Connect(function(name)
			if (name == "Image") or (name == "Team") or (name == "Color") or (name == "Enabled") then
				updateIndicatorUI(descendant, newIndicator)
			end
		end)
		table.insert(connections, attributeChanged)
		
		local data = {}
		data[1] = descendant
		data[2] = newIndicator
		data[3] = connections
		table.insert(indicatorTable, data)
		
		updateIndicatorUI(descendant, newIndicator)
		newIndicator.Parent = screenGui
	end
end

for _, descendant in ipairs(Workspace:GetDescendants()) do
	processWorkspaceDescendantAdded(descendant)
end

Workspace.DescendantAdded:Connect(processWorkspaceDescendantAdded)


local function processWorkspaceDescendantRemoving(descendant)
	if descendant:IsA("Attachment") and descendant.Name == "Indicator" then
		removeIndicator(descendant)
	end
end

Workspace.DescendantRemoving:Connect(processWorkspaceDescendantAdded)

local function updateIndicatorPositions()
	local viewportX = camera.ViewportSize.X
	local viewportY = camera.ViewportSize.Y
	
	if not indicatorTable[1] then return end
	local bufferSize = indicatorTable[1][2].AbsoluteSize.X
	
	
	local maxBoundsX = viewportX - (bufferSize * 2)
	local maxBoundsY = viewportY - (bufferSize * 2)
	
	
	local camCFrame = camera.CFrame
	local screenHypotenuse = math.sqrt((maxBoundsX/2)^2+(maxBoundsY/2)^2)
	
	
	local cameraForward
	do
		local cameraForward3D = camera.CFrame.LookVector
		cameraForward = Vector2.new(cameraForward3D.X, cameraForward3D.Z).Unit
	end

	for _, data in ipairs(indicatorTable) do
		local indicatorUI = data[2]
		if indicatorUI.Visible == false then
			continue
		end

		local position = data[1].WorldPosition
		
		--
		local maxDistance = 100
		local minDistance = 10

		local distance = (position - camCFrame.Position).Magnitude

		local transparency = 0
		if distance > minDistance then
			transparency = math.clamp((distance-minDistance)/(maxDistance-minDistance), 0, 1)
		end

		data[2].Transparency = transparency
		data[2].ArrowFrame.ArrowImage.ImageTransparency = transparency
		data[2].IconImage.ImageTransparency = transparency
		data[2].LayoutOrder = math.round(distance/(maxDistance - minDistance)*10)
		--

		local screenPosition3d, onScreen = camera:WorldToViewportPoint(position)

		local xPosition = math.clamp(screenPosition3d.X, bufferSize, viewportX - bufferSize)
		local yPosition = math.clamp(screenPosition3d.Y, bufferSize, viewportY - bufferSize)

		

		if (xPosition == screenPosition3d.X) and (yPosition == screenPosition3d.Y) and onScreen then
			indicatorUI.ArrowFrame.Visible = false
		else
			indicatorUI.ArrowFrame.Visible = true
			
			local worldDirection = position - camCFrame.Position
			local relativeDirection = camCFrame:VectorToObjectSpace(worldDirection)
			local relativeDirection2D = Vector2.new(relativeDirection.X, relativeDirection.Y).Unit

			
			local testScreenPoint = relativeDirection2D * screenHypotenuse
			
			local angle = math.atan2(relativeDirection2D.X, relativeDirection2D.Y)
			
			local screenPoint
			if math.abs(testScreenPoint.Y) > maxBoundsY/2 then
				screenPoint = relativeDirection2D * math.abs(maxBoundsY/2/relativeDirection2D.Y)
			else
				screenPoint = relativeDirection2D * math.abs(maxBoundsX/2/relativeDirection2D.X) 
			end
			
			xPosition = viewportX / 2 + screenPoint.X
			yPosition = viewportY / 2 - screenPoint.Y
			
			
			indicatorUI.ArrowFrame.Rotation = math.deg(angle)
		end
		

		indicatorUI.Position = UDim2.fromOffset(xPosition, yPosition)
	end
end

RunService.RenderStepped:Connect(updateIndicatorPositions)
1 Like

ah! i also have another question, is there a way i could have the markers be shown despite them not being streamed?

1 Like