Location Marker System

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

Ah, this is something I hadn’t considered! I’m honestly not sure if I programmed everything such that it can work with stuff being streamed in and out.

Assuming the code works though, the indicators would just need to be created on the client. I can program something specifically for this case.

@LuckyLion1378

Note: This is only for StreamingEnabled, which loads and unloads things that are far away (which includes all the indicators this system uses).

Okay! I finished the code for this. Basically how it works:

  • Takes all indicators in and added to Workspace
  • Gives them an ID and sends their data through a RemoteEvent to the client
  • The client takes that data and creates a new Indicator on the client
  • The server also sends RemoteEvents to update Color, Team, Image, and Enabled (when the server changes those properties). NOTE: Position is not replicated with this new system.
  • The indicators being destroyed on the server is also replicated through a RemoteEvent.

How to use:


Use cases:

  • Only for streaming enabled
  • Doesn’t work for things that move (at least movement isn’t replicated). I programmed it mainly for static Indicators that don’t change much.

Note: Changing indicators through LocalScripts doesn’t work because the indicators aren’t in the same place on the client. They are all under a single client-only part in Workspace. (Changes on the server ones replicate to the client ones though).


Let me know if this gives you any problems :+1:

2 Likes

does this streamingenabled one have fading out?

1 Like

nevermind, it works like a charm! thanks a million

1 Like

Thank you for this great resource.

Any plans to add the the ability of changing indicators through LocalScripts?

I would really like to use this system for things like a tutorial or quest system where indicators are turned on/off just for a specific player. I feel like changing the player group for every specific quest or tutorial is perhaps not optimal.

1 Like

To change an indicator from a LocalScript:

  • Find/wait-for the indicator Attachment
  • Change it’s attributes (Color, Image, Enabled, Team)

For your case, I’d recommend either:

  • Having pre-made indicators on the server with Enabled false then changing Enabled to true on the client when you need them.

or

  • Creating the indicators on the client when you need them and removing them when you don’t

Something to note about creating indicators, you need to name them “Indicator” before adding them to workspace for the connections to be created (it’s best practice anyways).

Let me know if you have any questions :+1:

1 Like

Thank you for your reply.
I’m using the Manual Indicator Replicator for streaming enabled, sorry for being unclear.

I also read Changing indicators through LocalScripts doesn’t work because the indicators aren’t in the same place on the client.

So I guess my only option to change via localscript is to turn streaming enabled off?

1 Like

Yeah, that system creates new indicators to represent the server sided ones and updates them when the server asks through remote events.

In that case you could use the second option, creating the indictors entirely on the client.

So, for example:

  • Quest starts on server
  • Replicate than info to the client (remote events, properties, etc.)
  • Create a new indicator with the attributes (on the client)
  • Remove the new indicator when the quest ends (on the client)
1 Like

How can i make it only visible when looking at it and when you look away it doesn’t show the arrow around the screen?

Thank you so much! This is exactly what I needed.