How to Make a Spectate!

Hey everyone! This is gonna be 2nd tutorial of mine (you can check out the first one here), this time, talking about how to create your own spectate! In here, I’m going to be talking about how to make a UI for such, and how to script it, while going over how it works. Let’s dig into it!

July 2023: Thank you for all the support the past few years, whether through your appreciation in the thread, or personally inquiring me about this tutorial. After looking back, I’ve refactored some of the code a bit to make it a bit simpler! I’ve got a few more tutorials in the works, too, so stay tuned!

Part I: Making the UI!

  • I like to start off with making the UI. For this tutorial, I’m thinking of a simplistic-style UI.
  • So, I’m going to insert a ScreenGui and a Frame inside it.
  • I want my frame to be a square shape, so I’ll insert a UIAspectRatioConstraint, and set the AspectRatio to 1, to get the square. I’ll also resize the frame using scaling, to make sure it appears the same on all devices!
How it looks so far!


My Position: {0.013, 0},{0.522, 0}
My Sizing: {0.057, 0},{0.095, 0}

Now, I want a rounded square for this, and there are two ways we can go about this:

  • Roblox has the UICorner element that you can parent to the frame, and adjust it’s rounding with the properties. I suggest playing around to see what you like best!
  • If you’re wanting to look elsewhere for options, you can also use the amazing Roundify plug-in (credits to @Stelrex!), and round the frame. As such, I’m going to set my BorderPixel to 0 before using the plug-in. When using it, I’ll set the Size to 8.
Visual Example

  • I’m going to be adding an ImageButton into the Frame. I’ll set its anchor point to 0.5, 0.5, and it’s position to {0.5,0},{0.5,0}, so we can center the button. I’ll size it to {0.8,0},{0.8,0}. I’ll set BackgroundTransparency to 1, and for the image, I’ll be uploading a free stock image from the website Canva. Feel free to use whatever image!
See the button!

  • Now, I also want to make the part so we can show the player, so I’ll make a frame, using scale positioning and sizing (parented to the ScreenGui). I’ll name this “SpectateFrame”. I’ll set it to be transparent (BackgroundTransparency = 1).
  • I’m going to add a TextLabel in the center, and 3 TextButtons. One for a “left” button, one for a “right” button, and one to stop spectating. Check it out below.
  • Our last thing is to set the ResetOnSpawn property of the ScreenGui to false, and Visible property on the SpectateFrame to false.
Check it out!

It’s key for the scripting portions you named these right, as we’re going to be referencing them!

Part II: Scripting!

  • We’re going to start off, and add a LocalScript, parented to our ScreenGui.
  • We’ll start off defining our objects with variables, which will make it easier to reference UI items.
-- Our local script!
local spectateFrame = script.Parent.SpectateFrame
local button = script.Parent.Frame.ImageButton
  • I want it so if we click the button, it’ll show the spectate stuff, but click it again, it’ll go away. We can use if statements to help out. If the spectate bar isn’t opened yet when I press the button, I’ll open up the spectateFrame. If it already is though, I’ll close it.
-- Our local script!
local spectateFrame = script.Parent.SpectateFrame
local button = script.Parent.Frame.ImageButton

button.MouseButton1Click:Connect(function()
     if spectateFrame.Visible == true then --> We're checking if the frame's already opened!
         spectateFrame.Visible = false
     else
          spectateFrame.Visible = true
     end
end)
  • Now, because we’ll be cycling through players to watch for the spectate, we’re gonna create a table of players, by using the :GetPlayers() function. When a player joins, they’ll be added. When they leave, they’ll be taken out. We’ll make a function for this.
-- Our local script!
local spectateFrame = script.Parent.SpectateFrame
local button = script.Parent.Frame.ImageButton
local playerList = game:GetService("Players"):GetPlayers()
local function updatePlayerList() --> For readability, we'll use playerList from here on out, instead of calling the :GetPlayers() function each time
     playerList = game:GetService("Players"):GetPlayers()
end

updatePlayerList() -- We'll update it right away, then when a player leaves/joins
game.Players.PlayerAdded:Connect(updatePlayerList)
game.Players.PlayerRemoving:Connect(updatePlayerList)


button.MouseButton1Click:Connect(function()
     if spectateFrame.Visible == true then --> We're checking if the frame's already opened!
         spectateFrame.Visible = false
     else
          spectateFrame.Visible = true
     end
end)
  • Now, we need to change the camera when the player spectates, and also cycle through the players. As such, we’re going to add 2 new variables, one to represent the camera, and another to know what “place” we’re in the table of players. We’ll set it to 1 by default. Another function will be made to change the camera to different players. We’ll also code in the LeftButton and RightButton now, so when clicked, they’ll cycle to the next player, and script the stop spectating button. As such, we’re also going to insert the updateCamera functions when spectate buttons are clicked, and when the spectate opens/closes.
-- Our local script!
local spectateFrame = script.Parent.SpectateFrame
local button = script.Parent.Frame.ImageButton
local playerList = game:GetService("Players"):GetPlayers()
local function updatePlayerList() --> For readability, we'll use playerList from here on out, instead of calling the :GetPlayers() function each time
     playerList = game:GetService("Players"):GetPlayers()
end

local function updateCamera(playerSubject) -- New function! This will change the camera view to whatever player is given for the function!
    pcall(function() -- To make sure it doesn't throw an error.
         spectateFrame.TextLabel.Text = tostring(playerSubject)
         cam.CameraSubject = playerSubject.Character
    end)
end


updatePlayerList() -- We'll update it right away, then when a player leaves/joins
game.Players.PlayerAdded:Connect(updatePlayerList)
game.Players.PlayerRemoving:Connect(updatePlayerList)


button.MouseButton1Click:Connect(function()
     if spectateFrame.Visible == true then --> We're checking if the frame's already opened!
         spectateFrame.Visible = false
     else
          spectateFrame.Visible = true
     end
end)


spectateFrame.LeftButton.MouseButton1Click:Connect(function()
    position = position - 1
    if position < 1 then
         position = #playerList -- Change position to last on the list
    end
    updateCamera(playerList[position]) -- Changes camera to person on the list
end)
spectateFrame.RightButton.MouseButton1Click:Connect(function()
    position = position + 1
    if position > #playerList then
         position = 1 -- Change position to last on the list
    end
    updateCamera(playerList[position]) -- Changes camera to person on the list
end)
spectateFrame.StopButton.MouseButton1Click:Connect(function()
     spectateFrame.Visible = false
     updateCamera(game.Players.LocalPlayer) -- Adding it to close so we can change back our camera!
end)
  • And just like that, we’re done! Let’s test it out, and see the end result!
End Result!

I hope you find this tutorial helpful, and I wish you good luck on your ventures! Have a good day, and see you soon! :grinning:

116 Likes

Very helpful, thanks for doing this. I tried to make my own but there was some problems but now it’s fine.

7 Likes

Hello, this worked but if the player I’m spectating dies the camera stays where the player was when he died. How would I fix this? Thanks for the tutorial!

1 Like

You could use a function that checks whenever the current spectating user dies. Something like this:

camera.CameraSubject:WaitForChild("Humanoid").Health.PropertyChangedSignal("Health"):Connect(function)
    if camera.CameraSubject.Humanoid.Health = 0 then
        position = position + 1
        if position > #playerList then
            position = 1 -- Change position to last on the list
        end
    end
end

I didn’t test it out, so if it doesn’t work just reply!

12 Likes

Thanks for your reply! I got it working with some code from this post:

In my game the players respawn after death, so changing the position wasn’t an option. Instead I am changing the CameraSubject after the character has respawned.

2 Likes

Good tutorial, also great explanations instead of just giving the code. Good job.

7 Likes

Also one thing I found is that if you set the CameraSubject to the player character, and then go into first person the camera doesn’t appear to be inside the head, but inside the HumanoidRootPart / chest of the character. This can be fixed by using player.Character:WaitForChild(“Humanoid”) as the subject.
As stated in the wiki humanoid is the default CameraSubject:

Humanoid : By default the CameraSubject is set to the Player/LocalPlayer|LocalPlayer's Humanoid . The camera scripts will follow the Humanoid factoring in the Humanoid's current state and Humanoid.CameraOffset

Camera | Documentation - Roblox Creator Hub.

3 Likes

Thank you for all your responses, and possible additions. When I have the time to edit this, I’ll be sure to credit you and your additions in!

2 Likes

Could you explain why you are making a PlayerList table rather than using GetPlayers?

1 Like

Sure.
For each time you choose to spectate another player, you’re cycling through the table of current players. I made a PlayerList table, so when someone leaves or joins, it’ll be updated and placed in a specific order.

From my understanding, if we were to call :GetPlayers(), the order of the players in the table returned can vary on each iteration of it, which can result in a couple minor issues, including:

  • Players pressing next or back, only to see the same player occassionally.
  • Specific players being shown again and again in the course of a few times of moving next or back on the spectate.

EDIT: After a bit of Studio testing, :GetPlayers() does return a specific order, and you can do that in your code. I just learned personally by using your own table (which, now I look back on it, was to help introduce the idea of tables).

As often in code, there are various ways to write code on a certain subject, and this would be one of them. I’ll be leaving the code as is, as there have been many who have already utilized it and may continue to be utilizing it, but I will be taking feedback into consideration for the next time I publish a tutorial.

Could there be better methods of writing this? Of course, as is any other application of programming. I went into this tutorial with the intended target of those who have started out a bit into spectates, as I anticipated those with more concepts and skills in programming would be able to “cobble up” a method for spectating. I understand there’s a fine line to walk with tutorials between those who are looking to mature to more skills, and those who are starting out and wanting to learn how certain features work in code.

I’m deeply sorry to those who this tutorial couldn’t meet standards too, and I will try better the next time around.

2 Likes

My Opinion:

I like this tutorial. But a lot can be changed, especially the formatting of this code. I think the purpose of giving us resources and tutorials is to teach us how to improve our own skills, and giving us the bare minimum of code practice expectations is not the best and can lead to beginner programmers (or any programmers) getting into bad practices/habits.

The idea is good, but you should be making this a lot more efficient. So just remember for next time that this might be influencing some other programmers. :smiley:

Thank you for responding back.

I thought of that issue too, where the players will not go in order, and I tested in Roblox too and the spectating works fine for me. But it’s very good that you are being really careful about the PlayerList, as you don’t want to mess up the order, because spectating in this way relies on the order of the players.

I think it’s very good to be careful in scenarios like this.

1 Like

I know you didn’t test it out, but realize that the CameraSubject can change, and it does change when you spectate other players.

CurrentCamera:GetPropertyChangedSignal("CameraSubject")

So it is best to check if the CameraSubject changed so simply when the character is spawned, you can change the CameraSubject.

Like to also note while roundify is amazing we now have UICorner a ROBLOX made corner remover.

2 Likes

Thanks for thay very well and helpful i can now make my own spectate gui!

where’s the first tutorial, I didnt join in the first place of this awesome guide :slight_smile:

What if I were to make an option to spectate parts and not players?

1 Like

You can set the CameraSubject to a specific part.

1 Like