Dynamically Selecting Sprite Size

Background

As I was working on polishing up Glide, one of the goals I had was the game would look good no matter the device it was being played on. Due to the large variance in mobile displays on the market, this meant I wanted to support all resolutions, from 480x800 all the way up to 2560x1440 and beyond.

Just extending how much the camera can see wasn’t a valid option for this game, I wanted the puzzles to take up a uniform amount of screen space across devices. This means I needed to scale-up the game for high-res screens, and scale it down on low-res screens.

Unity does a very good abstracting this away, so getting the scaling working was fairly straightforward. However after I implemented this and started testing it at different resolutions I discovered an issue.

The art style for Glide is very minimal, and makes use of clean edges. As the sprites were scaled up and down, blurriness or jagged edges were introduced. An example of this is shown below.

Example

To demonstrate this issue issue, I created the same sprite in 2 different resolutions, one at 128x128, and one at 512x512. These were imported into Unity with PixelsPerUnit set to their resolution (so 128 and 512 respectively) so each would be 1 unit size in Unity. They also both had mipmaps enabled.

To see the scaling in action, this first image shows the sprites side-by-side with the camera positioned so 1 unit=512pixels. This means the 512x512 image is not scaled at all, and the 128x128 has be scaled up 4x.

128x128 image on top up-scaled 4x. 512x512 on bottom with no scaling

As you can see, scaling up the 128x128 image causes significant blurriness. Scaling down causes similar issues, though not to the same degree. In this next image, the camera is positioned so 1 unit=128 pixels. This means the 512x512 image is scaled down 4x, and the 128x128 image is not scaled at all.

128x128 image on top with no scaling. 512x512 on bottom scaled down 4x.

Scaling down the 512x512 image actually doesn’t look too bad, however the edges have become slightly jagged. While I could probably live with the down-scaling, I decided to try and find a solution that would minimize or completly remove the scaling artifacts.

Solutions

Minimizing the artifacts means we want to display the images as close their native size as possible. This minimizes the amount of scaling that occurs, which in turn minimizes the amount of artifacts introduced.

One option I briefly investigated was getting rid of sprites completely, and SVGs directly inside of Unity. SVGs, or Scalable Vector Graphics is a graphics format that defines the image as a set of shapes. This is different from standard bitmap images which define a set of pixels. By defining shapes, the image can be exported to any desired resolution.

However, Unity does not have native support for SVGs, and while I found extensions that claimed to add support for SVGs, they were either too expensive for me, or looked like they wouldn’t work.

So while using SVGs directly in Unity wouldn’t work, my final solution did involve the use of SVGs. Most of my images were already created as SVGs, then exported to PNGs for use in Unity. Extending this a bit, instead of exporting each SVG once at a single size, I would export each SVG multiple times, each at a different size.

The at runtime I would have code that examined the device’s screen and select the set of images that would minimize the amount of scaling necessary.

Generating different resolutions

As I mentioned earlier, all my images were already stored as SVGs. The simple shapes used in the game lent them well to the SVG format. To make the images I used a free program called Inkscape. When I made changes to one of the images, I would then manually export the image to a PNG for use inside of Unity.

Manually exporting each image would not scale to the number of different sizes I wanted to support. I spent an embarrassing amount of time looking for command-line tools to export SVGs before I realized (Inkscape itself had the ability to do this)[https://inkscape.org/sk/doc/inkscape-man.html].

The parameters for Inkscape are pretty straightforward. We invoke it with:

  1. The name of the SVG we want to export (-f)
  2. The path where we want to export it to (-e)
  3. The size we want the PNG to have. There are a few options here, I used either
    • -h to specify the height of the PNG, or
    • -w to specify the width of the PNG.

Only one of -h and -w is needed, the other is calculated to keep the same aspect ratio as the original image. The final invocation looked like

C:\Program File (x86)\Inkscape\Inkscape.exe -f  -e  -w 

Armed with this, I then wrote a Powershell script to iterate over all SVGs in a directory, and export each one at different resolutions. The sizes were specified in the script as an array, allowing me to easily add or remove sizes.

The final PNGs would be placed in the Unity folder, each size grouped into it’s own folder. Most of my images were 1 unit in size, so the folder name would be the pixels per unit. The folder structure looked like

Resources/
    Images/
        128/
            BlueStar.png
        256/
            BlueStar.png
        512/
            BlueStar.png
        [..other sizes..]

Once the files exist, there is one important setting that needs to be changed in the Unity import settings. The Pixels Per Unit on each image needs to match it’s size. For me, each image was designed to be one Unit, so each image under 256 would set the Pixels Per Unit to 256. This ensures the size in Unity will be the same, no matter the underlying image size.

Every time I modified one of the SVGs I would rerun the Powershell script to regenerate all the images (I meant to try and integrate this into the build process, but never got around to it… The SVGs changed infrequently enough it wasn’t too much extra work to run it manually)

Also note above that the images are in the Resources folder. This is important because we’re not referencing all these sprites from our Scenes, and need the ability to load the dynamically at runtime.

Selecting resolution at runtime

Now that all the different image sizes are built, we still need a way to tell Unity which one to use at runtime. There’s nothing built-in to do this for us, so I had to write a new behavior to handle it.

On awake, this new behavior was responsible for

  1. Checking the display and camera settings to see how many pixels were in each Unit on screen
  2. Find the closest size from the sizes available

For an orthographic camera #1 is pretty straightforward. The Camera.orthographicSize setting contains the height of the screen in units, divided by two. Thus to determine the number of pixels per unit, we just need to:

int pixelsPerUnit = (int)(Screen.height / (Camera.main.orthographicSize * 2));

Once we have that we just need to pick the closest size to pixelsPerUnit. For ease of access, I ended up hard-coding the available sizes in an array. So the behavior would find the closest value in this array. The desired sprite can then be loaded with Resources.Load(”[selected size][image.png]“).

Here’s a complete example, covering everything we talked about above

[RequireComponent(typeof(SpriteRenderer))]
public class SpriteSelectorBehavior : MonoBehaviour
{
    // These need to match the sizes generated
    public static readonly int[] AvailableSizes = new int[] { 128, 512 };

    public void Awake()
    {
        // Get the current pixels per unit
        int pixelsPerUnit = (int)(Screen.height / (Camera.main.orthographicSize * 2));

        // Find the size that is closest to the current pixels per unit
        int selectedSize = AvailableSizes[0];
        int difference = Mathf.Abs(pixelsPerUnit - AvailableSizes[0]);

        for(int i = 1; i < AvailableSizes.Length; i++)
        {
            int localDifference = Mathf.Abs(pixelsPerUnit - AvailableSizes[i]);

            if(localDifference < difference)
            {
                selectedSize = AvailableSizes[i];
                difference = localDifference;
            }
        }

        // Find the name of the sprite we are currently using
        // The initial size used from the editor doesn't matter
        string name = this.GetComponent<SpriteRenderer>().sprite.name;

        // Build the name of the sprite we want to use
        string baseName = name.Split('_')[0];
        string newName = string.Format("{1}/{0}_{1}", baseName, selectedSize);

        // Load the sprite and assign it to the SpriteRenderer
        Sprite newSprite = Resources.Load<Sprite>(newName);
        this.GetComponent<SpriteRenderer>().sprite = newSprite;
    }
}

Now I just needed to attach this Behavior to every object containing a SpriteRenderer and I was good to go.

Conclusion

With this change I got the behavior I was looking for. Having different sizes of each sprite let me select best one at runtime, minimizing the number of scaling artifacts and giving me the clean shapes I was looking for.

Another benefit of loading the sprites dynamically at runtime came later on when I decided to add support for night-mode. Originally the game supported only a white-background, but I sometimes found this too bright, and decided to add an option for a black background. To support this, I build a slightly new folder structure and generated Dark-theme versions of all the sprites:

Resources/
    Images/
        Dark/
            128/
                BlueStar.png
            256/
                BlueStar.png
            512/
                BlueStar.png
            [..other sizes..]
        Light/
            128/
                BlueStar.png
            256/
                BlueStar.png
            512/
                BlueStar.png
            [..other sizes..]

Then I just add the currently selected theme to the path I was loading the sprite from, and bam, I could load multiple themes. Hooking this behavior up to a signal that fired when the Theme Setting changed let me change the theme almost instantly when the user changed the theme.

The method presented here is not the exact version used in the game, but the underlying idea is the same. Overall though I’m very happy with how this turned out.