Introduction
If you are an indie developer making a retro style game using pixel art, you definitely want to maintain pixel-perfect graphics across a wide range of resolutions. There are already a lot of tutorials on the topic out there so why bother writing another one? The matter of fact is, there is no silver bullet to solve this issue. During my searches I came across many forum posts coming from people confused about PPU (Pixel per unit), reference resolutions, frame cropping and the list goes on. In a nutshell, even if you follow the strategies outlined by others you still may end up with poor results on different resolutions. In this tutorial I’ll share my experience and present my solution to this problem based on a real case scenario.
I am struggling to understand the basics behind pixel art in Unity. I understand what a unit is, and that if I draw my sprite with 16x16 pixels, I should set PPU to 16. Take stardew valley for example, in the image below you can clearly see that the player is not square. We still need to set up the camera for proper rendering of pixel-perfect graphics. Unity Technologies came up with a brilliant add-on called 2D Pixel Perfect that you can attach to your main camera. It will dynamically calculate the proper pixel size for the current resolution based on the provided values.
Table of contents
- Setting up the principles for pixel-perfect graphics
- The sprite settings inside Unity
- Setting the camera for pixel-perfect graphics
- Achieving pixel-perfect GUI graphics
1. Setting up the principles for pixel-perfect graphics
A little disclaimer before you continue on reading. I don’t claim that what I present here will work for you as you may have to experiment a bit with your artwork. Also, for the purpose of this tutorial I don’t consider having any physics engine. That being said, I will present you my particular case scenario and workflow that worked best for me. What I wanted to achieve for my latest project is the crisp, pixel-perfect graphics on a whole spectrum of devices with different resolutions. There are several characteristics of the game that I had to take into consideration:
- Whenever you will import your pixel art in Unity, the default settings will make it look blurry. In this tutorial, we will cover how to fix that.#unityWant t.
- With 2D Pixel Perfect, the 2D team at Unity makes it easy for creators to produce sharp pixel art that scales and stays clear at all sizes, on a range of devices. Read the blog Great tips and tricks for 2D games.
- The game itself is based on a grid
- Each cell is 32 x 32 pixel in size (of course you can go with different sizes but the good rule of thumb is to stick to sizes that are to the power of 2)
- The game will have pixel art GUI that needs to be in line with the rest of the graphics presented in the game
Real case specification approach
The first thing you need to do is to establish your target (reference) resolution and it is super important that you stick to it during development. Secondly, you need to define target aspect ratio of the screen. I’ll make a new canvas in Aseprite of the size of my target resolution. After that, I’ll start drawing my levels and sprites within its boundaries. By doing so, I am able to maintain the size of individual sprites in reference to the world.
In case on my latest game that is 320 x 192 pixels, which gives me 5 : 3 aspect ratio. Since I’m planning my game to be based on a grid, each size of the cell is going to be 32 pixels. That tells me that I can fit 10 sprites horizontally and 6 sprites vertically within my target resolution. The process ensures that when I export my sprites to Unity I will know how to exactly set them without losing their quality.
2. The sprite settings inside Unity
When you import your spritesheets to Unity, there are few fundamental settings you have to make.
- In the Sprites “Inspector” panel, set Pixels Per Unit (PPU) to ‘100‘. This means that 100 pixels of your original spritesheet will fit inside 1 Unity world unit (1 meter), which is exactly what we want. PPU is pixel-to-unit ratio that needs to correspond to your artwork. Of course, I could set that to 32 (since each grid cell is 32 pixels in size), but I realized that this causes issues when scaling graphics to different resolutions. However, if you intend to utilize the physics engine in a fast-paced game (such as platformer) you better set it to original size of 32. The reason for that is the fact that colliders apply default contact offset while detecting collisions, which means that the scale heavily affects contact points.
- In the sprites Inspector panel, set Filter Mode to ‘Point‘.
- In the same panel, set Compression to ‘None‘.
- If your sprite sheet is bigger than 2048 pixels then set Max Size to ‘4096‘ or higher to prevent blurriness. This may happen if, for example, your spritesheet contains lengthy animations spanning multiple frames.
Pixel-perfect pivot point
Now that we have the basic settings made, we now have to set the pivot point. It will depend on the nature of your sprites and the point around which you wish to conduct visual transformations (translation, rotation etc.).
- Open the Sprite Editor
- Whatever your sprite mode is (Single or Multiple), make sure you set the pivot point while Pivot Unit Mode is set to ‘Pixels‘. This will ensure the proper placement of pivot point inside your sprite for pixel-perfect graphics.
This concludes the basic setup. Now we can move on to more advanced settings!
The grid settings
Since the game is based on a grid where each cell occupies 32 pixels, we need to make changes to how Unity handles it.
- Go to Edit > Grid and Snap Settings… from the top bar menu.
- Under Increment Snap in the Move fields for each individual component put 1 divided by the size of your sprite’s PPU. In our case it’s going to be 1/100. Please note that you can simply put 0.01 here.
- Create a new Tilemap in your level. This will automatically create a Grid as its parent.
- In the Inspector panel of Grid, set the cell size corresponding to a cell size you defined in the very beginning. In our case that is going to be 0.32 (32 x 32 cell size). This will ensure proper translation of the pixels from Aseprite to Unity. As I already mentioned, if your game heavily depends on physics engine it is best to stick to regular scale. In such case set your grid with 1×1 cell size.
- Create a new Tile Pallete out of your sprites (Window > 2D > Tile Pallete). Simply Create New Pallete from the dropdown menu inside the panel, select all your sprites and drag & drop them inside the grid section.
You can now start creating your levels with brushes provided by tile pallete. After that we have to set up one more thing before we can enjoy our pixel perfect graphics!
3. Setting the camera for pixel-perfect graphics
We still need to set up the camera for proper rendering of pixel-perfect graphics. Unity Technologies came up with a brilliant add-on called 2D Pixel Perfect that you can attach to your main camera. It will dynamically calculate the proper pixel size for the current resolution based on the provided values. In the early days of development it was necessary to build it from source and import it to the project manually. However, now you can install it directly from package manager. Go to Window > Package manager, search for 2D Pixel Perfect package and install it for your project. After that add it to your camera as a new component.
- Set Assets Pixels Per Unit to your PPU. In our case it’s going to be 100. From multiple experiments I concluded that the best option is to make sure that this value always stays 100. Again, if you intend to heavily rely on physics engine, use the original size (in my case it’s 32).
- Set the Reference Resolution to the Target Resolution we have established in the very beginning. In our case it’s going to be 320 x 192. It’s very important that this size corresponds to the size of the canvas made in Aseprite.
- Tick the Upscale Render Texture if you wish to maintain the unaliased and unrotated pixels during the transformations. I always leave this on but it depends on what kind of style you’re going for.
- If you want to achieve smooth movement of your sprites during gameplay then leave Pixel Snapping unticked. In my case I always leave this option unticked to get the best possible movement .
- Tick the appropriate boxes if you wish to crop the viewport along the checked axis in Crop Frame section. With few experiments I concluded that the best results can be achieved by ticking the Y axis, but this is entirely up to you.
- If you select both axes in Crop Frame section then you will have an option to Stretch Fill it. I usually don’t tick it but it really depends what kind of style you’re going for.
In the end my settings look like this in the Inspector panel.
Testing and validating the setup
When you now run the game in the play mode, you will see crisp, pixel-perfect graphics in all resolutions! At this point what I like to do is to make a transparent sprite of my target resolution size (320 x 192) with 32 x 32 pixels squares in each corner and put it on top of my scene. This allows me to measure if my settings are correct.
If you enable cropping on both, X and Y axes, you will notice that no matter what resolution you will set your game to in play mode, it will always show pixel-perfect graphics with red corners within the boundaries of the viewport. Below are just few examples. Please make sure you have Maximize On Play enabled to more accurately assess the results. You can find it in the top bar of the Game viewport window.
Solving the clashing issue with Cinemachine add on (Optional)
Today, many projects made in Unity take the advantage of Cinemachine plugin. It is totally understandable as it comes with a bunch of neat features that you would otherwise had to write from scratch. However, the way this plugin works inside your projects is that it modifies the orthographic size settings of your camera on the fly. This causes the infamous clash between the 2D Pixel Perfect add on. Luckily, there is an easy solution to this problem. Simply select CinemachinePixelPerfect extension from the dropdown menu in the Inspector panel of your virtual camera.
4. Achieving pixel-perfect GUI graphics
If you are developing a game with pixel art there is a high chance you will also prepare graphics in the same style for your GUI. In order to achieve the highest quality of aesthetic quality standard you most probably would like to make it in line with your game assets. In other words, you’d like pixels occupied by GUI graphics take the same amount of space on your screen as your other sprites utilized in the game.
The secret lies in using the second camera for rendering the canvas containing all GUI elements that need to be pixel-perfect. However, there are few things that we need to be careful about if we already have one camera for rendering the game.
- The camera responsible for rendering sprites in the viewport cannot have the UI mask included in the Culling Mask list.
- On the other hand, the camera dealing with the UI display needs to have only the UI mask enabled. Furthermore, it is necessary to set Depth Only clear flag here since we don’t want to occlude anything being rendered by main camera.
- The 2D Pixel Perfect script component needs to be attached to both cameras and have exactly the same settings. In case of any discrepancies the viewport can be unpredictable.
- In the Inspector panel of the canvas set the Render Mode to Render Space – Camera and select your “UI” camera from the project assets.
- For the Canvas Scaler component, set UI Scale Mode to Constant Pixel Size, Scale Factor to 1 and Reference Pixels Per Unit to 100. If you use physics engine in your game, set this value to your original size (32 in my case).
Unity Pixel Art Blurry
Adjusting the scaling factor for different resolutions
This concludes the main setup of the pixel-perfect GUI. However, since we have set Canvas Scaler’s scaling mode so that it maintains constant pixel size we need to adjust the scaling factor whenever the resolution changes. This can be easily sorted with a simple script that sets current pixel ratio as our scaling factor. Luckily for us, we don’t even have to calculate it ourselves since the 2D Pixel Perfect module already provides us with this value.
You may have noticed that I’ve placed the execution of the adjusting logic inside the LateUpdate()
. The reason for this is that some elements of the GUI may track objects that are constantly being updated inside the Update()
function. It may happen for example in case of minimap that updates the location of the pointer representing a character on the map. By doing so we ensure the integrity and minimize the risk of running into glitches.
To demonstrate this in action, I’ve placed a simple button with up arrow in the canvas we just created. The button itself was created with Aseprite and is meant to have a pixel look. If you look closely you’ll see that its pixels perfectly align with game assets.
You can fork a minimal sample project at my github page.
Conclusion
Unity Pixel Art Shader
In this tutorial I presented my solution to maintaining pixel-perfect graphics across wide range of different resolutions. I’ve showed you my workflow and mindset that I have while creating a retro style games. The real case scenario, which was used to demonstrate it is based on a grid with cells of fixed size. After that, I’ve installed 2D Pixel Perfect plugin that is available for free via Unity package manager. It controls the correct size of screen pixels and orthographic size according to desired reference resolution. In addition I showed you how to resolve a clash with Cinemachine plugin. I concluded the tutorial with the implementation of pixel-perfect GUI based on camera render mode. The setup ensured perfect alignment of pixels rendered in the viewport during play mode.