Author Topic: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts  (Read 7909 times)

SphereBaby

  • Newbie
  • *
  • Thank You
  • -Given: 1
  • -Receive: 4
  • Posts: 3
    • View Profile
There are several tutorials on Atlas Switching to handle HD and SD, but they all assume a FixedSize layout which is lazily stretched across the screen.

I wrote up this script and thought I'd share. It maintains "pixel perfect" formatting as DPI increases, scaling up elements in whole steps: 1x, 2x, 3x, etc. Atlas switching is also managed.

Instructions:
  • Create this script on anything
  • Set target DPI for 1x rendering.
  • Link each UIRoot you want scaled.
  • Link each reference atlas you want switched.
  • The scaled atlases must live in a "Resources" directory
  • The scaled atlases must use the "@1x" naming convection. A reference atlas named "wood" will look for "wood@1x" or "wood@2x" based on scaling.
  • The sprites must be named identically in each scaled atlas.

  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4.  
  5. /// <summary>
  6. /// Maintain PixelPerfect formatting as DPI increases.
  7. /// This is achieved by scaling UI in whole steps: 1X, 2X, 3x, etc.
  8. /// Atlas swapping is also managed, so higher resolution textures are used when appropriate.
  9. /// </summary>
  10. /// <example>
  11. /// For example, a layout made for iPhone 3 (320x480@160dpi)
  12. /// will look identical (but sharper) on iPhone 4 (640x960@320dpi)
  13. /// because we scale the UI by 2x and use textures with 2x resolution.
  14. /// </example>
  15. public class PixelPerfectDPIScaler : MonoBehaviour {
  16.  
  17.         /// <summary>
  18.         /// When DPI nears a multiple of this value, scale UIRoot appropriately.
  19.         /// </summary>
  20.         public int baseDPI = 160;
  21.  
  22.         /// <summary>
  23.         /// Scale to use in Editor
  24.         /// </summary>
  25.         [Range(1,4)]
  26.         public int editorScale = 1;
  27.  
  28.         public UIRoot[] uiRoots;
  29.  
  30.         /// <summary>
  31.         /// At runtime, this atlas will be made to reference its appropriate scaled variant.
  32.         /// Scaled variants must:
  33.         /// 1) Live in a "Resources" folder.
  34.         /// 2) Follow the "@1x" naming convention. A reference atlas named "wood" will look for "wood@1x", "wood@2x", etc.
  35.         /// 3) The sprites must be named identically in each scaled variant.
  36.         /// </summary>
  37.         /// <remarks>
  38.         /// If scale is 3, but only "@1x" and "@2x" atlases exist, "@2x" atlas will be used (stretched to 3x size).
  39.         /// </remarks>
  40.         public UIAtlas[] referenceAtlases;
  41.  
  42.         /// <summary>
  43.         /// Runtime changes on prefabs are not undone when playback ends.
  44.         /// Therefore, when running in editor, we cache original values to be restored later.
  45.         /// </summary>
  46.         private Dictionary<UIAtlas,UIAtlas> atlasReferences;
  47.  
  48.         public int GetScale() {
  49.                 if (Application.isEditor) {
  50.                         return editorScale;
  51.                 }
  52.                 else {
  53.                         // round to nearest multiple of baseDPI
  54.                         return Mathf.Max(1, Mathf.RoundToInt(Screen.dpi / (float)baseDPI));
  55.                 }
  56.         }
  57.  
  58.         public void Awake() {
  59.                 if (Application.isEditor) {
  60.                         CacheAtlasReferences();
  61.                 }
  62.                 UpdateReferenceAtlases();
  63.                 UpdateUIRoots();
  64.         }
  65.  
  66.         public void OnApplicationQuit() {
  67.                 if (Application.isEditor) {
  68.                         RestoreAtlasReferences();
  69.                 }
  70.         }
  71.  
  72.         public void Update() {
  73.                 // called each frame to handle device rotation and window resizing
  74.                 UpdateUIRoots();
  75.         }
  76.  
  77.         /// <summary>
  78.         /// Point each reference atlas at its appropriately scaled variant.
  79.         /// </summary>
  80.         private void UpdateReferenceAtlases() {
  81.                 int scale = GetScale();
  82.  
  83.                 foreach (UIAtlas referenceAtlasI in referenceAtlases) {
  84.                         string scaledAtlasName = "";
  85.                         for (int i = scale; i >= 1; i--) { // runs at least once
  86.                                 scaledAtlasName = string.Format("{0}@{1}x", referenceAtlasI.name, scale);
  87.  
  88.                                 if (referenceAtlasI.replacement != null && referenceAtlasI.replacement.name == scaledAtlasName)
  89.                                         break;
  90.  
  91.                                 UIAtlas scaledAtlas = Resources.Load<UIAtlas>(scaledAtlasName);
  92.                                 if (scaledAtlas) {
  93.                                         referenceAtlasI.replacement = scaledAtlas;
  94.                                         break;
  95.                                 }
  96.                         }
  97.  
  98.                         if (referenceAtlasI.replacement == null) {
  99.                                 Debug.LogError(string.Format("Cannot find atlas '{0}' in any 'Resources' folder", scaledAtlasName), this);
  100.                         }
  101.                 }
  102.         }
  103.  
  104.         /// <summary>
  105.         /// Scale UIRoots.
  106.         /// </summary>
  107.         /// <remarks>
  108.         /// We "double" the rendering size by telling a UIRoot that the screen is half its actual size
  109.         /// </remarks>
  110.         private void UpdateUIRoots() {
  111.                 int scale = GetScale();
  112.  
  113.                 // What if resolution is not divisible by scale? GOOD QUESTION
  114.                 if ((Screen.width % scale != 0) || (Screen.height % scale != 0)) {
  115.                         string msg = string.Format("Scaling is not pixel-perfect for {0}x{1} display @{2}x", Screen.width, Screen.height, scale);
  116.                         Debug.LogWarning(msg, this);
  117.                 }
  118.  
  119.                 int manualHeight = Mathf.RoundToInt(Screen.height / scale);
  120.                 foreach (UIRoot rootI in uiRoots) {
  121.                         if (rootI.scalingStyle != UIRoot.Scaling.FixedSize) {
  122.                                 rootI.scalingStyle = UIRoot.Scaling.FixedSize;
  123.                         }
  124.  
  125.                         if (rootI.manualHeight != manualHeight) {
  126.                                 rootI.manualHeight = manualHeight;
  127.                         }
  128.                 }
  129.         }
  130.  
  131.         private void CacheAtlasReferences() {
  132.                 atlasReferences = new Dictionary<UIAtlas,UIAtlas>();
  133.                 foreach (UIAtlas referenceAtlasI in referenceAtlases) {
  134.                         atlasReferences[referenceAtlasI] = referenceAtlasI.replacement;
  135.                 }
  136.         }
  137.        
  138.         private void RestoreAtlasReferences() {
  139.                 foreach (KeyValuePair<UIAtlas,UIAtlas> pair in atlasReferences) {
  140.                         pair.Key.replacement = pair.Value;
  141.                 }
  142.         }
  143.  
  144. }

I'm very new to Unity and NGUI, so apologies for any boneheaded mistakes.

Massive thanks to franktinsley who suggested using FixedSize scaling, and setting manualHeight to half the actual screen size to achieve 2x rendering.

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
« Reply #1 on: March 12, 2014, 09:32:36 PM »
Thanks for sharing!

adamnorton

  • Newbie
  • *
  • Thank You
  • -Given: 3
  • -Receive: 0
  • Posts: 5
    • View Profile
Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
« Reply #2 on: May 15, 2014, 09:53:03 AM »
Can fonts work with this too?  I'm been trying to figure it out, but not having much luck.  My coding skill are amateur at best.  Thanks for the script though, it's exactly what I've been looking for, providing I can get fonts to work with it too. 

SphereBaby

  • Newbie
  • *
  • Thank You
  • -Given: 1
  • -Receive: 4
  • Posts: 3
    • View Profile
Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
« Reply #3 on: May 15, 2014, 01:55:21 PM »
Yes, it scales UILabels as well. I was using Unity fonts and they rendered nicely at the desired resolution. A size 16 font would render at size 32 on a High-DPI device.

jandujar

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 0
  • Posts: 6
    • View Profile
Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
« Reply #4 on: May 28, 2014, 01:44:23 AM »
Can you post any example of usage?

I don't know how to implement this.

Does it works with asset bundles?