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.
- Inside a ScreenGui, have a ScrollingFrame inside a Frame.
- 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}.
- Inside the ScrollingFrame, add a LocalScript (“Setup”).
- 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.
- [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.