A Simple Linear Compass to Encompass All of Your Directional Desires!

,

Return

Greetings everyone!

Today is National Hug A Newsperson Day to show appreciation for our fellow news reporters! However, it’s probably not the smartest thing to do during these times (seriously, stay in your house and certainly keep your distance). From here on, in most of the topics I will create, I’ll be referring to one of these “National Days” just as a fun little thing (there are some totally absurd and comical ones)!

Anyways, without further ado, let’s dive straight into this tutorial. Today, you will learn to create a simple, linear compass using a scrolling frame. The good thing about this is that all of the code is inside of one script and within ~120 lines of code (that’s actually quite less for something this grand).

Here is the end product:


Sorry, I wasn’t able to get a GIF or a video because it was laggy for some reason. But essentially, this would scroll depending on your character’s orientation.

You may have seen something like this in exploration games or something similar.


Objects

The positions of these objects do not matter yet, you can set them to whatever you would like.

  1. Inside a ScreenGui, have a ScrollingFrame inside a Frame.
  2. Add an ImageLabel (“Arrow”) and center it horizontally in the Frame. First, set its AnchorPoint to 0.5, 0 and then set its Position to {0.5, 0, 0, 0}.
  3. Inside the ScrollingFrame, add a LocalScript (“Setup”).
  4. Also inside the ScrollingFrame, add 11 TextLabels with the names above. We will be scripting their positions later on. To center them horizontally and vertically, set their AnchorPoint to 0.5, 0.5.
  5. [NOT IN THE IMAGE ABOVE] Add a Part into Workspace and call it “CameraPart”. This part’s CFrame will always be set to the Camera’s CFrame. I did this because this method avoids complex CFrame math to convert it to orientation. Make sure you turn off CanCollide and make it completely transparent!

Why do we need 3 duplicate TextLabels? Well, it is to smoothly transition from the end of the ScrollingFrame to the beginning without the player noticing!

Here are the duplicate TextLabels selected, the selection is visible:

I moved a little to the right, the TextLabels are still selected, but they are not visible since they’re at the end now. These are the originals:


Barely even noticed, right?


Scripting

Open up the LocalScript (“Setup”) and start off by declaring these variables:

local frame = script.Parent --ScrollingFrame
--multiple variables per line to save space!
local n, e, s, w, ne, nw, se, sw, nw2, n2, ne2 = frame.N, frame.E, frame.S, frame.W, frame.NE, frame.NW, frame.SE, frame.SW, frame.NW2, frame.N2, frame.NE2 --TextLabels
local directions = {nw, n, ne, e, se, s, sw, w, nw2, n2, ne2}
local camera, cameraPart = workspace.CurrentCamera, workspace.CameraPart --Camera and CameraPart
local absoluteSize, canvasSize, Inc = 0, 0, 0 

absoluteSize and canvasSize are reffering to the corresponding properties of the ScrollingFrame. Inc is how much to scroll in the ScrollingFrame per 1 degree rotation.

Here is a function that will always keep CameraPart at the exact position and rotation of the Camera:

local function partToCamera()
    cameraPart.CFrame = camera.CFrame
end

We will be binding all functions to events at the end, so hang tight!

This function below will create tick marks at the specified X position (in pixels) and thickness:

local function tickMarks(position, thickness)
	local mark = Instance.new("Frame")
	mark.AnchorPoint = Vector2.new(0.5, 0)
	mark.Position = UDim2.new(0, position, 0, 0)
	mark.BorderSizePixel = 0
	mark.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	mark.Name = "TickMark"
        
	
	if thickness == "thicker" then mark.Size = UDim2.new(0, 5, 0.15, 0) end
	if thickness == "thick" then mark.Size = UDim2.new(0, 3, 0.3, 0) end
	if thickness == "thin" then mark.Size = UDim2.new(0, 1, 0.35, 0) end

	mark.Parent = frame
	return mark
end

You can play around with the properties if you would like, but make sure you rename your tick marks to something because we will need that later on! By the way, notice how I did the if-then in a single line. That’s a good way to keep your code from getting too long (but make sure it says in horizontally though or else there’s no point to it).

This simple function will remove all tick marks made. We only need to do this when the size of the ScrollingFrame changes (so that it will stay aligned properly, you’ll understand more later on).

local function removeTickMarks()
	for i, v in pairs(frame:GetChildren()) do
		if v.Name == "TickMark" then v:Destroy() end
	end	
end

This will now actually create the tick marks using the tickMarks() function from before:

local function updateTickMarks()
	for i, v in pairs(frame:GetChildren()) do
		if v:IsA("TextLabel") then
			local pxPos = v.Position.X.Offset
			
			if #v.Text == 1 then tickMarks(pxPos, "thicker") end --i.e. "N", "W"
			if #v.Text == 2 then tickMarks(pxPos, "thick") end -- i.e. "NE", "SW"
		end
	end
       --"sub" tick marks (in between the TextLabels); the numbers are in degrees. 22.5 is between 0 and 45, and etc. with the others
	for j = 22.5, 427.5, 45 do tickMarks(j * Inc, "thin") end
		
	--[[You can add in more "sub" tick marks if you want, but don't get to crazy since your game can get a little laggy due to 
	 due to an excessive number of frames!
		
        example:
	for k = 11.25, 416.25, 45 do tickMarks(k * Inc, "thin") end
	for l = 33.75, 438.75, 45 do tickMarks(l * Inc, "thin") end]]
end

Alright, this is the function that needs a little more explanation:

local function positionElements()
	absoluteSize = fr.AbsoluteSize.X

    --x5 to hold the duplicate TextLabels, another whole "direction" of them
	canvasSize = absoluteSize * 5
	
   --[[4 main directions, and 360 degrees
    the main directions ("N", "E", "S", "W") are going to be in the vertical
    middle of the Frame]]
	Inc = (absoluteSize * 4) / 360 
	
	for i, dir in ipairs(directions) do			
		dir.Position = UDim2.new(0, 45 * (i - 1) * Inc, 0.5, 0)       -- 0, 45, 90, ... canvasSize
	end
	
	removeTickMarks()
	updateTickMarks()
	
	frame.CanvasSize = UDim2.new(0, canvasSize, 0, 0)
end

Basically, this positions the TextLabels with the directions on them. Notice, the three variables absoluteSize, canvasSize, and Inc are being used now. The positioning of the TextLabels are based on 45-degree increments, each multiplied by Inc to give them a proper position in the ScrollingFrame. This is so that if your ScrollingFrame is bigger than 360 px, then it can still adjust accordingly. Also, as I said above, the 3 duplicate TextLabels are so that when the ScrollingFrame scrolls all the way to the end and then jumps back to the beginning, the duplicates and the originals are going to be in the same position. Then, afterwards, they smoothly move with the player’s orientation. This way, the player doesn’t see this little “cheat”! Also, we called the removeTickMarks() and updateTickMarks() functions to line them up with the new positions of the TextLabels (which will update when the AbsolutePosition of the ScrollingFrame changes).

Ok, phew that was a lot. Let us all take a deep breath, shall we? Alright, one final function. Just one more, come on, you’ll make it through!

local function moveWithOrientation()
	local orientationY = cameraPart.Orientation.Y
	local deg = 0
	
	if orientationY < 0 then deg = 180 + (180 + orientationY) else deg = orientationY end
	
    --[[Invert the degrees, this is because we need to make the degrees count up
    clockwise, which, by default is counter-clockwise. This way, it will match
    the X scroll direction of the ScrollingFrame.]]
	deg = 360 - deg
	
	local inc = (absoluteSize * 4) / 360
		
	frame.CanvasPosition = Vector2.new(deg * inc, 0)
	positionElements()
end

The reason why we did the if-then statement is because we need to turn the orientation to degrees. If you’re familiar with it, orientation can be negative. For example, -2 Y orientation is actually 358 degrees in reality, so the statement above converts it to that.

Now, the ending where we bind up them functions with events!

moveWithOrientation()
partToCamera()

cameraPart:GetPropertyChangedSignal("Orientation"):Connect(moveWithOrientation)
fr:GetPropertyChangedSignal("AbsoluteSize"):Connect(positionElements)
camera:GetPropertyChangedSignal("CFrame"):Connect(partToCamera)

Note: we need to call the functions initially just to make sure everything’s setup even when something didn’t change. But, we don’t need to call all the functions because they will be called inside of the functions we just called here, so they will also run (i.e. positionElements() will run when moveWithOrientation() is called).

That’s it, we’re all done now. Congratulations, you made it! That wasn’t so bad, right?


Full Script

Expand to view the entire script (note: some comments may not be there since I wrote them in directly here with the topic):
local frame = script.Parent
local n, e, s, w, ne, nw, se, sw, nw2, n2, ne2 = frame.N, frame.E, frame.S, frame.W, frame.NE, frame.NW, frame.SE, frame.SW, frame.NW2, frame.N2, frame.NE2
local directions = {nw, n, ne, e, se, s, sw, w, nw2, n2, ne2}
local camera, cameraPart = workspace.CurrentCamera, workspace.CameraPart
local absoluteSize, canvasSize, Inc = 0, 0, 0

local function partToCamera() 
	cameraPart.CFrame = camera.CFrame 
end

local function tickMarks(position, thickness)	
	local mark = Instance.new("Frame")
	mark.AnchorPoint = Vector2.new(0.5, 0)
	mark.Position = UDim2.new(0, position, 0, 0)
	mark.BorderSizePixel = 0
	mark.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	mark.Name = "TickMark"
	mark.Parent = frame

	if thickness == "thicker" then mark.Size = UDim2.new(0, 3, 0.15, 0) end
	if thickness == "thick" then mark.Size = UDim2.new(0, 2, 0.3, 0) end
	if thickness == "thin" then mark.Size = UDim2.new(0, 1, 0.35, 0) end
		
	return mark	
end

local function removeTickMarks()
	for i, v in pairs(frame:GetChildren()) do		
		if v.Name == "TickMark" then v:Destroy() end		
	end		
end

local function updateTickMarks()
	for i, v in pairs(frame:GetChildren()) do
		if v:IsA("TextLabel") then
			local pxPos = v.Position.X.Offset
			
			if #v.Text == 1 then tickMarks(pxPos, "thicker") end
			if #v.Text == 2 then tickMarks(pxPos, "thick") end	
		end
		
	end
	
	for j = 22.5, 427.5, 45 do tickMarks(j * Inc, "thin") end
	
	--[[You can add in more "sub" tick marks if you want, but don't get to crazy since your game can get a little laggy due to 
		 due to an excessive number of frames!]]
		
	for k = 11.25, 416.25, 45 do tickMarks(k * Inc, "thin") end
	for l = 33.75, 438.75, 45 do tickMarks(l * Inc, "thin") end
end

local function positionElements()
	absoluteSize = frame.AbsoluteSize.X
	canvasSize = absoluteSize * 5
	
	Inc = (absoluteSize * 4) / 360
	
	for i, dir in ipairs(directions) do			-- 0, 45, 90, ... canvasSize
		dir.Position = UDim2.new(0, 45 * (i - 1) * Inc, 0.5, 0)
	end
	
	removeTickMarks()
	updateTickMarks()
	
	frame.CanvasSize = UDim2.new(0, canvasSize, 0, 0)
end

local function moveWithOrientation()
	local orientationY = cameraPart.Orientation.Y
	local inc = (absoluteSize * 4) / 360
	local deg = 0
		
	if orientationY < 0 then 
		deg = 180 + (180 + orientationY) 
	else 
		deg = orientationY 
	end
	
	deg = 360 - deg
	frame.CanvasPosition = Vector2.new(deg * inc, 0)
	positionElements()
end

moveWithOrientation()
partToCamera()

cameraPart:GetPropertyChangedSignal("Orientation"):Connect(moveWithOrientation)
frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(positionElements)
camera:GetPropertyChangedSignal("CFrame"):Connect(partToCamera)

Place File

Compass.rbxl (38.6 KB)


Feedback

That concludes this tutorial. Hopefully, I explained everything well (please tell me if I was unclear about something). But, as always, roll them polls in!

Rate this tutorial overall (understanding, usefulness to you, etc.).

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

Did you learn anything new?

  • Yes
  • No

0 voters

If you have any comments or concerns, feel free to reply or message me,
and have a good one!


Topic Edits

EDIT Apr 5, 2020: If you’re asking how I chose the directions in their current orientations, then remember, the sun rises in the east. After playing around with the sun, you can easily figure that out. Then, you’ll find all the other directions.

EDIT Apr 11, 2020: I just tweaked some wording here and there and fixed my clumsy grammar mistakes. Also, the date of the edit above is now April instead of March (another oopsie by me). But, the overall meaning is still unchanged!

EDIT Aug 6, 2021: Updated full script and given place file to include a more optimized and readable script. I also changed corresponding parts of the tutorial to reflect the changes in the script. In all, the script reduced to around 92 lines.

76 Likes

Golly, this might help me and other developers with a game involving a compass, nice job! :+1:

1 Like

your a goddamn legend, you made a damn compass inside of roblox. couldn’t even know where south or west was irl and you made a whole compass in roblox. legend.

3 Likes

Is it possible to size it down and move it to the corner above, per say, a minimap?

1 Like

Yes, absolutely! All you have to do is size down and position the Frame (that encompasses everything) and it will also perform those operations on everything else. Make sure to adjust the size of the TextLabel if you used offset (like I did).

1 Like

How about marking something on this compass?
If I want to show a certain point and that it is always in the right direction relative to the player, even when moving?

Man, this is amazing! It so smooth and it act like the one in Call of Duty! Thank you so much, and I will credit you as always! :smiley:

1 Like

If you change the camera view, sometimes it keeps sending multiple signals even if is in the same camera orientation. Any idea to solve this problem?

how about landmarks? maybe do some collision like touching the landmark zone make the landmarkui invisible and then leaving it would make it visible again