Hey there! I’m attempting to create a minimap with a ViewportFrame, but I’m having some issues with multiple players showing up. As of now, the only player blip is the local player, not any others.
Currently how this is set up:
Player joins, new CFrameValue added to ReplicatedStorage
As the player moves, said CFrameValue updates to the player’s HumanoidRootPart position (excluding rotation)
LocalScript inside of the UI adds a new blip per player, and uses the CFrameValue to move the blip and roads accordingly.
Blips appear to move properly through their properties, although they don’t show up on the ViewportFrame at all.
As of now, this is the server script to change all of the values based on positions. This works completely fine through my testing, it’s just the ViewportFrame not updating properly.
game.Players.PlayerAdded:Connect(function(plr)
local PV = Instance.new("CFrameValue")
PV.Name = plr.Name
PV.Parent = game.ReplicatedStorage.MapPositions
plr.CharacterAdded:Connect(function(char)
local HRP = char:WaitForChild("HumanoidRootPart")
while HRP do
PV.Value = CFrame.new(HRP.Position.X, 768.2, HRP.Position.Z) * CFrame.Angles(0, math.rad(-90), math.rad(90))
wait()
end
end)
end)
Through a local script in the UI itself, this moves the blips properly.
for i, v in pairs(game.ReplicatedStorage.MapPositions:GetChildren()) do
local Val = v.Value
local Dot = Mark:FindFirstChild(v.Name)
if not Dot then
local PM = script.PlayerMarker:Clone()
PM.Name = v.Name
PM.Parent = Mark
Dot = PM
end
Dot:PivotTo(v.Value)
end
This is a video of the map in action working with all edge cases
This is the structure I am using inside StarterGui
This is the code to make it happen:
local RunService = game:GetService("RunService")
local PlayerService = game:GetService("Players")
script.Parent.Marker.ZIndex = math.huge
local world = {}
world.Size = Vector2.new(workspace.Baseplate.Size.X, workspace.Baseplate.Size.Z)
local minimap = {}
minimap.Frame = script.Parent.Minimap
minimap.Size = Vector2.new(minimap.Frame.AbsoluteSize.X, minimap.Frame.AbsoluteSize.Y)
minimap.MarkerTemp = script.Parent.Marker:Clone()
script.Parent.Marker:Destroy()
local function get_map_entries()
local entries = {}
for i, player in pairs(PlayerService:GetPlayers()) do
pcall(function()
local entry = {}
entry.Position = player.Character.PrimaryPart.Position
entry.Player = player
table.insert(entries, entry)
end)
end
return entries
end
local function world_to_map(position, marker) -- converts a worldposition VEC3 to a map pos UDIM2
local marker_offset = UDim2.new(0, marker.AbsoluteSize.X/2, 0, marker.AbsoluteSize.Y/2)
local conversion = Vector2.new(minimap.Size.X / world.Size.X, minimap.Size.Y / world.Size.Y)
local world_offset = UDim2.new(0, conversion.X * world.Size.X/2, 0, conversion.Y * world.Size.Y/2)
return UDim2.new(0, conversion.X * position.X, 0, conversion.Y * position.Z) - marker_offset + world_offset
end
local function render()
local map_entries = get_map_entries()
minimap.Frame:ClearAllChildren()
for i, entry in pairs(map_entries) do
local marker = minimap.MarkerTemp:Clone()
marker.Position = world_to_map(entry.Position, marker)
if entry.Player == PlayerService.LocalPlayer then
marker.BackgroundColor3 = Color3.fromRGB(220,50,50)
end
marker.Parent = minimap.Frame
end
end
RunService.Stepped:Connect(function()
render()
end)
This was just a prototype, you surely have to add your own background image to suit your map, you’ll also have to change the size of your world to whatever you want (make sure the aspect ratio of your world is the same as the one of your map, both should ideally be square!)
I realized I forgot to explain why I chose not to approach it your way:
Having the server keep track of all players, store their positions in values, and then access those values from local scripts feels unnecessarily cumbersome. In my opinion, it’s always better to aim for solutions that are concise yet coherent.
By handling everything locally, the system becomes completely independent, which makes it simpler and more efficient overall.
The only issue with this solution is the fact that I’m using a copied version of the current roads (makes things easier for map updates), not an image. I attempted to recreate this with how I have things set up, but to no avail.
It seems that ViewportFrames simply don’t update, unless I always have a camera being created & removed to update it.
For a couple of references, this is how I have the UI set up visually and structurally as of now.
You don’t create a camera inside the ViewportFrame, right? Instead, you place one in the workspace (for example, something like “MinimapCam”) and set the CurrentCamera property of the ViewportFrame to it.
From there, you modify the camera itself, not the ViewportFrame.
By the way, there are excellent resources available online to guide you:
local RunService = game:GetService("RunService")
local PlayerService = game:GetService("Players")
local localplayer = PlayerService.LocalPlayer
local camera = Instance.new("Camera")
local camera_offset = Vector3.new(0, 40, 0)
script.Parent.ViewportFrame.CurrentCamera = camera
RunService.RenderStepped:Connect(function()
pcall(function()
local plr_pos = localplayer.Character.HumanoidRootPart.Position
camera.CFrame = CFrame.lookAt(plr_pos + camera_offset, plr_pos)
end)
end)