How to make a north indicator GUI?

I’m currently cooking up a minimap for a game I’m making. As of right now, I’m using a combination of a Part to designate where “North” is, and a conversion of that part using WorldToScreenPoint to allow myself to convert it to a degree. However it’s proven to be unreliable, I was wondering if there’s a different way I could achieve this effect?

1 Like

I don’t get what you mean by the way you’re doing it. Get the camera direction vector and take the atan2() of the X and Z part, this will give you an angle that works.

1 Like

I decided to make a similar compass to see if I can get it to work. Getting it to work properly in the case of positive depth was quite easy by using WorldToScreenPoint. Yours apprently doesn’t always work even in this case. The angle should be calculated using the direction from the compass center to the screen position of the part. Did you do this or something else?

I’m not sure what would be the most logical way to calculate the direction when the depth is negative. I tried a few different approaches. The first one was just calculating the direction from the compass to the point given by :WorldToScreenPoint(), just like in the case of points with positive depth. When I realised that this projects points on the left of the camera to the right side of the screen and points below the camera to the upper side of the screen, I decided to flip the direction such that points on the left of the camera are on the left side of the screen and points and also the vertical direction changes correspondingly.

I still wasn’t happy with the results. The further behind the part was, the closer the calculated screen position was to the center of the screen. This caused the compass to point towars the center when moving far from the part. Next, I decided to use ortographic projection for the points with negative depth. In ortographic projection the coordinates are not scaled based on depth. This, however, has the problem that when the depth changes from positive to negative (when the camera moves), there’s a sudden jump from really big screen coordinates (outside the screen) to much smaller screen coordinates. This is because, when the depth approaches zero, the coordinates calculated using prespective projection approach infinity, but the coordinates calculated with orthographic projection don’t get bigger.

The final approach I tried was considering the screen coordinates of the part infinite when the depth is negative. In perspective projection, the direction of the projected point from the center of the screen is its direction from the camera in the x and y axes (left-right axis and vertical axis) of the camera. Thus, I decided that the screen position vector of the part is considered to be an infinitely long vector with this direction. The compass needle direction would then be calculated by subtracting the compass screen position (finite) from this screen position (infinite). However, when subtracting a finite vector from an infinite one, the finite one has practically no effect on the result. Thus, we can consider the compass direction to be the direction of this infinite vector. And said direction is the direction of the (x, y) position vector of the part in the coordinate system of the camera.

There may very well be better, more logical approaches that I just didn’t think of.

Here’s the code. The final approach is the one used in this code. You may need to edit the angle conversion based on where the north pointer is pointing when its Rotation property is 0. Mine is pointing to the right (positive screen x-axis direction).

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

local compassOffsetFromScreenEdge = 25
local compassDiameter = 75
local partPos = Vector3.new(50, 20, -75)

local screenGui
local mainFrame
local compassPointer
local partPosFrame

local part

local function createCompass()
	screenGui = Instance.new("ScreenGui")
	screenGui.Name = "CompassScreenGui"
	screenGui.ResetOnSpawn = false
	
	mainFrame = Instance.new("Frame")
	mainFrame.Name = "MainFrame"
	mainFrame.AnchorPoint = Vector2.new(1, 1)
	mainFrame.Position = UDim2.new(1, -compassOffsetFromScreenEdge, 1, -compassOffsetFromScreenEdge)
	mainFrame.Size = UDim2.fromOffset(compassDiameter, compassDiameter)
	
	local mainFrameUiCorner = Instance.new("UICorner")
	mainFrameUiCorner.CornerRadius = UDim.new(.5, 0)
	mainFrameUiCorner.Parent = mainFrame
	
	local compassMiddle = Instance.new("Frame")
	compassMiddle.Name = "CompassMiddle"
	compassMiddle.BackgroundColor3 = Color3.new(1, 1, 1)
	compassMiddle.Size = UDim2.fromOffset(10, 10)
	compassMiddle.ZIndex = 2
	compassMiddle.AnchorPoint = Vector2.new(.5, .5)
	compassMiddle.Position = UDim2.fromScale(.5, .5)
	
	local compassMiddleUICorner = Instance.new("UICorner")
	compassMiddleUICorner.CornerRadius = UDim.new(.5, 0)
	compassMiddleUICorner.Parent = compassMiddle
	
	compassMiddle.Parent = mainFrame
	
	compassPointer = Instance.new("Frame")
	compassPointer.Name = "CompassPointer"
	compassPointer.BackgroundColor3 = Color3.new(1, 0, 0)
	compassPointer.BorderSizePixel = 0
	compassPointer.Size = UDim2.new(.5, 0, 0, 5)
	compassPointer.Position = UDim2.fromScale(.75, .5)
	compassPointer.AnchorPoint = Vector2.new(.5, .5)
	compassPointer.Parent = mainFrame
	
	partPosFrame = Instance.new("Frame")
	partPosFrame.BackgroundColor3 = Color3.new(1, 0, 0)
	partPosFrame.Size = UDim2.fromOffset(5, 5)
	partPosFrame.AnchorPoint = Vector2.new(.5, .5)
	partPosFrame.Parent = screenGui
	
	mainFrame.Parent = screenGui
	
	screenGui.Parent = Players.LocalPlayer.PlayerGui
end

local function getCompassDirection()
	local camera = workspace.CurrentCamera
	local partPosInCameraSpace = camera.CFrame:Inverse() * part.Position
	if partPosInCameraSpace.Z < 0 then
		local screenPointWithDepth = camera:WorldToScreenPoint(part.Position)
		local screenPoint = Vector2.new(screenPointWithDepth.X, screenPointWithDepth.Y)
		
		partPosFrame.Visible = true
		partPosFrame.Position = UDim2.fromOffset(screenPoint.X, screenPoint.Y)
		
		local mainFrameCenterPosition = mainFrame.AbsolutePosition + Vector2.new(mainFrame.Size.X.Offset, mainFrame.Size.Y.Offset) * .5
		return (screenPoint - mainFrameCenterPosition).Unit
	end
	
	partPosFrame.Visible = false
	
	return Vector2.new(partPosInCameraSpace.X, -partPosInCameraSpace.Y).Unit
end

local function updateCompass()
	local screenDirectionFromCompassCenter = getCompassDirection()
	
	compassPointer.Position = UDim2.fromScale(.5 + screenDirectionFromCompassCenter.X * .25, .5 + screenDirectionFromCompassCenter.Y * .25)
	local angleInDegrees = math.atan2(screenDirectionFromCompassCenter.Y, screenDirectionFromCompassCenter.X) * 180 / math.pi
	compassPointer.Rotation = angleInDegrees
end

local function createPart()
	part = Instance.new("Part")
	part.Name = "CompassTestPart"
	part.Anchored = true
	part.CanCollide = false
	part.Position = partPos
	part.Parent = workspace
end

createPart()
createCompass()
RunService.RenderStepped:Connect(updateCompass)

Edit: I had named the angle variable angleInRadians although it was the angle in degrees.

The end result is far better than what I was trying before (which was using atan2)

Old Code:

r_s.Heartbeat:Connect(function()
	
	local vector, onscreen = camera:WorldToScreenPoint(workspace.NORTH_NODE.Position)
	local screenpoint = Vector2.new(vector.X, vector.Y)
	
	script.Parent.Rotation = math.deg(math.atan2(screenpoint.Y - script.Parent.AbsolutePosition.Y, screenpoint.X - script.Parent.AbsolutePosition.X)) + 90
	
end)

The old code provided some jank, so thank you for assisting with this.

End Result of new Code:

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.