This is my second post, let me know if there’s anything I have to improve in redaction or general topic arrangement/readibility.
Following my previous post, I would like to share my “FOV Controller” (I honestly don’t know how to name it) as a free community resource.
I’m not really using it, so why not? It could maybe be useful for anyone that was looking for something similar just like I used to do in the past.
Just like the title says, it maintains consistent FOV across different screen aspect ratios, it’s commonly seen in competitive games like Counter Strike and Valorant, where you don’t want widescreen players to have an advantage in comparison to players with narrower screens.
Demonstration of how different aspect ratios change FOV
Here is a video demonstration of how it works:
(If you wish to see it for yourself first, here is a test place for it.)
Let me know if there’s any bug or anything!
ModuleScript
--[[
ReplicatedStorage/FOVController (ModuleScript)
Mantains consistent FOV across different screen aspect ratios.
Gives the same horizontal and vertical FOV visibility regardless of the screen size.
Author: @soydegurime (https://www.roblox.com/es/users/3919560656)
How it works:
Adjusts the camera's field of view and matrix for FOV decoupling transformation based on screen aspect ratio relative to the desired ratio.
]]
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local FOVController = {}
-- Configuration constants
FOVController.TARGET_HORIZONTAL_FOV = 103
FOVController.TARGET_VERTICAL_FOV = 71
FOVController.TARGET_ASPECT_RATIO = 16 / 9
--[[
Adjusts the camera's field of view and transformation based on the calculated squash value.
@param squash (number) - The amount to adjust the FOV (values > 1 squash horizontally, values < 1 squash vertically)
@param camera (Camera) - The camera to adjust (defaults to workspace.CurrentCamera)
]]
function FOVController:DecoupleFOV(squash, camera)
local currentCamera = camera or workspace.CurrentCamera
if not currentCamera then
error("FOVController: Cannot adjust FOV - no camera available")
return
end
local cf = currentCamera.CFrame
if squash > 1 then
-- Wide screen: adjust horizontal FOV
currentCamera.CFrame = CFrame.fromMatrix(cf.Position, cf.XVector / squash, cf.YVector, cf.ZVector)
currentCamera.FieldOfViewMode = Enum.FieldOfViewMode.Vertical
currentCamera.FieldOfView = self.TARGET_VERTICAL_FOV
elseif squash < 1 then
-- Tall screen: adjust vertical FOV
currentCamera.CFrame = CFrame.fromMatrix(cf.Position, cf.XVector, cf.YVector * squash, cf.ZVector)
currentCamera.FieldOfViewMode = Enum.FieldOfViewMode.MaxAxis
currentCamera.MaxAxisFieldOfView = self.TARGET_HORIZONTAL_FOV
end
end
--[[
Calculates the required squash value based on the current viewport dimensions.
@param viewportSize (Vector2) - The dimensions of the viewport
@return (number) - The calculated squash value
]]
function FOVController:CalculateSquashValue(viewportSize)
local currentAspectRatio = viewportSize.X / viewportSize.Y
return self.TARGET_ASPECT_RATIO / currentAspectRatio
end
--[[
Updates the FOV based on the current viewport size.
This is called on every render frame.
]]
function FOVController:UpdateFOV()
local player = Players.LocalPlayer
if not player then return end
local camera = workspace.CurrentCamera
if not camera then return end
local viewportSize = camera.ViewportSize
local squashValue = self:CalculateSquashValue(viewportSize)
self:DecoupleFOV(squashValue, camera)
end
--[[
Initializes the FOV controller by binding to the RenderStep event.
This should be called once when your game starts.
]]
function FOVController:Init()
-- Bind with a priority slightly higher than camera to ensure it runs after camera updates
RunService:BindToRenderStep(
"FOVController",
Enum.RenderPriority.Camera.Value + 1,
function() self:UpdateFOV() end
)
print("FOVController initialized successfully")
end
--[[
Stops the FOV controller by unbinding from the RenderStep event.
Call this when you want to disable the FOV controller.
]]
function FOVController:Cleanup()
RunService:UnbindFromRenderStep("FOVController")
print("FOVController cleaned up")
end
return FOVController
LocalScript
--[[
FOVControllerClient.lua
Client script that initializes the FOV Controller.
Place this in StarterPlayerScripts to automatically enable the controller for all players.
]]
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local FOVController = require(ReplicatedStorage.FOVController)
-- Initialize the FOV Controller
FOVController:Init()
Instructions
- Create a ModuleScript in ReplicatedStorage named “FOVController”
- Copy the contents of the ModuleScript code
- Create a LocalScript in StarterPlayerScripts
- Copy the contents of the client code provided above