UI Layout With 3D Space: How to keep multiple parts in view?

I’m struggling to create a vision I have which can be seen from my crude drawing below:

  1. The light blue square represents the gameplay area (3D space)
  2. The gray square represents the “board” where the gameplay occurs
  3. The black boarder surrounding the gameplay area displays GUI elements (HUD)
  4. Parts A, B, C, and D are placed in 3D space between the GUI boarder and the “board” in the gameplay area

I am able to achieve 1, 2 and 3 by fixing the camera and focusing on the center of the “board”, using frames to hold GUI elements for the HUD. My main question is about point 4:

How can I ensure that the player will always see blocks A, B, C and D in the gameplay area?

With only GUI elements I can control how much of the screen they take up by scaling their size and position, but I’m struggling to figure out how this can work alongside the 3D space

I stumbled upon the following post: Forcing a certain aspect ratio for the camera on all clients which seems to be able to lock the player’s field of view in the horizontal or vertical directions, however if applied to what I’m trying to do, this will ensure the player will always see either parts A and D (locking vertical) or parts B and C (locking horizontal), not both.

I also looked into this ViewportRender tool: ViewportRender - A tool for fast and performant Viewport rendering to try using a ViewportFrame as the gameplay area however when adjusting the size of my screen I ran into the same issue that happened with the above forced aspect ratio solution.

I can achieve something close to this by adjusting the zoom of the fixed camera, however I’d like my game to be responsive to all screen sizes which requires parts A, B, C and D to always be displayed to the player.

How would I go about achieving this?

2 Likes

I’m assuming you want to change the Position and The Orientation of the players camera. You may just use “game.Workspace.CurrentCamera.CFrame” and “game.Workspace.CurrentCamera.LookVector”

You only need to know the Camera.FieldOfView, and the play area (light blue) size in studs. You can then calculate the minimum camera height to include all of the play area on the screen (or inside the GUI cutout). This is the best method assuming you want a fixed FoV and you already have the GUI done with a set cutout (or can calculate that beforehand).

tan(α) = (x/2)/h so height = math.max(X, Z)*2/math.tan(fov)
(In reality the value falls just a bit short, but that won’t matter in the following)

You can now add a variable to the FoV to set the relative size of your play area on the screen:

height = math.max(X, Z)*2/math.tan(fov*v) --v=1: occupies the entire screen, v=0.5: half the screen

Now assuming you know the GUI cutout (or the top-bottom margin) you can set that variable perfectly:

v = (cutout / cam.ViewportSize.Y)*.9 --the 0.9 is optional
--Assuming you know the GUI sizes only:
v = (1 - (top.AbsoluteSize.Y + bottom.AbsoluteSize.Y) / cam.ViewportSize.Y)*.9

Example final script (works in a blank Baseplate place):

local cam = workspace.CurrentCamera
local part = workspace.Baseplate
local v = .95
game:GetService("RunService").RenderStepped:Connect(function()
	local a = math.rad(cam.FieldOfView)
	local x = math.max(part.Size.X, part.Size.Z)
	local h = (x*2/math.tan(a*v))
	cam.CFrame = CFrame.new(Vector3.new(0, h, 0), part.Position)
end)
2 Likes

Thanks for your answer emojipasta, however this solution only locks in the vertical FoV, not the horizontal FoV. I used your final script in a blank Baseplate and added red parts on the edges of the baseplate to make my problem more clear.

Here is what the result is with “normal” screen dimensions:


Looks great, all red parts are in view.

Here is what the result is as I stretch the screen wider:


Still looks good, the baseplate became smaller to keep the red parts on top and bottom in view

Here is what the result is as I stretch the screen taller:


This is where the issue lies with your proposed solution; the red parts on the left and right of the baseplate are no longer in view. I would like the baseplate to scale in the same way it does when I stretch the screen wider so that all red parts are in the user’s FoV.

local cam = workspace.CurrentCamera
local part = workspace.Baseplate
local v = .95
game:GetService("RunService").RenderStepped:Connect(function()
    local vx, vy = cam.ViewportSize.X, cam.ViewportSize.Y
    if vy > vx then
        v = .95*vx/vy
    end
    local a = math.rad(cam.FieldOfView)
    local x = math.max(part.Size.X, part.Size.Z)
    local h = (x*2/math.tan(a*v))
    cam.CFrame = CFrame.new(Vector3.new(0, h, 0), part.Position)
end)

This solution is pretty naive but it works perfectly for me (as soon as you switch to vertical resolutions the margin stays the same, but is on the left-right edges of the viewport).

On the other hand I am wondering why this rare use case is required. I believe the only reason you should implement this is if you want to support portait mode mobile users. Either way, above solution works fine.

5 Likes

Thanks emojipasta. This is definitely a rare use case; I’m trying to avoid a situation where a desktop player’s client opens and the board or GUI is cutting off some of the board. But seeing this solution in action I think the player would end up stretching the screen wider regardless. Is there something more common used to solve this unrelated to the topic?

I’m marking your post as the solution although for the sake of resolving this topic further I noticed some weird behavior with your response:

  1. The transition as you change the ViewportSize.X is a bit choppier / laggier than when you change the ViewportSize.Y (may be related to how you said this solution is naive)
  2. For some reason players on the board will suddenly fall and die when changing ViewportSize.X:

Thanks again for your help!

As for your question, GUIs are usually not built with the intention of taking up as much screen space as possible (which is what your GUI will do essentially) but the opposite. You have a few buttons on the side and menus are always pop-ups in the middle of your screen. If a user has an awkward screen size, clever positioning of the UI elements will still make them able to use all buttons because there are not that many elements to juggle around in the first place. For inspiration you can just open an empty place, open the player list, backpack and chat, and see how they react to screen size changes.

  1. Think of the use case again. How often are people going to actually resize their screen, and if so, would they really notice that it’s choppy and be dissatisfied? Either way, from an objective standpoint, I assume it’s choppy because of the way Roblox itself handles screen resizing, not the script changing the camera.

  2. I’ve noticed this while testing and thought it was just a weird bug on my side. When I restarted studio to help you with this again indeed it seemed very, very strange since only the camera is being moved. Why would that influence the character at all? After testing every line in the script, turns out using the CFrame.new(Vector3, Vector3) constructor caused the issue. Simply defining the origin by the height and rotating the camera down completely solves this issue.

cam.CFrame = CFrame.new(0, h, 0) * CFrame.Angles(math.pi/-2, 0, 0)
2 Likes

Having a few buttons on the side with menus popping up in the middle makes sense for say a shooting game or many other games on Roblox. My game has a simple layout with a board, the best way I can describe this is like a virtual chess game:

Regardless of the size of my browser, I can always see the entire board. This proved difficult to do with the 3D space but your solution has helped.

  1. What you were hinting at is correct. With the chess game above as I was resizing my browser the board was choppy but I didn’t mind. The user isn’t going to constantly be readjusting their screen so I shouldn’t care so much about this.
  2. That resolved the player dying, thanks!