Unity handles sprite sheets well once it’s set up, but its defaults are tuned for high-resolution art. Import a 64 × 64 pixel-art sheet with the default settings and you get a blurry, compressed, wrongly scaled sprite with thin lines bleeding in from the next frame. This guide goes through the settings that matter, in the order you’ll meet them, then builds a working 4-direction character with an Animator and a small C# controller.
Everything here works in Unity 6 and Unity 2022 LTS with the 2D template (or the 2D Sprite package installed). Where the two differ, both are shown.
The sheet we’re importing
The examples use a typical character sheet:
- frames are 64 × 64 px, no padding;
- 8 frames per row;
- 4 rows, one per direction: down, left, right, up;
- the character’s feet sit on the same line near the bottom of every frame.
That’s the layout AI Sprite Maker exports, and it’s common for hand-made sheets too. If your sheet is packed differently (trimmed frames, a JSON atlas), skip to Slicing from a JSON atlas.
Step 1: import settings
Drop the PNG into Assets/Sprites/, select it, and set these in the Inspector:
| Setting | Value | Why |
|---|---|---|
| Texture Type | Sprite (2D and UI) | Makes it a sprite asset |
| Sprite Mode | Multiple | One texture, many sprites |
| Pixels Per Unit | 64 (your frame or tile size) | 1 world unit = 1 tile; keeps physics and the camera simple |
| Mesh Type | Full Rect | Every frame gets the same quad, so the pivot doesn’t drift |
| Wrap Mode | Clamp | Stops edge pixels wrapping around |
| Filter Mode | Point (no filter) | Square, sharp pixels |
| Compression | None | Compression adds colour noise you will see at pixel scale |
| Generate Mip Maps | Off (under Advanced) | Mip maps blur sprites when the camera zooms out |
| Max Size | At least the texture’s larger side | A 4096 px sheet with Max Size 2048 is silently downscaled |
Click Apply.
Make these the defaults
Setting this on every import gets old. Select a correctly configured texture, click the preset icon (the slider icon at the top right of the Inspector) and Save current to… a preset. Then in Project Settings → Preset Manager add it as the default for TextureImporter, optionally with a filter like glob:"Assets/Sprites/**".
Which Pixels Per Unit?
PPU decides how big a sprite is in world units. The rule that saves the most pain: use the same PPU for every sprite in the game, and set it to your tile size. If tiles are 16 px and characters are drawn on 32 × 32 frames, both get PPU 16, and the character is 2 × 2 tiles. Mixing PPUs means mixing pixel sizes on screen, which looks wrong immediately.
Step 2: slice in the Sprite Editor
Click Open Sprite Editor in the Inspector (if the button is missing, install 2D Sprite from the Package Manager).
- Click Slice at the top left.
- Type:
Grid By Cell Size. - Pixel Size: X
64, Y64. - Offset and Padding:
0unless your sheet has gaps between frames. - Pivot:
Bottom Centerfor characters. UseCenterfor items and effects. - Click Slice, then Apply at the top right.
Grid By Cell Count (columns × rows) works too if you’d rather type 8 and 4.
Unity names the sprites hero_0, hero_1, … left to right, top to bottom. With 8 columns, row r column c is hero_{r*8 + c}: the down row is 0–7, left 8–15, right 16–23, up 24–31.
Why Bottom Center matters
The pivot is where the sprite’s transform position sits. With a centre pivot, an attack frame that’s taller than the idle frame moves the character’s feet when it plays. With Bottom Center on frames whose feet share a baseline, the character stays planted, Y-sorting works, and a collider at the feet lines up with every animation.
After slicing, the Project window shows one sprite per cell. For a single-row strip like this one, hero_0 to hero_7 are simply left to right:
Step 3: make the animation clips
The fast way: in the Project window, expand the sheet, select frames hero_0 to hero_7 (click the first, Shift-click the last), and drag them into the Hierarchy (or Scene). Unity asks where to save the clip; call it Walk_Down.anim. It also creates a GameObject with a SpriteRenderer, an Animator and an Animator Controller named after the first sprite.
For the other rows, don’t drag them into the scene again; that creates more GameObjects and controllers. Instead:
- Select the GameObject, open Window → Animation → Animation.
- Use the clip dropdown → Create New Clip…, name it
Walk_Left. - Drag frames 8–15 onto the timeline.
Repeat for right and up, and for any other sheets (idle, attack).
Frame rate
New clips default to 60 samples per second, so 8 dragged frames play in about 0.13 seconds. Set the clip’s sample rate to your sheet’s fps (8–12 for walks):
- Unity 2022 and 6: in the Animation window, open the options menu (the ⋮ at the top right) and enable Show Sample Rate, then type
10in the Samples field. - Or select keys and spread them so each frame lasts 1/fps seconds.
If your sheet came with JSON metadata, the fps field tells you the intended rate.
Looping
Select the .anim asset and tick Loop Time for walks, runs and idles. Untick it for one-shot clips like attack, hurt and death.
Step 4: the Animator
Open Window → Animation → Animator with the character selected. You’ll see your clips as states. For a top-down character, a Blend Tree keeps the graph tidy:
- Right-click → Create State → From New Blend Tree, name it
Walk. - Double-click it. Set Blend Type to
2D Simple Directional. - Add parameters (left panel, Parameters tab):
MoveX(Float),MoveY(Float),Speed(Float). - Set the blend tree’s parameters to
MoveXandMoveY. - Add four motions with these positions:
| Motion | Pos X | Pos Y |
|---|---|---|
| Walk_Down | 0 | -1 |
| Walk_Left | -1 | 0 |
| Walk_Right | 1 | 0 |
| Walk_Up | 0 | 1 |
Do the same for an Idle blend tree. Then connect Idle → Walk with condition Speed > 0.1, and Walk → Idle with Speed < 0.1.
For pixel art, select each transition and set:
- Has Exit Time: off, so the switch happens immediately;
- Transition Duration:
0. Sprite animation can’t cross-fade; a non-zero duration just delays the change.
Blend trees and sprites
A 2D blend tree between sprite clips doesn’t blend images; it picks the clip whose position is closest to the input. That’s exactly what you want: diagonal input snaps to the nearest of the four directions. For 8 directions, add four more motions at (±0.7, ±0.7).
Step 5: drive it from C#
Attach this to the character along with a Rigidbody2D (Body Type Dynamic, Gravity Scale 0 for top-down, Interpolate Interpolate) and a collider.
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D), typeof(Animator))]
public class TopDownMover : MonoBehaviour
{
[SerializeField] float speed = 3f; // world units (tiles) per second
Rigidbody2D body;
Animator anim;
Vector2 input;
Vector2 facing = Vector2.down;
static readonly int MoveX = Animator.StringToHash("MoveX");
static readonly int MoveY = Animator.StringToHash("MoveY");
static readonly int Speed = Animator.StringToHash("Speed");
void Awake()
{
body = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
void Update()
{
input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
if (input.sqrMagnitude > 1f) input.Normalize();
if (input.sqrMagnitude > 0.01f) facing = input;
// Keep the last direction when idle so the character doesn't snap to "down".
anim.SetFloat(MoveX, facing.x);
anim.SetFloat(MoveY, facing.y);
anim.SetFloat(Speed, input.magnitude);
}
void FixedUpdate()
{
// Unity 6: linearVelocity. Unity 2022: use body.velocity instead.
body.linearVelocity = input * speed;
}
}
Input.GetAxisRaw uses the old Input Manager. If your project uses only the new Input System, read a Vector2 from an InputAction instead; the animator code stays the same. In Unity 6, Rigidbody2D.velocity still compiles but is marked obsolete; linearVelocity is the new name.
Side-scrollers: flip instead of a second row
For a platformer you usually have one direction of art. Flip it with the SpriteRenderer:
var sr = GetComponent<SpriteRenderer>();
if (input.x != 0) sr.flipX = input.x < 0;
flipX mirrors around the pivot, which is another reason for a bottom-centre pivot. Avoid flipping with a negative transform.localScale.x if the object has child colliders or effects you don’t want mirrored.
One-shot animations
For an attack, add a Trigger parameter Attack, a transition from Any State to Attack with that trigger (Has Exit Time off), and a transition from Attack back to Idle with Has Exit Time on and Exit Time 1, so it returns when the clip ends:
static readonly int Attack = Animator.StringToHash("Attack");
void Update()
{
// ...movement code...
if (Input.GetKeyDown(KeyCode.Space)) anim.SetTrigger(Attack);
}
To trigger damage on a specific frame, select the attack clip in the Animation window, move to that frame, and click Add Event. Point it at a public method on the character.
Step 6: pixel-perfect rendering
Point filtering keeps pixels sharp, but at non-integer zoom some pixels are drawn wider than others, and moving sprites shimmer. The Pixel Perfect Camera fixes that.
- URP (2D Renderer): add Pixel Perfect Camera (from
UnityEngine.Rendering.Universal) to the Main Camera. - Built-in pipeline: install the 2D Pixel Perfect package and add its Pixel Perfect Camera component.
Settings:
| Setting | Value |
|---|---|
| Assets Pixels Per Unit | Same PPU as your sprites (64 in this example, or your tile size) |
| Reference Resolution | Your game’s native resolution, e.g. 480 × 270 or 320 × 180 |
| Crop Frame | None, or Windowbox/Letterbox if you want exact integer scaling with borders |
| Grid Snapping | Pixel Snapping (or Upscale Render Texture for a stricter retro look) |
With a 320 × 180 reference resolution, a 1920 × 1080 screen scales by exactly 6. A 1366 × 768 laptop can’t scale 320 × 180 by a whole number; Crop Frame decides whether you get borders or a slightly cropped view.
Slicing from a JSON atlas
Packed atlases (trimmed frames, rotated frames, different sizes) can’t be sliced with a grid. Your options:
- TexturePacker’s importer for Unity reads TexturePacker data files and creates sprites with the right rects and pivots. Simple if you already own TexturePacker.
- Re-pack as a grid. Our free Sprite Sheet Slicer cuts any sheet into frames, and the Sprite Sheet Maker packs them back into an even grid with no trimming. That’s often the least effort for a handful of sheets.
- An editor script using the 2D Sprite package’s data provider. Minimal version for a grid with known frame size, which you can adapt to read rects from JSON:
// Assets/Editor/GridSlicer.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
public static class GridSlicer
{
[MenuItem("Assets/Slice As 64x64 Grid (bottom-center pivot)")]
static void Slice()
{
var tex = Selection.activeObject as Texture2D;
if (tex == null) return;
var path = AssetDatabase.GetAssetPath(tex);
var importer = (TextureImporter)AssetImporter.GetAtPath(path);
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.filterMode = FilterMode.Point;
importer.textureCompression = TextureImporterCompression.Uncompressed;
importer.SaveAndReimport();
var factory = new SpriteDataProviderFactories();
factory.Init();
var provider = factory.GetSpriteEditorDataProviderFromObject(importer);
provider.InitSpriteEditorDataProvider();
const int size = 64;
var rects = new List<SpriteRect>();
int cols = tex.width / size, rows = tex.height / size, i = 0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
rects.Add(new SpriteRect
{
name = $"{tex.name}_{i++}",
// Unity's rect origin is bottom-left, so row 0 (top of the image) has the highest y.
rect = new Rect(c * size, tex.height - (r + 1) * size, size, size),
alignment = SpriteAlignment.BottomCenter,
pivot = new Vector2(0.5f, 0f),
spriteID = GUID.Generate(),
});
provider.SetSpriteRects(rects.ToArray());
provider.Apply();
importer.SaveAndReimport();
}
}
Right-click a texture → Slice As 64x64 Grid and it’s done, with the same naming order as the Sprite Editor. Note the y flip: Unity measures sprite rects from the bottom-left of the texture, while most JSON atlases measure from the top-left. Forgetting that is the usual reason script-sliced sprites come out as the wrong frames.
Troubleshooting
| Problem | Fix |
|---|---|
| Sprite is blurry | Filter Mode Point, Compression None, mip maps off |
| Thin lines from the neighbouring frame at the edges | Point filtering plus Pixel Perfect Camera usually fixes it. If you pack sheets yourself, add 1–2 px padding (or extrude) between frames. With a Sprite Atlas, set its padding to 2 or more |
| Colours look slightly off or noisy | Compression is still on, or the platform override (Android/iOS tab) has its own compression |
| Animation plays far too fast | The clip is at 60 samples per second; set it to the sheet’s fps |
| Character hops when switching animations | Pivots differ between clips. Slice everything with Bottom Center and keep frames the same size |
| Wrong frames in a script-sliced sheet | You forgot that sprite rects start at the bottom-left |
| Character slides on the ground | Move speed doesn’t match the stride. Adjust speed until the planted foot stays still |
| Sprite is huge or tiny | Inconsistent Pixels Per Unit. Use one PPU everywhere |
Getting sheets to import
If you need characters to try this with, AI Sprite Maker’s sprite animation tool outputs exactly the layout used here: equal frames, no padding, one row per direction, feet on a shared baseline. A library animation costs 10 credits per direction, and the free daily credits cover a test run. Export the Frames ZIP if you prefer to drag individual PNGs into Unity, or the PNG sheet and slice it as above. For other engines, see Exporting sprites to your engine.
If you’d rather skip the manual steps, our free Unity package applies the import settings from step 1, slices from the sheet’s frame data, and builds the AnimationClips and Animator Controller for you. It also slices any PNG that has an Aseprite or TexturePacker JSON next to it.