Friday, August 31, 2018

Unity Quick Tip: Snap to grid with arrow keys


Explanation

If you're working with pixel art in Unity, you know how difficult it can be to have a workflow that's fast and comfortable. First off, if you're not sure about the relationship between pixel art, PPU and Orthographic camera size, you should read thist post and this thread.

GameObjects Positions

As you know, GameObjects have a Transform component that determines its position in the game world, measured in Units
The problem when you're working with pixel art is that a GameObject position cannot be any number we want, it should be a multiple of our "pixel size". This means that if one pixel occupies 0.0625 units, we want every position to be a multiple of 0.0625. 
PPU stands for Pixel Per Unit, so as you probably guessed, our pixel size will be 1/PPU.

Unity Built-in snapping

You should know that Unity has a built-in function to move objects a fixed amount. It's called Unit snapping, you can customize the amount in Edit/Snap Settings and move an object by that amount by dragging it while holding Ctrl.
This helps, partially, because you can set your Snap amount to your pixel size in every axis and always move objects holding Ctrl. The problem is that if the starting position of the object is not a multiple of your pixel size, you will end up with a position thats outside the grid anyway, because snapping only moves the object by a fixed amount, it doesn't really snaps to a grid. Also, dragging objects is not precise, and it's uncomfortable.
What we want is to be able to move an object using the arrow keys, and to make its position only take values that are multiples of our pixel size.

Our own snapping

First off, calculate your pixel size. Remember that it is 1/PPU. Personally I like to set it in Snap Settings in every axis, and I'll asume that you're doing it that way too, but you could store this value anywhere you like, just have in mind that we will need to be able to access it from an Editor script.
The script itself is pretty simple so I'll make a broad explanation of what we are doing and you can check the details in the script itself. What we are doing is based on this script to use the arrow keys, so be sure to check it out!.
  • We will: 
    • Subscribe to OnSceneGUIDelegate, to be able to get the key events.
    • Check if the Move tool is currently selected (You can ignore this if you want)
    • Check the current event to see if an arrow key is pressed
    • Get our pixel size from the Snap Settings (or from anywhere you store it)
    • Check which direction we are moving
    • Get every selected object, add one pixel size to its position
    • Clamp the new position to our grid so every component is a multiple of our pixel size

Note that we are only setting X and Y, that's because as we are working with pixel art I don't need to change the Z axis, but you could do it the same way.

using System;
using System.Collections;
using UnityEditor;
using UnityEngine;

[InitializeOnLoad]
public class ArrowEditorMove {

   static ArrowEditorMove() {
       //avoid registering twice to the SceneGUI delegate
       SceneView.onSceneGUIDelegate -= OnSceneView;
       SceneView.onSceneGUIDelegate += OnSceneView;
   }

   static void OnSceneView(SceneView sceneView) {
       if (Tools.current != Tool.Move) { return; }

       Event currentEvent = Event.current;
       bool keyDown = currentEvent.isKey && currentEvent.type == EventType.KeyDown;
       bool arrowKeys = (currentEvent.modifiers == EventModifiers.None
|| currentEvent.modifiers == EventModifiers.FunctionKey); //arrow keys are function keys
       //if the event is a keyDown on an orthographic camera
       if (keyDown && arrowKeys && sceneView.camera.orthographic) {
           Vector3 movement = Vector3.zero;
           float xStep = EditorPrefs.GetFloat("MoveSnapX");
           float yStep = EditorPrefs.GetFloat("MoveSnapY");

           switch (currentEvent.keyCode) {
               case KeyCode.RightArrow:
                   movement.x = xStep;
                   break;
               case KeyCode.LeftArrow:
                   movement.x = -xStep;
                   break;
               case KeyCode.UpArrow:
                   movement.y = yStep;
                   break;
               case KeyCode.DownArrow:
                   movement.y = -yStep;
                   break;
           }

           moveSelectedObjects(movement, xStep, yStep);
       }

   }

   static void clampPosition(ref Vector3 position, float xStep, float yStep) {
       position.x = Mathf.Round(position.x / xStep) * xStep;
       position.y = Mathf.Round(position.y / yStep) * yStep;
   }

   static void moveSelectedObjects(Vector3 movement, float xStep, float yStep) {
       UnityEngine.Object[] selectedObjects = Selection.GetFiltered(typeof(GameObject),
SelectionMode.Editable | SelectionMode.ExcludePrefab);
       for (int i = 0; i < selectedObjects.Length; i++) {
           Transform objectTransform = (selectedObjects[i] as GameObject).transform;
           Undo.RecordObject(objectTransform, "Move Step"); //allow undo of the movements
           Vector3 newPosition = objectTransform.position + movement;
           clampPosition(ref newPosition, xStep, yStep);
           objectTransform.position = newPosition;
       }

       // only consume the event if there was at least one gameObject selected, otherwise the camera will move as usual
       if (selectedObjects.Length > 0) {
           Event.current.Use();
       }

   }

}

Now just put this script in a folder named Editor inside your Assets folder, if you don't have an Editor folder just create it.

Any feedback or questions? Let me know in the comments!
If you enjoyed the post, remember to check the Patreon!

You can also follow Strangewire on Facebook, TwitterTumblr