Tasharen Entertainment Forum

Support => NGUI 3 Support => Topic started by: SphereBaby on March 12, 2014, 08:33:48 PM

Title: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
Post by: SphereBaby on March 12, 2014, 08:33:48 PM
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:

  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 (http://www.tasharen.com/forum/index.php?topic=3532.msg35068#msg35068) using FixedSize scaling, and setting manualHeight to half the actual screen size to achieve 2x rendering.
Title: Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
Post by: ArenMook on March 12, 2014, 09:32:36 PM
Thanks for sharing!
Title: Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
Post by: adamnorton 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. 
Title: Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
Post by: SphereBaby 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.
Title: Re: Retina/High-DPI/Atlas-Switching script for PixelPerfect layouts
Post by: jandujar 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?