Just wondering if there are any guidelines for making pixel perfect retro 2d games in the new unity 4.3?
E.g, scaling/cropping and aligning a small ‘screen’ to fit a designated resolution without any blurring or artifacts?
Thanks,
Scott
Just wondering if there are any guidelines for making pixel perfect retro 2d games in the new unity 4.3?
E.g, scaling/cropping and aligning a small ‘screen’ to fit a designated resolution without any blurring or artifacts?
Thanks,
Scott
I have the same question: How to configure the main camera to have 1 pixel = 1 Unite?
It will be very helpfull to have “clean” X/Y positions like 118 pixels or unite instead of 5.9885631…
Thank you very much! ![]()
Bump on this! I am also wondering how to set up pixel perfection across all mobile devices. Should one import Retina iPad sized sprites and then have Unity somehow scale them down? Or is there / will there be an atlas-switching setup like in NGUI/2d toolkit?
Yeah I wonder this to, if there are any nice tools now in Unity to handle this.
It’s all math. Which I suck at, but I can try for ya.
Sprites have a resolution in their import settings - it defaults to 100 pixels per unit.
You know how high your screen is (Screen.Height), and an ortho camera’s “size” indicates how tall of an area it displays.
So in the game’s startup somewhere, you need to do something similar to this:
float UnitsPerPixel = 1f / 100f;
float PixelsPerUnit = 100f / 1f; // yeah, yeah, 100
Camera.main.orthographicSize =
Screen.height / 2f // ortho-size is half the screen height…
So if you have a 640x480 resolution, you’ll end up with an ortho size of 2.4 (that’s half of 480, divided by 100)
Yeah, it looks good for me: 640x960, main camera ortho size 4.8
Thank you ![]()
Not working like expected. I get one Pixel offset (tripple checked everything).
However if I move the Camera Y around 0.0001 the offset fits again. The Texture then offsets again at 0.5f, 0.25f, 0.125f… you see the Pattern =)
Has someone a solution without Offsetting my Camera on certain numbers?
Edit: On the Default Sprite Material there is a Option called Pixel Snap that solved my issue. But it still appears in uneven hight resolutions like 481. In case someone want to take a look at the built-in shader you can Download them here: http://unity3d.com/unity/download/archive/
Thanks for the feedback!
I’m working on a retro title and wanted to scale and center pixel perfect art for multiple devices.
The following code assumes you have a max vertical art size you can set and a background that can bleed off the edges,
art with filter mode ‘point’ and units to pixels = 1.
It scales the art at some integer multiple to maintain pixel fidelity
using UnityEngine;
using System.Collections;
public class ConfigCamera : MonoBehaviour {
public float maxPixelHeight = 214f;
void Awake () {
float scale = Mathf.Ceil(Screen.height / maxPixelHeight);
Camera.main.orthographicSize = Screen.height / 2f / scale;
}
}
I don’t know if it needs any slight offsets or how to modify the pixel snap in the default shader…
and if anyone has done any work on writing ‘anchor’ scripts to place ui elements as in some of the other 2d packages that would be pretty slick.
Thanks,
Scott
I’m really struggling with this also.
In Photoshop i’ve designed my level in a document size based on iPad Retina (2048x1536). I’ve imported some of my sprites into Unity and they take up a much bigger proportion of the camera’s viewable area, than they do within photoshop.
For example, one of my level props takes up approximately 17% of the document window in photoshop. With my game view set to the same dimensions as photoshop, the imported sprite takes up approximately 38% of the screen space. I’ve no idea how to get around this, and the code listed above does not seem to be working for me.
EDIT: I have attached a crude example image, which hopefully demonstrates my issue. It shows the space one of my props takes up in photoshop, and then in Unity.
EDIT2: I tried doubling the orthographic size, so my InitCamera class does the following:
amera.main.orthographicSize = (Screen.height / 2) / _pixelsToUnits * _mod; // mod is set to 2.
The above now resembles photoshop. However, i’m now unclear as to what happens when someone is playing on a non-retina iPad. If I change _mod to equal 1, the camera’s viewable area is halved, and my prop bottom-right is not on screen anymore. This must be a common problem. Can anyone recommend a solution?
Thanks for your help
Mat

Bump! I am also facing the same issue. Are there any good solutions?
Really wish I could find a good tutorial on this, this is driving me nuts coming from other SDK. Everyone I talk to just gives me a generic answer “unity doesn’t have default resolution and it will handle it automatically” but it does.
I read PC you may need to offset by .5px but osx you don’t. I assume mobile you don’t either but not 100% sure. Still trying to figure this out.
Unity developers could you please clarify this and maybe make a short tutorial as it appears quite a few of your users are struggling with this same topic…
Thanks,
Vanz
The only problem with that though, is I think you will loose the built-in draw-call batching of the current system by using multiple Sprite Materials for each texture. This greatly reduces performance, since the game needs to do a Draw Call for each material. The way the new integrated Sprite material works, is that it uses that single material for every texture you’ve set to Sprite. Meaning only 1 draw call is used for all your textures. The PixelSnap is a PRO feature, sadly =/
And it may disable some of the other performance enhancements as well, like the automatic atlas creation.
They go through some of the performance enhancements here:
When using the monkey Language there is an import I use called Autofit. It was programmed for the monkey language by DruggedBunny a.k.a James L Boyd. He made it available to everyone and released it to the Public Domain.
This code has never failed me when using Monkey for a 2d game. It is capable of zooming, autoscaling to fit device res with or without borders and has virtual controls that also work flawlessly on any res.
The code is pasted below but I have also attached the monkey file.
What brave soul is going to convert it to work with unity? ![]()
Strict
' -----------------------------------------------------------------------------
' AutoFit - Virtual Display System... [Public domain code]
' -----------------------------------------------------------------------------
' Couldn't think of a better name!
Import mojo.app
Import mojo.graphics
Import mojo.input
' Changes - made Strict and added VTouchX/Y.
' Changes - moved LOADS of locals in UpdateVirtualDisplay into fields
' and now perform very few checks/calculations on each call (most are
' only done when the device size or zoom level is changed).
' -----------------------------------------------------------------------------
' Usage. For details see function definitions...
' -----------------------------------------------------------------------------
' -----------------------------------------------------------------------------
' SetVirtualDisplay
' -----------------------------------------------------------------------------
' Call during OnCreate, passing intended width and height of game area. Design
' your game for this fixed display size and it will be scaled correctly on any
' device. You can pass no parameters for default 640 x 480 virtual device size.
' Optional zoom parameter default to 1.0.
' -----------------------------------------------------------------------------
' UpdateVirtualDisplay
' -----------------------------------------------------------------------------
' Call at start of OnRender, BEFORE ANYTHING ELSE, including Cls!
' -----------------------------------------------------------------------------
' VMouseX ()/VMouseY ()
' -----------------------------------------------------------------------------
' Call during OnUpdate (or OnRender) to get correctly translated MouseX ()/MouseY ()
' positions. By default, the results are bound to the display area within the
' borders. You can override this by passing False as an optional parameter,
' and the functions will then return values outside of the borders.
' -----------------------------------------------------------------------------
' VTouchX ()/VTouchY ()
' -----------------------------------------------------------------------------
' Call during OnUpdate (or OnRender) to get correctly translated TouchX ()/TouchY ()
' positions. By default, the results are bound to the display area within the
' borders. You can override this by passing False as an optional parameter,
' and the functions will then return values outside of the borders.
' -----------------------------------------------------------------------------
' VDeviceWidth ()/VDeviceHeight ()
' -----------------------------------------------------------------------------
' Call during OnUpdate (or OnRender) for the virtual device width/height. These
' are just the values you passed to SetVirtualDisplay.
' -----------------------------------------------------------------------------
' SetVirtualZoom
' -----------------------------------------------------------------------------
' Call in OnUpdate to set zoom level.
' -----------------------------------------------------------------------------
' AdjustVirtualZoom
' -----------------------------------------------------------------------------
' Call in OnUpdate to zoom in/out by given amount.
' -----------------------------------------------------------------------------
' GetVirtualZoom
' -----------------------------------------------------------------------------
' Call in OnUpdate or OnRender to retrieve current zoom level.
' -----------------------------------------------------------------------------
' Function definitions and parameters...
' -----------------------------------------------------------------------------
' -----------------------------------------------------------------------------
' SetVirtualDisplay: Call in OnCreate...
' -----------------------------------------------------------------------------
' Parameters: width and height of virtual game area, optional zoom...
Function SetVirtualDisplay:Int (width:Int = 640, height:Int = 480, zoom:Float = 1.0)
New VirtualDisplay (width, height, zoom)
Return 0
End
' -----------------------------------------------------------------------------
' SetVirtualZoom: Call in OnUpdate...
' -----------------------------------------------------------------------------
' Parameters: zoom level (1.0 being normal)...
Function SetVirtualZoom:Int (zoom:Float)
VirtualDisplay.Display.SetZoom zoom
Return 0
End
' -----------------------------------------------------------------------------
' AdjustVirtualZoom: Call in OnUpdate...
' -----------------------------------------------------------------------------
' Parameters: amount by which to change current zoom level. Positive values
' zoom in, negative values zoom out...
Function AdjustVirtualZoom:Int (amount:Float)
VirtualDisplay.Display.AdjustZoom amount
Return 0
End
' -----------------------------------------------------------------------------
' GetVirtualZoom: Call in OnUpdate or OnRender...
' -----------------------------------------------------------------------------
' Parameters: none...
Function GetVirtualZoom:Float ()
Return VirtualDisplay.Display.GetZoom ()
End
' -----------------------------------------------------------------------------
' UpdateVirtualDisplay: Call at start of OnRender...
' -----------------------------------------------------------------------------
' Parameters:
' Gah! Struggling to explain this! Just experiment!
' The 'zoomborders' parameter can be set to False to allow you to retain FIXED
' width/height borders for the current device size/ratio. Effectively, this
' means that as you zoom out, you can see more of the 'playfield' outside the
' virtual display, instead of having borders drawn to fill the outside area.
' See VMouseX ()/Y information for more details on how this can be used...
' The 'keepborders' parameter, if set to True, means the outer borders are
' kept no matter how ZOOMED IN the game is. Setting this to False means you
' can zoom into the game, the borders appearing to go 'outside' the screen
' as you zoom further in. You'll have to try it to get it, but it only
' affects zooming inwards.
' NB. *** TURNING 'keepborders' OFF ONLY TAKES EFFECT IF zoomborders IS
' SET TO TRUE! Borders will remain otherwise... ***
Function UpdateVirtualDisplay:Int (zoomborders:Bool = True, keepborders:Bool = True)
VirtualDisplay.Display.UpdateVirtualDisplay zoomborders, keepborders
Return 0
End
' -----------------------------------------------------------------------------
' Misc functions: Call in OnUpdate (optionally)...
' -----------------------------------------------------------------------------
' Mouse position within virtual display; the limit parameter allows you to only
' return values within the virtual display.
' Set the 'limit' parameter to False to allow returning of values outside
' the virtual display area. Combine this with ScaleVirtualDisplay's zoomborders
' parameter set to False if you want to be able to zoom way out and allow
' gameplay in the full zoomed-out area...
Function VMouseX:Float (limit:Bool = True)
Return VirtualDisplay.Display.VMouseX (limit)
End
Function VMouseY:Float (limit:Bool = True)
Return VirtualDisplay.Display.VMouseY (limit)
End
Function VTouchX:Float (index:Int = 0, limit:Bool = True)
Return VirtualDisplay.Display.VTouchX (index, limit)
End
Function VTouchY:Float (index:Int = 0, limit:Bool = True)
Return VirtualDisplay.Display.VTouchY (index, limit)
End
' Virtual display size...
Function VDeviceWidth:Float ()
Return VirtualDisplay.Display.vwidth
End
Function VDeviceHeight:Float ()
Return VirtualDisplay.Display.vheight
End
Class VirtualDisplay
Global Display:VirtualDisplay
Private
Field vwidth:Float ' Virtual width
Field vheight:Float ' Virtual height
Field device_changed:Int ' Device size changed
Field lastdevicewidth:Int ' For device change detection
Field lastdeviceheight:Int ' For device change detection
Field vratio:Float ' Virtual ratio
Field dratio:Float ' Device ratio
Field scaledw:Float ' Width of *scaled* virtual display in real pixels
Field scaledh:Float ' Width of *scaled* virtual display in real pixels
Field widthborder:Float ' Size of border at sides
Field heightborder:Float ' Size of border at top/bottom
Field sx:Float ' Scissor area
Field sy:Float ' Scissor area
Field sw:Float ' Scissor area
Field sh:Float ' Scissor area
Field realx:Float ' Width of SCALED virtual display (real pixels)
Field realy:Float ' Height of SCALED virtual display (real pixels)
Field offx:Float ' Pixels between real borders and virtual borders
Field offy:Float ' Pixels between real borders and virtual borders
Field vxoff:Float ' Offsets by which view needs to be shifted
Field vyoff:Float ' Offsets by which view needs to be shifted
Field multi:Float ' Ratio scale factor
Field vzoom:Float ' Zoom scale factor
Field zoom_changed:Int ' Zoom changed
Field lastvzoom:Float ' Zoom change detection
Field fdw:Float ' DeviceWidth () gets pre-cast to Float in UpdateVirtualDisplay
Field fdh:Float ' DeviceHeight () gets pre-cast to Float in UpdateVirtualDisplay
Public
Method New (width:Int, height:Int, zoom:Float)
' Set virtual width and height...
vwidth = width
vheight = height
vzoom = zoom
lastvzoom = vzoom + 1 ' Force zoom change detection! Best hack ever. (vzoom can be zero.)
' Store ratio...
vratio = vheight / vwidth
' Create global VirtualDisplay object...
Display = Self
End
Method GetZoom:Float ()
Return vzoom
End
Method SetZoom:Int (zoomlevel:Float)
If zoomlevel < 0.0 Then zoomlevel = 0.0
vzoom = zoomlevel
Return 0
End
Method AdjustZoom:Int (amount:Float)
vzoom = vzoom + amount
If vzoom < 0.0 Then vzoom = 0.0
Return 0
End
Method VMouseX:Float (limit:Bool)
' Position of mouse, in real pixels, from centre of screen (centre being 0)...
Local mouseoffset:Float = MouseX () - Float (DeviceWidth ()) * 0.5
' This calculates the scaled position on the virtual display. Somehow...
Local x:Float = (mouseoffset / multi) / vzoom + (VDeviceWidth () * 0.5)
' Check if mouse is to be limited to virtual display area...
If limit
Local widthlimit:Float = vwidth - 1
If x > 0
If x < widthlimit
Return x
Else
Return widthlimit
Endif
Else
Return 0
Endif
Else
Return x
Endif
Return 0
End
Method VMouseY:Float (limit:Bool)
' Position of mouse, in real pixels, from centre of screen (centre being 0)...
Local mouseoffset:Float = MouseY () - Float (DeviceHeight ()) * 0.5
' This calculates the scaled position on the virtual display. Somehow...
Local y:Float = (mouseoffset / multi) / vzoom + (VDeviceHeight () * 0.5)
' Check if mouse is to be limited to virtual display area...
If limit
Local heightlimit:Float = vheight - 1
If y > 0
If y < heightlimit
Return y
Else
Return heightlimit
Endif
Else
Return 0
Endif
Else
Return y
Endif
Return 0
End
Method VTouchX:Float (index:Int, limit:Bool)
' Position of touch, in real pixels, from centre of screen (centre being 0)...
Local touchoffset:Float = TouchX (index) - Float (DeviceWidth ()) * 0.5
' This calculates the scaled position on the virtual display. Somehow...
Local x:Float = (touchoffset / multi) / vzoom + (VDeviceWidth () * 0.5)
' Check if touches are to be limited to virtual display area...
If limit
Local widthlimit:Float = vwidth - 1
If x > 0
If x < widthlimit
Return x
Else
Return widthlimit
Endif
Else
Return 0
Endif
Else
Return x
Endif
Return 0
End
Method VTouchY:Float (index:Int, limit:Bool)
' Position of touch, in real pixels, from centre of screen (centre being 0)...
Local touchoffset:Float = TouchY (index) - Float (DeviceHeight ()) * 0.5
' This calculates the scaled position on the virtual display. Somehow...
Local y:Float = (touchoffset / multi) / vzoom + (VDeviceHeight () * 0.5)
' Check if touches are to be limited to virtual display area...
If limit
Local heightlimit:Float = vheight - 1
If y > 0
If y < heightlimit
Return y
Else
Return heightlimit
Endif
Else
Return 0
Endif
Else
Return y
Endif
Return 0
End
Method UpdateVirtualDisplay:Int (zoomborders:Bool, keepborders:Bool)
' ---------------------------------------------------------------------
' Calculate/draw borders, if any, scale, etc...
' ---------------------------------------------------------------------
' ---------------------------------------------------------------------
' Check for 'real' device resolution change...
' ---------------------------------------------------------------------
If (DeviceWidth () <> lastdevicewidth) Or (DeviceHeight () <> lastdeviceheight)
lastdevicewidth = DeviceWidth ()
lastdeviceheight = DeviceHeight ()
device_changed = True
Endif
' ---------------------------------------------------------------------
' Force re-calc if so (same for first call)...
' ---------------------------------------------------------------------
If device_changed
' Store device resolution as float values to avoid loads of casts. Doing it here as
' device resolution may potentially be changed on the fly on some platforms...
fdw = Float (DeviceWidth ())
fdh = Float (DeviceHeight ())
' Device ratio is calculated on the fly since it can change (eg. resizeable
' browser window)...
dratio = fdh / fdw
' Compare to pre-calculated virtual device ratio...
If dratio > vratio
' -----------------------------------------------------------------
' Device aspect narrower than (or same as) game aspect ratio:
' will use full width, borders above and below...
' -----------------------------------------------------------------
' Multiplier required to scale game width to device width (to be applied to height)...
multi = fdw / vwidth
' "vheight * multi" below applies width multiplier to height...
heightborder = (fdh - vheight * multi) * 0.5
widthborder = 0
Else
' -----------------------------------------------------------------
' Device aspect wider than game aspect ratio:
' will use full height, borders at sides...
' -----------------------------------------------------------------
' Multiplier required to scale game height to device height (to be applied to width)...
multi = fdh / vheight
' "vwidth * multi" below applies height multiplier to width...
widthborder = (fdw - vwidth * multi) * 0.5
heightborder = 0
Endif
Endif
' ---------------------------------------------------------------------
' Check for zoom level change...
' ---------------------------------------------------------------------
If vzoom <> lastvzoom
lastvzoom = vzoom
zoom_changed = True
Endif
' ---------------------------------------------------------------------
' Re-calc if so (and on first call), or if device size changed...
' ---------------------------------------------------------------------
If zoom_changed Or device_changed
If zoomborders
' Width/height of SCALED virtual display in real pixels...
realx = vwidth * vzoom * multi
realy = vheight * vzoom * multi
' Space in pixels between real device borders and virtual device borders...
offx = (fdw - realx) * 0.5
offy = (fdh - realy) * 0.5
If keepborders
' -----------------------------------------------------
' Calculate inner area...
' -----------------------------------------------------
If offx < widthborder
sx = widthborder
sw = fdw - widthborder * 2.0
Else
sx = offx
sw = fdw - (offx * 2.0)
Endif
If offy < heightborder
sy = heightborder
sh = fdh - heightborder * 2.0
Else
sy = offy
sh = fdh - (offy * 2.0)
Endif
Else
sx = offx
sw = fdw - (offx * 2.0)
sy = offy
sh = fdh - (offy * 2.0)
Endif
' Apply limits...
sx = Max (0.0, sx)
sy = Max (0.0, sy)
sw = Min (sw, fdw)
sh = Min (sh, fdh)
Else
' Apply limits...
sx = Max (0.0, widthborder)
sy = Max (0.0, heightborder)
sw = Min (fdw - widthborder * 2.0, fdw)
sh = Min (fdh - heightborder * 2.0, fdh)
Endif
' Width and height of *scaled* virtual display in pixels...
scaledw = (vwidth * multi * vzoom)
scaledh = (vheight * multi * vzoom)
' Find offsets by which view needs to be shifted...
vxoff = (fdw - scaledw) * 0.5
vyoff = (fdh - scaledh) * 0.5
' Ahh, good old trial and error -- I have no idea how this works!
vxoff = (vxoff / multi) / vzoom
vyoff = (vyoff / multi) / vzoom
' Reset these...
device_changed = False
zoom_changed = False
Endif
' ---------------------------------------------------------------------
' Draw borders at full device size...
' ---------------------------------------------------------------------
SetScissor 0, 0, DeviceWidth (), DeviceHeight ()
Cls 0, 0, 0
' ---------------------------------------------------------------------
' Draw inner area...
' ---------------------------------------------------------------------
SetScissor sx, sy, sw, sh
' ---------------------------------------------------------------------
' Scale everything...
' ---------------------------------------------------------------------
Scale multi * vzoom, multi * vzoom
' ---------------------------------------------------------------------
' Shift display to account for borders/zoom level...
' ---------------------------------------------------------------------
If vzoom Then Translate vxoff, vyoff
Return 0
End
End
1453488–78728–$autofit.monkey.txt (18.4 KB)
Nachtmahr solution works for me and from all the test that I’ve done, I was able to keep my batching and also able to use PixelSnap with Unity Free version. . ![]()
So here’s what have done.
1 - Add a script to your camera to automatically set the othographic size to get 1:1 pixel.
public void Awake()
{
//
camera.orthographicSize = (Screen.height / 100f / 2.0f); // 100f is the PixelPerUnit that you have set on your sprite. Default is 100.
}
2 - Create a new material, choose from the shader list the Sprites/Default shader and check the Pixel Snap option on your material.

3 - From now you need to add this material on the SpriteRenderer component of each sprites in your game. (Adding this material on all your sprites will keep batching.)

And that’s it.
P.S. As Nachtmahr said it will not work with uneven screen height like 485. But I’ve found that sometimes, if you uncheck PixelSnap when the screen height is uneven it will work. I’ve tried to uncheck PixelSnap at runtime when the screen height is uneven, but unfortunately, it looks like PixelSnap does not update the shader at runtime. ![]()
Hope it will helps ! ![]()
Cheers.
Awesome, that fixed my problem with my random tile map generator. I noticed that depending on the camera’s position the UV coordinates would sometimes be one pixel off. I think perhaps there should be a special camera option for 2D games that would automatically enable pixel snap for all sprites. If done at the camera level it could also be designed in such a way that it would work for both even and odd screen resolutions.
and this a very good tutorial about this issue
Does it make sense to use
public void Awake()
{
//
camera.orthographicSize = (Screen.height / 100f / 2.0f); // 100f is the PixelPerUnit that you have set on your sprite. Default is 100.
}
?
The reason I ask is that the Screen.height seems to pick up whatever your final resolution is when you build the game. So in the event that you’re using something like 640x480 for all your graphics, and then the user sets their resolution to 1280x960, it ends up setting your camera to display more than you would have wanted once they run the game.
I’m only a couple weeks into learning Unity, so I’m not sure what the ideal way would be to do this…
This doesn’t work. None of it works.
It gets the correct size, such as turning a 40x40 square into a 40x40 pixels displayed. However, the pixels displayed are incorrect.
This is what I call “Pixel Imperfect”
Here is an image as to what I’m talking about:
edit: this seems to be an issue with the image being 40x40 and not a Power of Two.
edit: fixed this by using TexturePacker to pack all animations of a character into a POT image, while still keeping their 40x40 size. Alternatively, one could batch resize “Canvas Size” in Photoshop from NPOT (40x40) to POT (64x64) which would also resolve Unity’s butchering of the image when it is NPOT.

