Dealing With Fullscreen In Unity

Games From Earth
7 min readJan 19, 2021

--

Fullscreen is easy, right? Unity handles it for you, and you can always just set Screen.fullScreen, right?

Well, yes and no. There are some weird quirks in Unity’s fullscreen implementation, at least on Mac. I haven’t verified whether these issues also exist on Windows, but the workarounds I came up with don’t break anything on Windows, so they’re safe to use (as far as I’m aware).

Unity has some issues with fullscreen

If you have any questions about this article or about Unity development, you can find me in my discord server : https://discord.gg/zeCKc8SnHs

The problem

Let’s start with a blank Unity project. I set it to 2D (but the issue applies to 3D as well), and set the camera to clear to a nice solid color.

To demonstrate the issue properly, I checked the “Resizable Window” setting in the player settings.

After building, our “game” looks like this, filling the screen nicely :

Initially, full screen works as expected

Now let’s exit fullscreen, and resize the window to something a bit more vertical :

Scaling the window to a different aspect ratio

Now if you enter fullscreen again, this happens :

Oh no! Unity is keeping the scaled aspect ratio in fullscreen mode!

Unity keeps the dimensions of the window, scales it up to fit, and letterboxes it. This clearly is a bit of an issue.

You might think that turning off “Resizable Window” would solve this issue. Well, only kind of. On Mac, you can still somewhat resize the window with the Zoom (Maximize) button, enough to make some letterboxing happen. And also, non-resizable windows aren’t great for the user.

It’s also important to note that this issue exists in both ‘Fullscreen Window’ and ‘Exclusive Fullscreen’ modes.

The solution

There may be a much simpler solution to this than what I’m presenting — if there is, please let me know. But I’ve seen a coupe of indie games that do not address this issue at all, so I figured it would be worthwhile to share my solution.

My solution works by detecting a change in fullscreen like so :

if (Screen.fullScreen != prevFullScreen) {
if(Screen.fullScreen) {
// We just entered fullscreen
} else {
// We just left fullscreen
}
prevFullScreen = Screen.fullScreen;
}

Entering fullscreen

When we enter fullscreen, we manually set the resolution to… some resolution. But which one?

If you have a settings screen where users can change the resolution, and they’ve previously selected a resolution, that would be a good choice. But only if that resolution is still present in the list of available resolutions (Screen.resolutions). After all, they may be using a different screen now.

But if they haven’t picked a resolution yet (ie it’s the first time they’re playing) — or the previously selected resolution isn’t available anymore, we need to pick some default value.

For these cases, a safe bet would be to pick the last resolution in the Screen.resolutions array. As far as I can tell, the last entry is usually either the currently selected system resolution, or the highest natively supported resolution supported by the screen+gpu combo.

But just for completeness, I want to present a couple of other options that I came across while researching how to pick a default resolution.

  • There’s Screen.currentResolution which gives you the OS’es resolution, but it’s allegedly only accurate while you’re in windowed mode. But you could start your game in windowed mode, grab this resolution and store it.
  • Another option is using Display.main.systemWidth and Display.main.systemHeight. An issue with these is that for “retina” displays, they give you half of the actual physical resolution. One thing you could do is checking if (Display.main.systemWidth * 2, Display.main.systemHeight * 2) is a supported resolution in Screen.resolutions. If it is, that might be a good default resolution to pick as a default. If it’s not, you can check if (Display.main.systemWidth, Display.main.systemHeight) is supported.

There are some other caveats to picking a default resolution. On Mac, it’s possible for the OS to render to a higher resolution than the screen supports, and scale down. When they do this, this higher resolution would appear at the start of the Screen.resolutions array, even though it may be a resolution that’s too high for the GPU to support (on the 2015 5K iMacs this is an issue, for instance). This is hard to detect, but you might want to filter out resolutions that you deem too high. For most games, supporting up to 4K or 5K is probably enough.

But when filtering out resolutions that you deem too high, there’s always the possibility that the Screen.resolutions list doesn’t contain anything else. I’m not sure if this would ever happen, but it’s still good practice to try to catch any potential bugs. So if filtering out entries leaves you with an empty array, revert to the unfiltered list.

Exiting fullscreen

When the user exits fullscreen, I like scaling the window to about half the size of the screen. Usually, I have a list of preferred windowed sizes, and pick the one that’s slightly smaller than half the screen size.

Enough talk, show me the code

Here’s an example of how to handle this. This is a rewrite of my code, made for this tutorial, so it hasn’t been tested in a real game. I did test it, but do let me know if you find any issues.

I recommend trying to understand the code, and adjust it to suit your needs. Not every game will have the exact same requirements.

It’s quite elaborate, but it’s not rocket science. It might be over-engineered a bit, so I’m definitely interested if anyone knows a simpler solution.

Since Medium doesn’t have syntax highlighting, I also posted the code here : https://pastebin.com/y7kf7s7F

using System;
using System.Collections.Generic;
using UnityEngine;
public class FullScreenManager : MonoBehaviour {
public static FullScreenManager instance;
// Update this list to contain your preferred window sizes for windowed mode, from large to small
private List<Vector2Int> windowedResolutions = new List<Vector2Int> {
new Vector2Int(2560, 1440),
new Vector2Int(1920, 1080),
new Vector2Int(1280, 720),
new Vector2Int(640, 360),
new Vector2Int(320, 180)
};
private bool prevFullScreen;// If needed, you can set PreferredWidth and PreferredHeight from your settings screenprivate int? _preferredWidth = null;
// PreferredWidth will return 0 if this hasn't been set before
public int PreferredWidth {
get {
if (_preferredWidth == null) _preferredWidth = PlayerPrefs.GetInt ("PreferredResolutionWidth", 0);
return (int)_preferredWidth;
}
set {
if (value != _preferredWidth) {
_preferredWidth = value;
PlayerPrefs.SetInt ("PreferredResolutionWidth", value);
}
}
}
private int? _preferredHeight = null;
// PreferredHeight will return 0 if this hasn't been set before
public int PreferredHeight {
get {
if (_preferredHeight == null) _preferredHeight = PlayerPrefs.GetInt ("PreferredResolutionHeight", 0);
return (int)_preferredHeight;
}
set {
if (value != _preferredHeight) {
_preferredHeight = value;
PlayerPrefs.SetInt ("PreferredResolutionHeight", value);
}
}
}
void Start () {
if (instance != null) {
// If an instance already exists, we disable this one.
// It will remain in the hierarchy, but that's fine. I prefer this over destroying, less work for the GC
gameObject.SetActive (false);
return;
}
DontDestroyOnLoad (gameObject);
instance = this;
// Start up in fullscreen
// You could also store the previous state and use that when reopening
SetupFullscreen ();
}
void Update () {
if (Screen.fullScreen != prevFullScreen) {
if (Screen.fullScreen) {
SetupFullscreen ();
} else {
SetupWindowed ();
}
prevFullScreen = Screen.fullScreen;
}
}
private void SetupFullscreen () {
if(PreferredWidth != 0 && PreferredHeight != 0) {
// We have previous settings, check if it's available
if(ResolutionAvailable(PreferredWidth, PreferredHeight)) {
SetFullScreenResolution (PreferredWidth, PreferredHeight);
return;
}
}
// Find the first resolution <= 5K
for (var i = Screen.resolutions.Length - 1; i >= 0; i--) {
Resolution res = Screen.resolutions[i];
if(res.width <= 5120) {
SetFullScreenResolution (res.width, res.height);
return;
}
}
// There wasn't a resolution <= 5K... Try the first thing in the resolutions array
if(Screen.resolutions.Length > 0) {
Resolution highest = Screen.resolutions[Screen.resolutions.Length - 1];
SetFullScreenResolution (highest.width, highest.height);
return;
}
// Nothing in the Screen.resolutions array? Not sure if this will ever happen, but let's try Screen.display
SetFullScreenResolution (Display.main.systemWidth, Display.main.systemHeight);
}private bool ResolutionAvailable (int preferredWidth, int preferredHeight) {
foreach(var res in Screen.resolutions) {
if(res.width == preferredWidth && res.height == preferredHeight) {
return true;
}
}
return false;
}
private void SetupWindowed () {
// Let's try to find a resolution equal to or smaller than half the screen width
// It needs to also fit the height
for (int i = 0; i < windowedResolutions.Count; i++) {
var res = windowedResolutions[i];
if(res.x <= Screen.currentResolution.width / 2 && res.y <= Screen.currentResolution.height) {
SetWindowedResolution (res.x, res.y);
return;
}
}
// If nothing matched, let's go the other way and find the first one that's wider than half the screen width
for (int i = windowedResolutions.Count - 1; i >= 0; i--) {
var res = windowedResolutions[i];
if (res.x >= Screen.currentResolution.width / 2 && res.y <= Screen.currentResolution.height) {
SetWindowedResolution (res.x, res.y);
return;
}
}
// Just for sanity's sake, fall back to the smallest resolution in the list
var last = windowedResolutions[windowedResolutions.Count - 1];
SetWindowedResolution (last.x, last.y);
}
private void SetFullScreenResolution (int width, int height) {
// For my games, I prefer ExclusiveFullScreen with a fixed refresh rate of 60hz.
Screen.SetResolution (width, height, FullScreenMode.ExclusiveFullScreen, 60);
}
private void SetWindowedResolution (int width, int height) {
Screen.SetResolution (width, height, FullScreenMode.Windowed, 60);
}
}

And that’s it. If you find any issues with this snippet, please let me know.

If you need help with this code or if you have other Unity-related questions, there’s a #game-dev-advice channel in my Discord : https://discord.gg/zeCKc8SnHs

--

--