Location Marker System

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

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