Roblox — Навигация

Автор: | 10 сентября, 2026
Поделиться...

В стандарте, для Роблокса, принято водить игрока за ручку, т.е. по стрелке.

Примитивная реализация этого — Beam из игрока к цели.

Но для RPG данный вариант не подходит. Нам нужна взрослая реализация, а именно по координатам и компасу. Что-то на подобии вот этого:

Для начала нам нужна сама координатная линейка типа такой:

Достаточной длинны, чтобы её можно было смещать в обе стороны относительно центра.

За нулевой вектор направления берём ось Z и направление на север.

И создаём такую структуру в StarterGui:

Чтобы у нас на экране получилось такое:

NavigateAdd это приклеенная копия Navigate (линейка) справа от неё. А у NavFrame мы выставили ClipDescendans в истину, чтобы не отображалось всё что выходит за рамки фрейма. Ну и для градиента UIGradient выставляем ZIndex побольше и параметр для Transparency:

для затемнения по краям.

Код же будет примитивен до безобразия:

-- Навигационный модуль

local workspace = game:GetService("Workspace")
local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer

-- компас
local nav = script.Parent.All.NavFrame.Navigate -- картинка с компасом
local pos = 0	-- текущее направление

-- позиция
local navPos = script.Parent.All.PosFrame.Coordinate	-- вывод квадранта
local kvadrant = 20	-- размер квадранта

while task.wait() do
	
	-- обслуживание компаса для навигации
	local look = camera.CFrame.LookVector
	local angle = math.deg(math.atan2(look.X, -look.Z))
	
	pos = angle / 360 + 0.5
	nav.Position = UDim2.new(pos, 0, 0.5, 0)
	
	-- обслуживание позиции персонажа
	local character = player.Character
	if character then
		local hrp = character:FindFirstChild("HumanoidRootPart")
		if hrp then
			local pos = hrp.Position
			local x = math.floor(pos.X / kvadrant)
			local z = math.floor(-pos.Z / kvadrant)	-- север с отрицательными значениями

			navPos.Text = x .. " x " .. z
		end
	end
end

Что касается квадрантов. Показывать позицию по каждому смещению как то не гоже и потому мы делим плоскость на квадратики. И вот уже эти квадратики и будут нашими координатами.

Ну и получаем нечто такое:

Первичная навигация готова. Работает на стороне клиента, да ещё и сама по себе.

Переходим ко второму шагу — добавление маркеров цели. Сразу скажу, что указателей на объекты на карте не нужно использовать — при некотором удалении объекты на стороне клиента не будут прогружены, хотя на сервере они и будут присутствовать. Поэтому используем обычные Vector3Value с координатами цели. А в качестве меток будут ImageLabel превращённые в квадраты с белыми иконками на прозрачном фоне.

Накидываем во фрейм компаса наши цели (с координатами того, что набросали на плейсе):

И меняем код (ага, тут я уже подключил ИИ, т.к. было лень расчётами заниматься):

--[[ Навигационный модуль
	Отображеине линейки компаса относительно направления камеры и отображение позиции игрока на карте

PosFrame - Отображение текущей координаты (квадранта)
NavFrame - Отображение компаса

TargetMarker - новый маркер для указания цели в нём и задаётся цвет маркера (первично белые) и сама иконки
	TargetPos - в нём указывает координаты цели на карте
]]

local workspace = game:GetService("Workspace")
local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer

-- компас
local navFrame = script.Parent.All.NavFrame
local nav = navFrame.Navigate -- картинка с компасом
local pos = 0	-- текущее направление

-- позиция
local navPos = script.Parent.All.PosFrame.Coordinate	-- вывод квадранта
local kvadrant = 30	-- размер квадранта

-- НАСТРОЙКИ ДЛЯ ЦЕЛЕЙ
local HIDE_ON_BORDER = false -- true = скрывать маркер на краю; false = задерживать (прилипать) к краю
local MIN_BORDER = 0.05      -- левая граница фрейма (в Scale), где маркер останавливается/исчезает
local MAX_BORDER = 0.95      -- правая граница фрейма (в Scale)

local MAX_DISTANCE = 100     -- расстояние (в studs), при котором маркер уменьшается до минимального размера
local MAX_SIZE_SCALE = 1.0   -- 100% размера, когда игрок находится вплотную к цели
local MIN_SIZE_SCALE = 0.5   -- 25% размера, когда игрок на расстоянии MAX_DISTANCE или дальше
local BASE_SIZE_Y = 0.8      -- базовый размер маркера по высоте относительно NavFrame (в Scale)

-- нам не нужны привязки к тикам или рендеренгу
while task.wait(.01) do

	-- обслуживание компаса для навигации
	local look = camera.CFrame.LookVector
	local cameraAngle = math.deg(math.atan2(look.X, -look.Z))
	--if cameraAngle < 0 then cameraAngle = cameraAngle + 360 end

	pos = cameraAngle / 360 + 0.5
	nav.Position = UDim2.new(pos, 0, 0.5, 0)

	-- обслуживание позиции персонажа и маркеров целей
	local character = player.Character
	if character then
		local hrp = character:FindFirstChild("HumanoidRootPart")
		if hrp then
			local playerPos = hrp.Position

			-- Координаты квадранта
			local x = math.floor((kvadrant/2 + playerPos.X) / kvadrant)
			local z = math.floor((kvadrant/2 + -playerPos.Z) / kvadrant)
			navPos.Text = x .. ", " .. z

			-- ОБРАБОТКА ВСЕХ ЦЕЛЕЙ С ИМЕНЕМ TargetMarker
			
			for _, child in ipairs(navFrame:GetChildren()) do
				if child.Name == "TargetMarker" and child:IsA("GuiObject") then
					local vectorValue = child:FindFirstChild("TargetPos")

					if vectorValue and vectorValue:IsA("Vector3Value") then
						local targetPos = vectorValue.Value

						-- 1. Считаем расстояние до цели и настраиваем размер (Scale)
						local distance = (targetPos - playerPos).Magnitude

						-- Рассчитываем множитель размера от 0.25 до 1.0 на основе дистанции
						local sizeClamp = math.clamp(1 - (distance / MAX_DISTANCE), 0, 1)
						local currentScale = MIN_SIZE_SCALE + (sizeClamp * (MAX_SIZE_SCALE - MIN_SIZE_SCALE))

						-- Применяем размер. За счет UIAspectRatioConstraint ширина подстроится сама
						local finalSizeY = BASE_SIZE_Y * currentScale
						child.Size = UDim2.new(finalSizeY, 0, finalSizeY, 0)
						child.ImageTransparency = BASE_SIZE_Y - finalSizeY

						-- 2. Считаем положение на компасе (Математика встроена напрямую)
						local direction = targetPos - playerPos
						local targetWorldAngle = math.deg(math.atan2(direction.X, -direction.Z))
						if targetWorldAngle < 0 then 
							targetWorldAngle = targetWorldAngle + 360 
						end

						-- Относительный угол между взглядом камеры и целью (-180 до 180)
						local relativeAngle = targetWorldAngle - cameraAngle
						if relativeAngle > 180 then
							relativeAngle = relativeAngle - 360
						elseif relativeAngle < -180 then
							relativeAngle = relativeAngle + 360
						end

						local markerScaleX = 0.5 + (relativeAngle / 360)

						-- 3. Логика поведения на границах (Флаг HIDE_ON_BORDER)
						if markerScaleX >= MIN_BORDER and markerScaleX <= MAX_BORDER then
							child.Visible = true
							child.Position = UDim2.new(markerScaleX, 0, 0.5, 0)
						else
							if HIDE_ON_BORDER then
								child.Visible = false
							else
								child.Visible = true
								local clampedX = math.clamp(markerScaleX, MIN_BORDER, MAX_BORDER)
								child.Position = UDim2.new(clampedX, 0, 0.5, 0)
							end
						end
					else
						child.Visible = false
					end
				end
			end
			
		end
	end
end

И по итогу получаем нужный результат:

Вот теперь всё по взрослому.

Ну и собственно, прилагаю сам плейс:


Поделиться...

Добавить комментарий