Unity Resolution/FullScreenModes

  • 2023-01-20
    • initially published
  • 2023-02-13
    • Many updates. Most important: I brought the Bomb Sworders implementation of these ideas to the code section. Note: this code hasn’t yet seen wide use, so use at your own risk. I’ll try to update my experience here in the future. Hoping that OS/Unity behavior doesn’t change significantly in the near future — the code in the example should be a good start to your game’s resolution/fullScreenMode handling. Fixed some stuff. Removed redundant sections. Added some info about how the game moves between monitors. Complained a bit about ExclusiveFullscreen mode and the behavior in Linux.

Why?

Because resolution/fullscreenModes are small but important parts of your game. And, if you use the default behavior, it’s very easy for a user to suddenly find themselves rendering at a much higher resolution than they expected. Which is no good… so these notes provide some code and then a lot of detail about how the various resolution/fullscreenModes settings seem to work in Unity.

Intro Notes

  • These notes provide information about working with resolution/FullScreenModes in Unity 2021.3.12f1
  • they are adapted from my personal notes — so excuse the outline style.
  • I can’t claim any in-depth knowledge
    • e.g. like how ExclusiveFullScreen actually works
  • but the information here is built from some reading and lots of testing
  • and is an attempt to describe the results of those tests
  • and to form conclusions about how various problems might be solved
  • This is not an article about how the pros handle resolution/fullScreenModes in Unity
  • because mostly the pros… seem not to handle it
    • (or make just a partial effort)
  • An important takeaway from that:
    • doing very little and mostly letting Unity/the-user handle it actually works okay-ish
  • But there are a few things that can definitely be better…
  • and it’s possible to fix them… so why not?
  • If you’re in a hurry, just read to the end of the code section.

  • this is aimed at Windows, Mac, and Linux
  • and tested on Windows 11, Mac Ventura 13.1, and Linux Mint 21.1 Cinnamon
    • both on a single monitor setup and a dual monitor setup
  • throughout the notes I refer to Windows, Mac, and Linux but the behavior described is really just what I saw in the specific versions listed above
  • but I suspect/hope the behavior generalizes fairly well
  • sneak peek:
    • while I, seemingly, got the Windows and Mac implementations to work well…
    • I wasn’t able to improve the Linux implementation much over what Unity gives you by default.
    • in short: maybe it’s best to discourage Linux users from using OS keyboard shortcuts to toggle-fullscreen and, if they are having issues with odd windowed sizes, have them set the OS display scaling to 100%.
  • some information is based on documentation or fairly recent (<3 years old) forum posts by Unity engineers
  • some information is based on the excellent A Clockwork Berry’s article
    • but a lot of things have changed since then (2015)
    • primarily, Unity has made working with resolution/fullScreenModes a bit easier
    • so this article is kinda an update

article terminology

  • native monitor resolution
    • the max resolution the user’s (player’s) monitor/projector/whatever is capable of displaying
    • e.g. if you have a UHD monitor (sometimes referred to as a 4k monitor), then your monitor’s native monitor resolution is 3840 x 2160 pixels.
    • how to get this value?
      • it’ll be the largest resolution in the Screen.resolutions array
  • display resolution
    • the resolution the user set their monitor to in their OS’s interface
      • e.g. for Windows: Display Settings > Display resolution
    • obviously this cannot be > the native monitor resolution
    • how to get this value?
      • Screen.mainWindowDisplayInfo.width/height when not in ExclusiveFullScreen mode
      • note I’ll often just refer to this as Screen.mainWindowDisplayInfo
  • render resolution
    • of course: the resolution the game is rendering at
    • how to get this value?
      • Screen.width
      • Screen.height
  • windowed mode
    • This and, more formally, FullScreenMode.Windowed both refer to the same thing.
    • –> that is, a non-fullScreen window that the player can drag around
    • Sometimes I’ll just call this windowed.
    • This is not to be confused with FullScreenMode.FullScreenWindow
      • which is actually fullScreen
  • (?)
    • I use this a few times just to say: “Not sure”

Code

The script below is designed/tested especially for FullScreenMode.FullScreenWindow and FullScreenMode.Windowed. All the FullScreenModes are described further below in detail, but using specifically these two — one for fullscreen and one for windowed — will be good for the large majority of Unity games released on Windows/Mac/Linux. That being said, the code below should also handle FullScreenMode.ExclusiveFullScreen ok but I didn’t do quite as much testing on that.

Again, note that this script has not yet seen wide use — so use at your own risk. I’ll try to update these notes with anything I find in the future. And I’m happy to hear ideas/feedback.

The code attempts to maintain the render resolution that you or the user set under situations where, by default, the Unity game would instead set its render resolution to the display resolution. E.g. if the user sets the game to render at 1920×1080 but are on a monitor with display resolution of 3840×2160… this script prevents the render resolution from suddenly jumping to 3840×2160 when the user presses the OS keyboard shortcut to toggle to fullscreen, uses the OS keyboard shortcut to move the game to a different monitor, and during calls to Screen.MoveMainWindowTo(...)

It also provides 3 events that other objects can subscribe to:

  • OnRenResChanged
  • OnRenResSettled, OnModeSettled
    • these are invoked only when the script believes that the current render resolution/FullScreenMode will be held for a while — that is, it isn’t there for just a frame or two before transitioning to something else.
    • this can be useful if, say, you have some expensive work you want to run whenever the resolution changes… but you don’t want to be running that work dozens of times in a row while the player resizes the window by dragging its edge.

And it also includes a couple helpful methods that you can call from anywhere in your code:

  • ResModeManager.TrySetRes(...)
    • To change render resolution and fullScreenMode
    • You’ll use this method instead of Screen.SetResolution(...)
  • The ResModeManager.GetResOptions(...) method returns a list of viable resolutions
    • You can use this to populate a Game Settings screen with resolutions the user can choose from
  • Unity stuff the script works well with:
    • Screen.fullscreen
      • read it or set it
    • Screen.MoveMainWindowTo(...)

setup

  • Put the script on a new gameObject in your scene. Within the script, DontDestroyOnLoad(...) is used to make sure the object stays available on all following scenes. And feel free to copy the gameObject into multiple scenes — the script will automatically destroy any instances of the gameObject that aren’t needed.
  • (Optional) Modify the Start() method to control the starting game resolution/mode
  • (Optional) Modify the GetValidResMode(...) and/or GetResOptions_Util(...) methods for your specific use.
    • read the comments for more details

The code is based on my game, Bomb Sworders. (If these notes are helpful to you, please consider wishlisting or purchasing the game as thanks! Of course, if you purchase the game, you can also see Bomb Sworders‘ resolution/fullScreenMode handling in action 🙂

Example Project Settings:

  • Project Settings > Player > Resolution and Presentation
    • initial setting: FullScreenMode.FullScreenWindow
      • when the user opens the game for the first time it’ll be a FullScreenWindow.
        • also known as a “borderless full screen”
      • this is a really friendly mode for users.
      • the game will take up the whole screen, no matter what render resolution you set.
      • if the aspect ratio of your render resolution is different than the display resolution:
        • Mac and Windows will add black bars
        • The native behavior on Linux is to stretch the image oddly to fill the screen
      • If you’re interested in details about this mode and the others, fairly extensive notes can be found further down
    • Default is Native Resolution: ON
      • when the game loads for the very first time, the render resolution will be set to the display resolution.
        • this will be completed already by the time Awake() is called
      • there’s no real harm in setting this ON
      • but we’ll be overriding it anyway in the script
    • Allow Fullscreen Switch: ON
      • I want the user to be able to switch between fullscreen (FullScreenMode.FullScreenWindow) and windowed (FullScreenMode.Windowed) using their OS’s keyboard shortcut
        • e.g. on Windows: alt+return
      • (On Linux this setting seems to have no effect)
    • Mac Retina Support: ON
      • (?) I can’t think of a good reason to ever turn this off…
      • (maybe you want to slightly annoy every person that bought a Mac in the last 8 years?)
    • Resizable Window: OFF
      • whether the user can change the render resolution of a windowed game by dragging on the window’s edges or using an OS keyboard shortcut or screen snapping or whatever
      • No real need to in my game.
      • that being said, the code below is written to handle this fine.

a bit more before the code

  • the code on Linux:
    • as mentioned near the beginning: for Linux (at least the version I was using) this code has only slight improvement over Unity’s default behavior. Mostly just the onRenResChanged and onRenResSettled events.
    • Again: for Linux users, it seems best to discourage them from using their OS keyboard shortcut for toggling fullscreen and to encourage them to turn off display scaling in their OS settings if they are seeing issues.
  • If running the game in windowed mode, never trust that the current render resolution will be what you last set it. e.g. For whatever reason in the default behavior on Windows, it can adjust slightly if the user drags the window around — even if they leave the window on the same monitor. The lesson: to get the render resolution, always use Screen.width/height
  • The script will default to using FullScreenWindow when doing fullscreen on Mac.
    • If you’d like to use MaximizedWindow instead (unlikely), you can change the _macDesiredFsM variable
  • If you want more details on moving-the-game-to-another-monitor…
    • see the below section “notes on moving the game to another monitor
  • What’s not included in the code example… but you should definitely include:
    • a Game Settings screen that lets the user choose among several resolutions
    • and maybe between windowed, fullscreen modes
  • If you discover bugs or unexpected behavior in the code, please let me know!
  • You can define the symbol “RES_DEBUG” to log messages about how the script is working.
    • Warnings and Errors will be logged whether or not you have the symbol defined.
    • Subscribe to the OnLog... events if, say, you want some in-game UI to respond to the logs
      • e.g. you could make a simple log-viewer
C#
// The MIT License (MIT)
// Copyright © 2023 Ross Klettke
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and
// associated documentation files (the “Software”), to
// deal in the Software without restriction, including
// without limitation the rights to use, copy, modify,
// merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom
// the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
// OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using UnityEngine;
using Debug = UnityEngine.Debug;

public class ResModeManager : MonoBehaviour {
    private static ResModeManager _instance;

#if RES_DEBUG
    uint _frameNum;

    // can hook into these to listen for log messages
    // e.g. you can build a UI that shows you logs
    // about resolution while running the build
    public static event Action<string> OnLogInfo;
    public static event Action<string> OnLogWarning;
    public static event Action<string> OnLogError;
#endif


    private struct ResMode : IEquatable<ResMode> {
        public Vector2Int RenRes { get; private set; }
        public FullScreenMode Mode { get; set; }

        public ResMode(int pW, int pH, FullScreenMode pMode) {
            RenRes = new Vector2Int(pW, pH);
            Mode = pMode;
        }

        public ResMode(Vector2Int pRes, FullScreenMode pMode) {
            RenRes = pRes;
            Mode = pMode;
        }

        public bool Equals(ResMode other) {
            return RenRes.Equals(other.RenRes) && Mode == other.Mode;
        }

        public override bool Equals(object obj) {
            return obj is ResMode other && Equals(other);
        }

        public override int GetHashCode() {
            return HashCode.Combine(RenRes, (int)Mode);
        }

        public bool isFullScr => Mode != FullScreenMode.Windowed;

        public override string ToString() {
            return $"({RenRes.x}x{RenRes.y}) {Mode}";
        }
    }


    // what was it the last frame?
    ResMode _prev;

    // the monitor/projector/display the game was on last frame.
    // note: if the displayRes changes from one frame to the
    // next then the Equals instance method will show it as
    // a "different" display
    DisplayInfo _prevDisplay;

    // and on the frame before that (a bit silly but...)
    DisplayInfo _prevPrevDisplay;

    // the most recent resMode that was "settled", that is:
    // distinguishing it from those that might appear for just
    // a single frame when, say, toggling the game to fullScreen
    ResMode _settled;

    // an event that other scripts can subscribe to.
    // note: this script tries to only invoke this event
    // at the moment it expects the current renderRes
    // will be "held".
    // when the listener hears this event, they can use
    // Screen.width/height to get the current renderRes.
    // also note: there are ways to "improve" this...
    // making it a bit faster to respond to an expected
    // heldRenderRes and making it resilient to super low
    // framerates. but the increase (though slight) in
    // complexity of the code I don't think is worth it
    // for the small improvements... especially given how
    // rare these renderRes changes are.
    // also note: given the testing I did, it doesn't
    // appear this will give "incorrect" invokes under
    // reasonable/even-fairly-unreasonable conditions. but
    // if worse comes to worse, an "incorrect" invoke
    // probably shouldn't be too bad -- depends on what
    // you have subscribed to this event
    public static event Action onRenResSettled;

    // similar as above but for when mode changes settle.
    // use Screen.FullScreenMode to get the current mode
    public static event Action onModeSettled;

    //called every frame the render res is changed
    public static event Action onRenResChanged;

    // in cases where the game thinks a newly-
    // arrived-at renderRes might be transitory,
    // wait this period of time before concluding
    // that this renderRes is "settled" --> at which
    // point the "on...Settled" events will be invoked. 
    float _settledDelay = float.PositiveInfinity;


    // on Mac, if the green "maximize" button is used to 
    // turn a windowed game to fullscreen or you call
    // Screen.MoveMainWindowTo(...) in code, the process
    // can take some time to execute and the value here
    // needs to be longer to not accidentally consider
    // any "transitory" resModes as "settled".
    // (despite the longer "settle" time -- it really doesn't
    //  have that big of an effect --> only in those rare
    //  moments when the player is changing the resolution
    //  by e.g. dragging the edge of a window, or those
    //  mentioned above)
#if UNITY_STANDALONE_OSX
    const float _STANDARD_ALERT_DELAY = 3f;
#else
    const float _STANDARD_ALERT_DELAY = 0.8f;
#endif

    // if there's a call to TrySetRes, the renderRes and
    // the fullScreenMode that are eventually passed into
    // Screen.SetResolution(...) are stored here while the
    // resolution-change is executing
    ResMode? _target;


    enum MacFullScreenMode {
        FullScreenWindow,
        MaximizedWindow
    }

    // an odd behavior of Mac is that when you call
    // Screen.MoveMainWindowTo(...) on a fullScreen game
    // it will automatically switch to MaximizedWindow on
    // the new display.
    // That's a bit annoying. So here, you can define for
    // Mac whether, when it's fullScreen, it should be
    // MaximizedWindow or FullScreenWindow.
    // (I think 99.9% of people will choose the latter so
    //  that's the default)
    [SerializeField] MacFullScreenMode _macDesiredFsM;


    /// <summary>
    /// Try to set a renderResolution and FullScreenMode.
    /// Note: "Try" because it may change the renderResolution
    /// and/or FullScreenMode you requested if the display doesn't
    /// support it.
    /// </summary>
    /// <param name="pRenW">the requested renderResolution width</param>
    /// <param name="pRenH">the requested renderResolution height</param>
    /// <param name="pMode">the requested FullScreenMode</param>
    public static void TrySetRes(int pRenW, int pRenH, FullScreenMode pMode) {
        if (_instance == null) {
            LogResError("No ResModeManager instance available");
            return;
        }
        _instance.TrySetRes(new Vector2Int(pRenW, pRenH), pMode);
    }


    bool GetIsFirstTimeOpeningGame() {
        var isFirstTimeOpening = !PlayerPrefs.HasKey("hasOpenedBefore");
        PlayerPrefs.SetInt("hasOpenedBefore", 1);
        return isFirstTimeOpening;
    }


    Vector2Int GetCurrentDisplayResolution() {
        var curFsM = Screen.fullScreenMode;
        if (curFsM == FullScreenMode.ExclusiveFullScreen) {
            LogResError("Screen.mainWin... invalid during exclusive");
        }

        var displayInfo = Screen.mainWindowDisplayInfo;
        return new Vector2Int(displayInfo.width, displayInfo.height);
    }


    private List<Vector2Int> _resOpts = new List<Vector2Int>();
    private void TryAddRes(Vector2Int pRes, Vector2Int pDisRes) {
        var aResMode = new ResMode(pRes, Screen.fullScreenMode);
        var validResMode = GetValidResMode(pRes);

        bool isValidRes = aResMode.Equals(validResMode);

        if (isValidRes && !_resOpts.Contains(validResMode.RenRes)) {
            _resOpts.Add(aResMode.RenRes);
        }
    }


    /// <summary>
    /// Get available resolution options for the current FullScreenMode.
    /// Useful for populating options into a Game Settings screen
    /// </summary>
    /// <param name="pIncludeCurrent">Include the current resolution?</param>
    /// <returns>A list of valid resolution options</returns>
    public static List<Vector2Int> GetResOptions(bool pIncludeCurrent) {
        // you may want to set pIncludeCurrent to TRUE if there's a
        // chance that the renderRes will be changed outside your control:
        // e.g. if Project Settings > Resizable Window is enabled and
        // the user drags on a window edge. Or if the user drags a
        // windowed game from one monitor to another differently-scaled
        // monitor on Windows.
        // AND you are now using GetResOptions(...) to populate some
        // buttons on like a Game Settings screen -- and you want to
        // be able to show the current renderRes among the options

        if (_instance == null) {
            LogResError("no valid ResModeManager");
            return null;
        }

        return _instance.GetResOptions_Util(pIncludeCurrent);
    }


    // get "valid" resolutions for the current mode.
    // Useful for populating options onto a
    // Game Settings screen of some kind.
    // this implementation will work for a lot of games
    // but feel free to tweak it however you like.
    // note that all resolutionOptions are checked against
    // GetValidResMode(...)
    private List<Vector2Int> GetResOptions_Util(bool pIncludeCurrent) {
        _resOpts.Clear();
        var currentMode = Screen.fullScreenMode;

        if (currentMode == FullScreenMode.ExclusiveFullScreen) {
            // when in this mode, Screen.mainWindowDisplayInfo
            // doesn't contain a valid displayRes... but it
            // doesn't really matter
            // --> because Screen.resolutions has all the valid
            //     resolutions for when in exclusiveFullScreen

            var resolutions = Screen.resolutions;
            for (int i = 0; i < resolutions.Length; i++) {
                var aRes = resolutions[i];

                var aResVec = new Vector2Int(aRes.width, aRes.height);
                var aResMode = GetValidResMode(aResVec);
                if (_resOpts.Contains(aResMode.RenRes) == false) {
                    _resOpts.Add(aResMode.RenRes);
                }
            }
        } else {
            var displayRes = GetCurrentDisplayResolution();
            float aspectRatio = (float)displayRes.x / displayRes.y;

            if (Mathf.Approximately(aspectRatio, 16f / 9)) {
                TryAddRes(new Vector2Int(1024, 576), displayRes);
                TryAddRes(new Vector2Int(1280, 720), displayRes);
                TryAddRes(new Vector2Int(1600, 900), displayRes);
                TryAddRes(new Vector2Int(1920, 1080), displayRes);
                TryAddRes(new Vector2Int(2560, 1440), displayRes);
                TryAddRes(new Vector2Int(3840, 2160), displayRes);
            } else if (Mathf.Approximately(aspectRatio, 16f / 10)) {
                TryAddRes(new Vector2Int(1000, 625), displayRes);
                TryAddRes(new Vector2Int(1280, 800), displayRes);
                TryAddRes(new Vector2Int(1440, 900), displayRes);
                TryAddRes(new Vector2Int(1680, 1050), displayRes);
                TryAddRes(new Vector2Int(1920, 1200), displayRes);
                TryAddRes(new Vector2Int(2560, 1600), displayRes);
            } else if (Mathf.Approximately(aspectRatio, 21f / 9)) {
                TryAddRes(new Vector2Int(1920, 800), displayRes);
                TryAddRes(new Vector2Int(2560, 1080), displayRes);
                TryAddRes(new Vector2Int(3440, 1440), displayRes);
                TryAddRes(new Vector2Int(5120, 2160), displayRes);
            } else if (Mathf.Approximately(aspectRatio, 4f / 3)) {
                TryAddRes(new Vector2Int(640, 480), displayRes);
                TryAddRes(new Vector2Int(800, 600), displayRes);
                TryAddRes(new Vector2Int(1024, 768), displayRes);
                TryAddRes(new Vector2Int(1280, 960), displayRes);
                TryAddRes(new Vector2Int(1400, 1050), displayRes);
                TryAddRes(new Vector2Int(1600, 1200), displayRes);
                TryAddRes(new Vector2Int(1920, 1440), displayRes);
                TryAddRes(new Vector2Int(2048, 1536), displayRes);
            } else if (Mathf.Approximately(aspectRatio, 5f / 4)) {
                TryAddRes(new Vector2Int(960, 720), displayRes);
                TryAddRes(new Vector2Int(1350, 1080), displayRes);
                TryAddRes(new Vector2Int(2160, 1728), displayRes);
                TryAddRes(new Vector2Int(2700, 2160), displayRes);
            } else {
                TryAddRes(new Vector2Int((int)(480 * aspectRatio), 480), displayRes);
                TryAddRes(new Vector2Int((int)(720 * aspectRatio), 720), displayRes);
                TryAddRes(new Vector2Int((int)(900 * aspectRatio), 900), displayRes);
                TryAddRes(new Vector2Int((int)(1080 * aspectRatio), 1080), displayRes);
                TryAddRes(new Vector2Int((int)(1440 * aspectRatio), 1440), displayRes);
                TryAddRes(new Vector2Int((int)(2160 * aspectRatio), 2160), displayRes);
            }
        }


        if (pIncludeCurrent) {
            var currentRenRes = new Vector2Int(Screen.width, Screen.height);
            if (_resOpts.Contains(currentRenRes) == false) {
                _resOpts.Add(currentRenRes);
                _resOpts.Sort((pFirstRes, pSecondRes) => {
                    if (pFirstRes.x == pSecondRes.x) {
                        return pFirstRes.y.CompareTo(pSecondRes.y);
                    } else {
                        return pFirstRes.x.CompareTo(pSecondRes.x);
                    }
                });
            }
        }


#if RES_DEBUG
        var strBuilder = new StringBuilder();
        strBuilder.AppendFormat("returning {0} res options: ", _resOpts.Count);
        for (int i = 0; i < _resOpts.Count; i++) {
            var aRes = _resOpts[i];
            if (i == 0) {
                strBuilder.AppendFormat("{0}x{1}", aRes.x, aRes.y);
            } else {
                strBuilder.AppendFormat(", {0}x{1}", aRes.x, aRes.y);
            }
        }

        LogResInfo(strBuilder.ToString());
#endif

        return _resOpts;
    }


    IEnumerator ExclusiveFix(Vector2Int pRenRes, FullScreenMode pMode) {
        LogResInfo("doing exclusiveModeFix");
        yield return null;
        TrySetRes(pRenRes, pMode);
    }


    // tries to set the renderResolution and mode.
    // note "Try" --> you aren't guaranteed to get
    // what you passed in here. It depends on logic
    // inside this method and the characteristics of
    // the display you're actually on: displayRes,
    // nativeMontiorRes, etc.
    void TrySetRes(Vector2Int pRenRes, FullScreenMode pMode) {
        LogResInfo($"try set res: {pRenRes.x}x{pRenRes.y}. {pMode}");

        var resMode = new ResMode(pRenRes, pMode);

        // be aware that this might be called when this script
        // detects that the view was moved to another monitor
        // and that this other monitor might have a different
        // displayRes than the prev monitor


        // ideally, any resolution passed to Screen.SetResolution(...)
        // at the end is "valid". That is:
        // if windowed: any resolution <= the displayRes.
        // if fullScreenWindow: any resolution <= the displayRes.
        // if exclusive (Windows OS only):
        //   -- the resolution must appear within the Screen.resolutions array

        if (Screen.fullScreenMode == FullScreenMode.ExclusiveFullScreen) {
            if (resMode.Mode == FullScreenMode.ExclusiveFullScreen) {
                // then need to make sure the resolution lines up
                // with one of those in Screen.resolutions
                resMode = GetValidResMode(resMode.RenRes);
            } else {
                // unfortunately, while in Exclusive we don't
                // have access to displayRes... so first hop
                // to the desired fullScreenMode, attempting the same
                // resolution that we currently have. Note that this
                // will prevent an OnRenResSettled call from occuring
                // (which is good -- we don't want "settled" to
                // be invoked for this transitory resolution).
                // also note: it'll prevent the the 2-frame delay
                // that exclusiveMode has when switching to a
                // higher-res other FullScreenMode.
                StartCoroutine(ExclusiveFix(resMode.RenRes, resMode.Mode));

                var aRenRes = new Vector2Int(Screen.width, Screen.height);
                resMode = new ResMode(aRenRes, resMode.Mode);
            }
        } else {
            if (resMode.Mode == FullScreenMode.MaximizedWindow &&
                _macDesiredFsM == MacFullScreenMode.FullScreenWindow) {
                resMode.Mode = FullScreenMode.FullScreenWindow;
            }

            // you can modify GetValidResMode() to
            // define which are valid for your game
            resMode = GetValidResMode(resMode.RenRes, resMode.Mode);
        }

        Screen.SetResolution(
            resMode.RenRes.x,
            resMode.RenRes.y,
            resMode.Mode
        );
        _target = resMode;
        LogResInfo($"--> setRes: {resMode}");
    }


    // morphs a given resolution into a valid
    // one for the current FullScreenMode
    ResMode GetValidResMode(Vector2Int pRes) {
        return GetValidResMode(pRes, Screen.fullScreenMode);
    }


    // morphs a given resolution into a valid one for
    // the provided FullScreenMode.
    // Every call to TrySetRes(...) is passed through this.
    // That includes both calls you've made and also "automatic"
    // calls done by this script: e.g. when the game is in windowed
    // mode and the user presses the OS shortcut to toggle
    // to fullscreen.
    // Note that this method is also used to limit the options
    // provided by GetResolutionOptions().
    // (despite this method, note that the game can have
    // renderResolutions outside "valid": e.g. if
    // Project Settings > Resizable Window is enabled and
    // the user drags on a window edge. Or if the user drags a
    // windowed game from one monitor to another differently-scaled
    // monitor on Windows.)
    ResMode GetValidResMode(Vector2Int pRes, FullScreenMode pMode) {
        if (Screen.fullScreenMode == FullScreenMode.ExclusiveFullScreen &&
            pMode != FullScreenMode.ExclusiveFullScreen) {
            // this means you tried GetValidResMode for a mode
            // that we can't currently evaluate
            LogResError("Can't access displayRes when in Exclusive");
            // silly but give an odd aspect ratio (square) to hint the error
            return new ResMode(700, 700, pMode);
        }

        if (pMode == FullScreenMode.ExclusiveFullScreen) {
#if UNITY_STANDALONE_WIN
            var validReses = Screen.resolutions;
            if (validReses.Length == 0) {
                // just in case
                pMode = FullScreenMode.FullScreenWindow;
                return GetValidResMode(pRes, pMode);
            } else {
                // find the nearest and return that.
                // if want, could ensure the resolution is the
                // same aspect ratio as the monitor here
                var first = validReses[0];
                var bestValidRes = new Vector2Int(first.width, first.height);
                float bestDstSqr = (pRes - bestValidRes).sqrMagnitude;
                for (int i = 1; i < validReses.Length; i++) {
                    var aRes = validReses[i];
                    var validRes = new Vector2Int(aRes.width, aRes.height);
                    var dstSqr = (pRes - validRes).sqrMagnitude;
                    if (dstSqr < bestDstSqr) {
                        bestValidRes = validRes;
                        bestDstSqr = dstSqr;
                    }
                }
                return new ResMode(bestValidRes, pMode);
            }
#else
           LogResWarn("exclusiveMode invalid on this OS");
           pMode = FullScreenMode.FullScreenWindow;
#endif
        }

        if (pMode == FullScreenMode.MaximizedWindow) {
#if UNITY_STANDALONE_OSX
                if (_macDesiredFsM == MacFullScreenMode.FullScreenWindow) {
                    pMode = FullScreenMode.FullScreenWindow;
                }
#else
            LogResWarn("maximizedWindow invalid on this OS");
            pMode = FullScreenMode.FullScreenWindow;
#endif
        }

        var displayRes = GetCurrentDisplayResolution();

        // then use displayRes to figure out good adjustments:
        // here it just makes sure that the renRes will fit
        // within th displayRes. Additionally, you may want to
        // guarantee that the renderRes has the same aspect
        // ratio as the display res.
        int newWidth = Mathf.Min(pRes.x, displayRes.x);
        int newHeight = Mathf.Min(pRes.y, displayRes.y);
        var resToUse = new Vector2Int(newWidth, newHeight);

        return new ResMode(resToUse, pMode);
    }


    void Awake() {
        if (_instance != null) {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }


    void Start() {
        _prevDisplay = Screen.mainWindowDisplayInfo;

        _prevPrevDisplay = _prevDisplay;
        _settled = new ResMode(Screen.width, Screen.height, Screen.fullScreenMode);
        _prev = _settled;

        LogResInfo($"initial renRes: {Screen.width}x{Screen.height}");

        if (GetIsFirstTimeOpeningGame()) {
            // just one example of what you might do
            // for choosing a renderResolution when the
            // user opens the game for the very first time:

            // choose a render resolution with the same
            // aspect ratio as the display and with a height that's
            // the larger of (displayResHeight / 2) or 720

            LogResInfo("first time playing");

            if (Screen.fullScreenMode == FullScreenMode.ExclusiveFullScreen) {
                if (Screen.resolutions.Length > 0) {
                    // if is the first time ever starting the game
                    // and in exclusiveMode, then try to choose
                    // a renderRes that is roughly half of what the
                    // monitor supports.
                    var lastIndex = Screen.resolutions.Length - 1;
                    var highestRes = Screen.resolutions[lastIndex];
                    var desiredRes = new Vector2Int();
                    desiredRes.x = highestRes.width / 2;
                    desiredRes.y = highestRes.height / 2;
                    TrySetRes(desiredRes, Screen.fullScreenMode);
                } else {
                    // probably this will never happen but just in case...
                    var desiredRes = new Vector2Int(1280, 720);
                    TrySetRes(desiredRes, FullScreenMode.FullScreenWindow);
                }
            } else {
                var displayRes = GetCurrentDisplayResolution();
                float dAspectRatio = (float)displayRes.x / displayRes.y;

                int desiredHeight = Mathf.Max(displayRes.y / 2, 720);
                int desiredWidth = (int)(desiredHeight * dAspectRatio);

                var desiredRes = new Vector2Int(desiredWidth, desiredHeight);

                TrySetRes(desiredRes, Screen.fullScreenMode);
            }
        } else {
            // anytime the resolution/fullscreenMode stuff is set,
            // Unity automatically stores the values to PlayerPrefs so
            // that the next time the game is run, the same
            // render resolution, fullScreenMode, currentDisplay,
            // and currentDisplayPos can automatically be used again.

            // But what if since the last time the game was run,
            // the user is now using a different monitor?
            // Or has set a different display resolution?
            // (tbh I didn't fully test whether Unity recognizes a new
            // monitor/different displayRes and automatically ignores
            // the "old" PlayerPrefs stuff... but also it's easy
            // to do the following and not have to worry about it)

            LogResInfo("using res from last time playing");
            TrySetRes(_settled.RenRes, Screen.fullScreenMode);
        }
    }


    void Update() {
        var cur = new ResMode(Screen.width, Screen.height, Screen.fullScreenMode);

        if (_target.HasValue && _target.Value.Equals(cur)) {
            LogResInfo($"target reached. {cur} vs {_prev}");

            if (cur.RenRes != _prev.RenRes) {
                onRenResChanged?.Invoke();
            }

            _settledDelay = -0.1f;
        } else if (cur.RenRes != _prev.RenRes) {
            onRenResChanged?.Invoke();
            LogResInfo($"cur {cur} != prev {_prev}");

            // covers things like if the user is dragging the edges
            // of a resizableWindow. or dragged/moved the window
            // onto another monitor. or called Screen.SetResolution(...)
            // with an invalid resolution for the display
            _settledDelay = _STANDARD_ALERT_DELAY;

            if (cur.isFullScr) {
                var currentDisplay = Screen.mainWindowDisplayInfo;
                if (currentDisplay.Equals(_prevDisplay) == false) {
                    LogResInfo("switched to another monitor programmatically");
                    TrySetRes(_settled.RenRes, cur.Mode);

                    // make sure "switched to another monitor using OS shortcuts"
                    // won't be unnecessarily triggered next frame.
                    // silly but...
                    _prevDisplay.width = -1;
                } else if (_prevDisplay.Equals(_prevPrevDisplay) == false) {
                    LogResInfo("switched to another monitor using OS shortcuts");
                    TrySetRes(_settled.RenRes, cur.Mode);
                } else if (_prev.isFullScr == false) {
                    // might have been caused by toggling to fullScreen
                    // using OS shortcuts...

                    // or, on mac, if doing a programmatic move...
                    // and we started from a true fullscreen mode
                    // --> there's a condition under which this
                    // will be triggered.

                    LogResInfo("became fullscreen");

                    var fullScreenModeToUse = cur.Mode;

                    if (fullScreenModeToUse == FullScreenMode.MaximizedWindow &&
                        _macDesiredFsM == MacFullScreenMode.FullScreenWindow) {
                        fullScreenModeToUse = FullScreenMode.FullScreenWindow;
                    }

                    TrySetRes(_settled.RenRes, fullScreenModeToUse);
                }
            }
#if UNITY_STANDALONE_LINUX
            // in my testing on linux, pressing the OS shortcut
            // for toggle-fullscreen caused a windowed view to
            // set its renderRes to the same as the displayRes.
            // this here attempts to detect that by looking for
            // moments that the renderRes suddenly jumped to
            // the displayRes and switching it out for
            // fullScreenWindow mode with the same renderRes
            // as before.
            // this is a bit sloppy and really it appears that
            // dealing with renderRes/mode on Linux is tough
            // --> so the best strategy seems to be to encourage
            // Linux users to not use OS shortcuts for
            // toggle-fullscreen and, if they have issues, to
            // perhaps, in their OS settings, turn off
            // display scaling
            else if (cur.Mode == FullScreenMode.Windowed) {
                if (cur.RenRes == GetCurrentDisplayResolution()) {
                    // alternatively they might be "maximizing the
                    // window" but I think I'm okay capturing that
                    // and converting it into a fullscreen...

                    float dstToPrevRes = (cur.RenRes - _prev.RenRes).magnitude;
                    // 200f is an arbitrary number
                    if (dstToPrevRes > 200f) {  
                        //presumably in here there's a decent chance
                        // this is the user pressing the fullscreen toggle
                        LogResInfo("linux: togged to fullscreen?");
                        TrySetRes(_settled.RenRes, FullScreenMode.FullScreenWindow);
                    } else {
                        LogResInfo("linux: resized window to same as displayRes?");
                    }
                }
            }
#endif
        } else if (cur.Mode != _prev.Mode) {
            LogResInfo("detected programmatic fullscreen toggle," +
                       "or OS shortcut into/out-of fullscreen");
            if (cur.isFullScr) {
                var fullScreenModeToUse = cur.Mode;

                if (fullScreenModeToUse == FullScreenMode.MaximizedWindow &&
                    _macDesiredFsM == MacFullScreenMode.FullScreenWindow) {
                    fullScreenModeToUse = FullScreenMode.FullScreenWindow;
                }

                TrySetRes(cur.RenRes, fullScreenModeToUse);
            } else {
                // we went from fullScreen to windowed (and kept the same resolution).
                // if we wanted, this is the spot we would put a fix for the case of:
                // e.g. a displayRes of 1920x1080. exclusiveMode renRes of 3840x2160
                // --> and toggle out of fullscreen.
                // default behavior is that the renderRes will still be 3840x2160
                // and we'll only see a quarter of the window.
                // --> so if that is undesired behavior... fix it here.
                // (I think there's a decent reason not to)
                _settledDelay = -0.1f;
            }
        }

        // determine if the current renderRes should be
        // considered as "settled" --> and invoke the events
        if (_settledDelay < 0f) {
            _settledDelay = float.PositiveInfinity;

            bool resChanged = false;
            bool modeChanged = false;

            if (cur.RenRes != _settled.RenRes) {
                resChanged = true;
                onRenResSettled?.Invoke();
            }

            if (cur.Mode != _settled.Mode) {
                modeChanged = true;
                onModeSettled?.Invoke();
            }

            if (resChanged && modeChanged) {
                LogResInfo($"on resAndMode SETTLED. {cur}");
            } else if (resChanged) {
                LogResInfo($"on res SETTLED. {cur}");
            } else if (modeChanged) {
                LogResInfo($"on mode SETTLED. {cur}");
            } else {
                LogResInfo("NOTHING CHANGED");
            }

            _settled = new ResMode(cur.RenRes, Screen.fullScreenMode);
            _target = null;
        }

        _settledDelay -= Time.deltaTime;
        _prev = cur;
        _prevPrevDisplay = _prevDisplay;
        _prevDisplay = Screen.mainWindowDisplayInfo;
    }


    [Conditional("RES_DEBUG")]
    void LogResInfo(string pMessage) {
#if RES_DEBUG
        string message = $"{_frameNum}:: {pMessage}";
        Debug.Log(message);
        OnLogInfo?.Invoke(message);
#endif
    }

    void LogResWarn(string pMessage) {
#if RES_DEBUG
        string message = $"{_frameNum}:: {pMessage}";
        Debug.LogWarning(message);
        OnLogWarning?.Invoke(message);
#else
        Debug.LogWarning(pMessage);
#endif
    }

    static void LogResError(string pMessage) {
#if RES_DEBUG
        if (_instance == null) {
            Debug.LogError(pMessage);
            OnLogError?.Invoke(pMessage);
        } else {
            string message = $"{_instance._frameNum}:: {pMessage}";
            Debug.LogError(message);
            OnLogError?.Invoke(message);
        }
#else
        Debug.LogError(pMessage);
#endif
    }


#if RES_DEBUG
    void LateUpdate() {
        _frameNum++;
    }
#endif


    void OnDestroy() {
        if (_instance == this) {
            _instance = null;
        }
    }
}

what follows is

With the 2023-02-13 update, the following has been somewhat backseated as just “the notes that fed into the code”. Still, for a person who wants a fuller grasp, it is worth skimming at the very least.

The high-level overview is:

  • the different FullScreenModes
  • some “maybe useful miscellany”

FullScreenModes overview

  • FullScreenMode.Windowed
  • FullScreenMode.MaximizedWindow (Mac only)
  • FullScreenMode.ExclusiveFullScreen (Windows only)
  • FullScreenMode.FullScreenWindow

one note before getting to the mode descriptions

  • after calling Screen.SetResolution(renderWidth, renderHeight, fullScreenMode)
  • as mentioned previously, it takes 1 frame before
    • the new resolution/mode is active
    • and the values of Screen.width, Screen.height, etc are updated
  • BUT there is an exception:
    • on Windows if you are switching from ExclusiveFullScreen to a higher render resolution in some other FullScreenMode (FullScreenWindow or Windowed)
    • then it appears to take 2 frames before the various resolution/FullScreenMode related values are updated

FullScreenMode.Windowed

  • well, actually this is if you don’t want fullscreen
  • this mode draws the game into a window
  • e.g. if the display resolution is 3840×2160 and you tell Unity you want a Windowed view with a render resolution of 1920×1080
    • Screen.SetResolution(1920,1080, FullScreenMode.Windowed)
    • –> the game window will take up a quarter of your display
  • choose whatever resolution you want
    • aspect ratio doesn’t matter — go wild
    • Mac and Linux will let you choose any render resolution
      • so be careful — make sure to check it against the display resolution.
        • again, you can get that from Screen.mainWindowDisplayInfo
    • on Windows the render resolution will automatically be limited by total width and total height of the connected monitors’ display resolutions
      • e.g. You have 2 1920×1080 monitors side-by-side
      • (side-by-side: as defined within your OS’s display settings)
      • and request a window at resolution 5000×5000:
      • –> will result in a window with width: (1920*2) = 3840 and height: 1080

FullScreenMode.MaximizedWindow

  • MacOS only
  • if this mode is used on Windows or Linux, the build will fallback to using the FullScreenWindow mode
  • In practice this fills the screen with the game while also displaying the app’s top menu bar
    • note: if attempting to go from FullScreenWindow to MaximizedWindow, the top menu bar will not appear
    • that might be a bug…
  • You can choose any resolution
    • black bars will automatically be added as necessary
    • render resolution will not automatically be limited <= the display resolution
      • so, again, be careful about that

FullScreenMode.ExclusiveFullScreen

  • Windows only
  • if this mode is used on Mac or Linux, the build will fallback to FullscreenWindow mode
  • ExclusiveFullScreen mode is an interesting one
  • the big feature of ExclusiveFullScreen mode is how closely it communicates with the monitor
  • because of that, it is the fastest, most efficient option
    • possibly good if your game is fast-paced AND really pushing the device to its limits…
    • but, the downside is that ExclusiveFullScreen doesn’t have great multiple-monitor and app-switching support
      • which can be annoying to users
    • so nowadays most games use the FullScreenWindow mode instead (described below)
    • over time, the efficiency/feature advantage of ExclusiveFullscreen over FullScreenWindow has been decreasing
      • I’ve seen recent posts by 2 different Unity engineers — one said ExclusiveFullScreen is worth considering for fast-paced, performance-crucial games… and the other said to pretty much just default to using FullScreenWindow
      • for a while things like GSync and FreeSync were only available in Unity’s ExclusiveFullScreen mode… but now they’re in FullScreenWindow too
  • It’s also unique in that it’s the only mode that can show a render resolution > the monitor’s display resolution
    • (only in the scenario that the display resolution is < native monitor resolution)
    • note: I’m trying to be careful with the phrasing here by saying “the only mode that can show the render resolution > the monitor’s display resolution
      • because some other FullScreenModes, as mentioned above, might allow you to set the render resolution > the display resolution
        • but, in those scenarios, it seems(?) that the higher render resolution is then squashed to fit within the display resolution
        • –> a waste of resources
        • –> aka something you want to avoid doing
  • Screen.SetResolution(width,height,FullScreenMode.ExclusiveFullScreen) can be called at anytime
    • not just at start-up (as some, probably old, online info states)
    • UPDATE: no longer sure if this is true about “can be called at anytime” — for a while it seemed like I was able to change ExclusiveFullScreen anytime (or at least Unity was telling me that it was successful)… but more recently in one of my test projects… it just suddenly stopped consistently working — very often giving me a perpetual, occasionally flickering, black screen.
    • that being said, the code in the code example above has so far continued to work as if it is possible to change the ExclusiveFullScreen resolution at any time…
  • Calling Screen.SetResolution(…) with ExclusiveFullScreen mode does not change the display resolution
    • to return to this article’s definition of display resolution: “the resolution the user set their monitor at in their OS’s interface”
    • e.g. in a scenario where the native monitor resolution is 3840×2160 and the display resolution is 1920×1080
      • if we call Screen.SetResolution(3840,2160,FullScreenMode.ExclusiveFullScreen)
      • when the screen updates, the render resolution will be 3840×2160
      • and it will be shown at that resolution
      • but the display resolution will still be 1920×1080
        • note that within ExclusiveFullScreen, the value of Screen.mainWindowDisplayInfo is invalid
  • when using ExclusiveFullScreen, you, the dev, can only set it to resolutions that the monitor explicitly supports.
    • to get a list of these use Screen.resolutions
      • these resolutions are filtered by what you’ve enabled in Project Settings > Player > Resolution and Presentation > Supported Aspect Ratios
      • and by whether the “Mac Retina Support” option is enabled.
      • note that some of the resolutions in the list may be > the display resolution
    • if you try setting to a resolution that isn’t in that list
      • Unity will automatically choose a similar resolution from the list
    • some of these “supported resolutions” may have aspectRatios different than the monitor
      • if you choose one of those, the render will be stretched, awkwardly, to fill the monitor (no black bars appear)
      • –> it can look pretty unpleasant
  • in testing, the same resolution in FullScreenMode.ExclusiveFullScreen looked slightly sharper than it did in FullScreenMode.FullScreenWindow
    • noticeable in low resolutions <= 720p
    • harder for me to tell the difference >= 1080p
    • (?) presumably due to how the render is scaled in FullScreenMode.FullScreenWindow

FullScreenMode.FullScreenWindow

  • also known as “borderless full screen”
  • probably the best-and-easiest option for the majority of games
  • in this mode the game is rendered, sent to the OS, and then the OS handles drawing it to the screen
    • so the game is treated just like any other app
    • and it works great with multiple-monitors and app-switching
  • it takes up the entire screen. No titleBar or dock(macOS) visible.
  • you should set the render resolution <= the display resolution
    • doesn’t have to be one of the resolutions within Screen.resolutions
    • what happens if you pick a lower resolution?
      • –> stretched to fit the screen
    • what if you choose an aspectRatio that isn’t the same as the display resolution?
      • on Windows and Mac
        • –> automatically get black bars
        • (a note for devs familiar with the “old black bars”: the new improved ones will redraw themselves constantly)
      • on Linux
        • –> no black bars
        • –> instead the render is awkwardly stretched to fill the entire screen
    • what if you ignore the advice and choose a render resolution > the display resolution?
      • Linux and Windows
        • –> the render resolution is automatically limited to the display resolution
      • Mac
        • –> no limits
        • –> (?) rendered at the higher res and then it’s scrunched down to fit the screen
          • a waste of resources

Maybe Useful Miscellany

Note: In general, the below assumes that the game, when in fullScreen, is using FullScreenMode.FullScreenWindow.

a bit about display-scaling

  • for the most part you can ignore the various OS display-scaling options.
    • that is, on Mac ignore the “Looks like…” thing and “retina” word.
    • and on Windows, ignore “Display Scaling”
  • there is one case I’ve found where, at least, Windows’ scaling does seem to come into play:
    • when moving a windowed-game to another monitor by dragging it or using the OS keyboard shortcut.
    • The code example itself ignores this behavior
    • … and I think that is an okay approach
  • unfortunately, in the version of Linux I used, the display scaling functionality can have a pretty huge impact.
    • if no screen scaling is used –> good
    • but if using scaling:
      • with the by-default-enabled setting of “fractional scaling controls (experimental)” –> very odd/annoying behavior for windowed mode sizing.
      • with the good ole not-“fractional scaling controls” –> fine

getting the display resolution on older versions of Unity

  • (I’m including some info about prior versions of Unity here but, generally, the overall notes are still only for Unity 2021.3.12f1)
  • Unity 2021 has Screen.mainWindowDisplayInfo which is the simpler/better/easier route to getting the display resolution nowadays. Again, note that it is invalid while in ExclusiveFullScreen mode… but when in ExclusiveFullScreen mode you’ll be using Screen.resolutions to see valid options anyway.
  • Unity 2019 doesn’t have Screen.mainWindowDisplayInfo
    • But you can still get the display resolution.
    • On Linux:
      • Screen.currentResolution seems to always contain the display resolution
    • On Mac and Windows:
      • Screen.currentResolution will contain the display resolution when the game is in FullScreenMode.Windowed
      • so, if the game is currently in some other FullScreenMode
      • the strategy is to:
      • Screen.SetResolution(, , FullScreenMode.Windowed)
      • wait 2 frames in a coroutine to make sure the Screen values are all updated
      • grab Screen.currentResolution — which now contains the display resolution
      • then switch back to whatever FullScreenMode you want
  • Unity 2017 doesn’t have Screen.mainWindowDisplayInfo
    • But you can still get the display resolution.
    • (I haven’t tested this one recently… but I think it may be correct)
    • On Linux:
      • (?) It seems that the behavior of Screen.currentResolution might have been hard to pin down in a useful way
    • On Mac:
      • Screen.currentResolution seems to always contain the display resolution
    • On Windows:
      • Screen.currentResolution will contain the display resolution when the game is in FullScreenMode.Windowed
      • so, if the game is currently in some other FullScreenMode
      • the strategy is to:
      • Screen.SetResolution(, , FullScreenMode.Windowed)
      • wait 2 frames in a coroutine to make sure the Screen values are all updated
      • grab Screen.currentResolution — which now contains the display resolution
      • then switch back to whatever FullScreenMode you want

if you really want black bars…

  • and need them to work on Linux OR are using FullScreenMode.ExclusiveFullScreen(Windows only)
  • –> maybe take a look at the AspectUtility.cs script for inspiration.
  • (?) perhaps there are better approaches — I didn’t spend time looking into this

the thing about PlayerPrefs

  • when you open a build of the game, as you make changes to the resolution, fullScreenMode, etc — the values are automatically stored to PlayerPrefs
  • then on future loads of the game, those PlayerPrefs are used to initialize the screen
    • this is done before Awake is called
  • generally this is great: users can set their preferences when they’re in the game
    • and then the next time they play, those preferences will be loaded back automatically
  • … but it might be annoying when you, the dev, are trying to test out various resolution/FullScreenMode behavior.
  • if it is, then from within the build you can call PlayerPrefs.DeleteAll()
    • then the next time the build is opened, it’ll use the defaults specified in Project Settings > Player > Resolution and Presentation
    • obviously that approach deletes all PlayerPrefs…
    • Alternatively, you could delete the specific PlayerPref keys:
      • Screenmanager Resolution Width
      • Screenmanager Resolution Height
      • Screenmanager Fullscreen mode
        • (note that “mode” is lowercase)
      • but be aware that these keys may change in future versions of Unity
  • If for some reason you don’t want to modify PlayerPrefs at all:
    • Unity offers a bunch of different flags for starting a game from the command line
      • the resolution options you provide there will override the PlayerPrefs
    • unfortunately, none of the flags seem to give control over the FullScreenMode on Mac/Linux(?)
    • (I haven’t tested the commandLine method)

have you had problems with PlayerPrefs not updating after a call to Screen.SetResolution(…)?

notes on moving the game to another monitor

  • you can do it programmatically
    • use Screen.GetDisplayLayout(…) to populate a list of the DisplayInfos of connected monitors
    • and then Screen.MoveMainWindowTo(…)
    • use a coroutine to wait for the Screen.MoveMainWindowTo(…) AsyncOperation to complete
      • e.g. yield return Screen.MoveMainWindowTo(otherMonitorDisplayInfo, Vector2Int.zero)
    • and then you’ll have access to all the updated Screen-related variables
    • rather than using the AsyncOperation to handle the results of Screen.MoveMainWindowTo(…) maybe it’s best to try to write code that’ll handle all the ways that the game might move monitors. See the Code Example
  • the user, themselves, might also move the game to another monitor:
    • dragging if the game is windowed or
    • e.g. on Windows, by pressing: windowsKey + shift + left/right arrowKey
      • note that this shortcut appears to be disabled when the mode is ExclusiveFullScreen
  • important: the render resolution might automatically change when the game is moved to another monitor
    • if the game was fullScreen:
      • e.g. on Windows
      • if the monitors have different display resolutions, it’ll attempt to switch the render resolution to the display resolution of the new monitor
      • I might not have a full grasp on all the factors here but the end result is:
      • –> when a fullScreen game is moved to another monitor, you should probably call Screen.SetResolution(...) if you want to maintain some control over the render resolution
    • if the game was windowed:
      • the render resolution may change under several factors — some out of your control
        • (this is where on Windows the relative Display Scale between the monitors can come into play when the user uses the OS keyboard shortcut to move the game to another monitor or if they drag the windowed game)
          • this doesn’t happen if Screen.MoveMainWindowTo(…) is used
          • but it doesn’t appear to be possible to stop the user from using the other methods
        • (?) This change to render resolution probably won’t be very much
      • and, in fact, due to a couple annoying issues in Windows (and possibly the other OSs)
        • that I wasn’t able to figure out satisfactorily…
        • for my game, Bomb Sworders, I don’t try to “correct” the render resolution when the user drags a windowed game around.
        • (nor when the OS keyboard shortcut is used)
        • the user still can, of course, go to the game settings screen and select a render resolution themselves if they want
      • What are the annoying issues?
        • if the windowed game is moved to the other monitor by OS shortcut key or by Screen.MoveMainWindowTo(...)
          • –> fine
        • but if the game window is moved by the user dragging it to another monitor…
          • (?) it seems that Windows/Unity is constantly attempting to set the resolution while the drag is occurring
          • and (maybe I’m overlooking something easy…) it was hard to determine how to “get the last word” as far as what the resolution should be set at.
          • in fact, calling Screen.SetResolution(…) at various points actually sometimes led to the game crashing.
          • if you find yourself really caring about trying to control the render resolution as the user drags the windowed game from one monitor to the other
          • (?) potentially the solution is to, upon detecting a render resolution change
            • bring the game to some sort of pause screen
            • and then when the user clicks on the screen to unpause, presumably Windows’/Unity’s constant setting-resolution behavior will be finished and Screen.SetResolution() can be called safely at that point
            • (there might also be some odd interaction with the Windows’ snapping feature)
          • If you figure out a good solution, please let me know!
        • also there’s an issue where if you set the resolution and then the user drags the window a bit (even leaving it on the same monitor)
          • –> when the user mouse-ups, the render resolution may change slightly
          • (?) I think taking into account the topBar and borders of the window

should “Mac Retina Support” be enabled?

  • YES!
  • Project Settings > Player > Resolution and Presentation

the primary way that you, the dev, will change resolution/FullScreenMode

  • Screen.SetResolution(renderWidth, renderHeight, fullScreenMode)
  • Note: If you’re using my script — you’ll instead be using ResModeManager.TrySetRes(...)

ways the user (the player) might change resolution/FullScreenMode/etc

  • if “Resizable Window” is enabled and the game is in Windowed mode
    • the user can drag the edges of the window to resize it.
    • the “Resizable Window” option can be found in:
      • Project Settings > Player > Resolution and Presentation
  • as mentioned previously, if the game is in Windowed mode, the user could drag it to another monitor.
    • –> which can trigger a render resolution change
  • some additional examples in Windows 11:
    • alt + return
      • to toggle back and forth between Windowed and the fullScreenMode you defined in the Project Settings
        • (I suspect but didn’t test: if you have Windowed as the default fullScreenMode in Project Settings — maybe it defaults to FullScreenWindow?)
      • It seems that when you go from Windowed to fullscreen using alt + return
      • the render resolution will be changed to match the display resolution
      • which can be annoying…
      • 2 options:
        • respond to it using the method described in the Code Example section above
        • OR disable Project Settings > Player > Resolution and Presentation > Allow Fullscreen Switch
          • this disables the alt + return functionality
    • windowsKey + shift + left/right arrowKey
      • move the game to another monitor
    • (if in FullScreenMode.Windowed) windowsKey + arrowKey
      • change the render resolution and reposition the game window
      • this ability is disabled if “Resizable Window” is turned off in the Project Settings
  • a note on Linux:
    • if the game is in FullScreenMode.FullScreenWindow and the user presses the OS keyboard shortcut to toggle fullscreen
      • –> you’ll come across a new/annoying mode-behavior that I’m naming: FullScreenMode.WindowedFullScreenWindow
      • It has the characteristics of FullScreenWindow on Linux… but is windowed.
      • basically, the worst characteristics of both…
      • The problem: I haven’t found a way in code to actually detect that this has happened.
      • So this is why you should discourage Linux users from using the OS keyboard shortcut for toggling fullscreen…
      • (One final time: I’m only describing the results of what I saw on my one Linux computer– and I am not really a Linux user… I would be interested to hear the results of people’s testing on other versions of Linux)

Write a Comment

Comment