Menu Close

Better grid snapping in Unity

Grid snapping in the Unity editor

Unity comes with a built in functionality for snapping objects to a grid. In many cases this is good enough, but it’s not very configurable and you might sooner or later run into limitations if you need it to do anything more complex than “snap everything to 0.1 units from the center”.

We can do better

Thankfully, Unity can be extended in a gazillion ways. The most straight forward one is to simply create a script that runs in the Update() method in the editor. You can find the complete script at the bottom.

First things first – architecture

Architecture is a very fancy word for something that will end up utilising only two simple classes. But it’s always a good idea to sit back and think about what you want to achieve, and what individual parts should make up your solution, before hacking away on the keyboard.

I wanted to be able to have multiple scripts that manage snapping within the same scene. That way, I can attach the script to a parent GameObject which only manages the snapping for the GameObjects below itself (or in another defined part of the hierarchy) and potentially end up with a few different ones in my scene that manage separate groups of GameObjects. Or I could simply add one single one to a scene level object that manages all snapping done in my entire scene, if that’s all it takes.

-> I need a GridSnapper class that can be used more than once in the same scene without messing with other instances of itself.

On the other hand, I want my GridSnapper class to handle different settings, at least to a certain degree. For example, I might have a HouseBuilder GameObject, that makes sure all its Wall prefabs snap to 0.25 units, but snaps all Floor prefabs to 1 unit. Anything else, like decorative items, needs to be left alone. In this scenario I’d rather not have to add two or three GridSnapper components to my HouseBuilder. One script should satisfy all of its snapping needs.

-> I need another class with GridSnapSettings, so I can attach an list of them to my GridSnapper class.

The script will simply run in the MonoBehaviours Update() method, which is fine, because the editor only calls it when something changes in the scene or in the hierarchy. It doesn’t run 60+ times a second in the editor. When running our game by entering play mode, the Update() method checks whether EditorApplication.isPlaying and makes sure the script doesn’t do anything. And when actually compiling the game, the entire script will be skipped because it’s inside a preprocessor directive.

The script snaps all objects that are selected in the hierarchy. Usually this means the one we’re moving around with our mouse right now. However, if we place objects all over the place, and only afterwards add a GridSnapper with settings that would include these objects, they will be left alone for the moment. We can simply select them in the hierarchy though, and the magic happens.

Snap Settings

As outlined above, I want some settings directly on the GridSnapper, but others I want to be able to specify even more granularly. Mainly, depending on where a GameObject is in the hierarchy, I want to apply the number of units it snaps to.

This will allow me to create a GameObject “Walls” located under a GameObject “House” and make sure that only the GameObjects that are placed in the hierarchy under this exact path snap to 0.25 units.

Here is an example of them being used in the editor:

The GridSnapper script affects all Objects that contain “House/Walls” in their hierarchy path, and snaps them to 0.25 units on all 3 axes. Anything else that doesn’t match any of these settings, isn’t affected at all.

But what if we want the floors to snap to whole units? We simply add another SnapSetting to our list:

Now the floors snap nicely to whole units, and the walls to quarter units (0.25). But their origin is fixed to the scenes zero coordinates (0/0/0). This means if our house is located at 0.237/0/0, our floors will not snap to the global coordinates 0.237/0/0, or 1.237/0/0, as you’d want them so they’re actually aligned to our house object. They will only snap to the global coordinates 0/0/0, 1/0/0 and so on. That’s no good.

So, we introduce the option to set a CenterTransform. If set, all affected GameObjects will be snapped relative to this center transform, instead of relative to 0/0/0.

Why didn’t I add this setting to the GridSnapSetting class? Because for my scenarios, so far, it’s good enough on the higher level. There’s nothing stopping me from moving it to the GridSnapSetting, if and when my project requires it. But I found that if certain objects need to be centered around a different object, it’s better to use a separate GridSnapper component on that particular center object to keep everything neat and tidy.

No rules without exceptions

I use this script for a few different scenarios. The above example is one of them. When building a house, I want the walls and floors to snap to a high setting (1 or 1.5 units). All other items, which in the real world are very much aligned to the walls, like e.g. cooking appliances, I want to snap, but still in a way that I can place them freely enough (e.g. 0.1 units). All other items, like furniture and decorative items, I want to place freely.

Some of these items are prefabs that are made up of other objects. In that case, I really only want the top object to snap. When I’m placing an oven, I want the oven to snap to my house, any child objects of it (like the oven door) are fine where they are (relative to the main oven).

When trying to wrap this in a rule, I came up with “I only want objects to snap if they have a renderer, and I don’t want any objects to snap if they are a child of something with a renderer” (you could also use “mesh” instead of “renderer”). This works great when working with external prefabs to build an environment that doesn’t do anything but exist as a backdrop for your scene. If you’re working with your own prefabs with their own behaviour and scripts, this isn’t a great set of rules, because one of the main guidelines when creating your own prefabs is “never use a mesh (or a renderer) on your top level”. You want your behaviour on your top level, and the meshes/renderers etc. on its children, so they can be replaced easily.

So, in those cases you want to snap your prefabs, the top level objects, even though they have no renderer or mesh attached. The child objects with the actual models (renderers and meshes) will then follow automatically. This is why the rules from above can be overridden on the GridSnapper:

Now you can activate “Snap Objects Without Renderer”, which means that even objects that don’t have a renderer (but it’s children might) will be snapped.

As an alternative, you can also explicitly force or forbid objects to snap if their own name contains a certain string. And last, but not least, you can make sure that objects aren’t snapped if they have a parent object whose name contains a certain string:

The last two options go hand in hand. Generally when I force a certain object to snap, I don’t want the children of these objects to snap as well. The children are placed perfectly in releation to the parent, only the parent needs to adhere to a grid to snap into.

Instead of checking the name of objects, we can also check if they have a certain component attached. For example, we could hard code that we want all GameObjects with a “Turret” component attached to snap, but we in turn we want children of these GameObjects with a “Turret” component never to snap. There is a (commented) example of that in the script.

That’s it?

That’s the current version of the script. Feel free to download it, add to it, modify it, print it and make a paper airplane, or use it in any other form you want. I will most likely not come back and post an updated version of this script – it’s intended as a base, that you can take and modify as suits your projects, and I will do the same with mine.

And here is the script:

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

#if UNITY_EDITOR
[ExecuteInEditMode]
public class GridSnapper : MonoBehaviour
{
    [System.Serializable]
    public class GridSnapSetting
    {
        public string PathContains = "";
        public float SnappingX = 0f;
        public float SnappingY = 0f;
        public float SnappingZ = 0f;
    }

    // Public Properties
    [Tooltip("Settings check the Path of the GameObject in the Scenes hierarchy. If you want to apply settings only to GameObjects that are located under 'Environment -> Buildings -> Walls', you can use 'Environment/Buildings/Wall'. If you want to apply them to all objects that contain 'Walls' in their hierarchy, simply use 'Walls'. Set values at which to snap to for each axis individually (0 = no snapping).")]
    [SerializeField] private GridSnapSetting[] _snapSettings;

    [Tooltip("If this is empty, 0/0/0 will be used as a center. If you set a center Transform, the affected GameObjects will be snapped relative to this object. For example, with a center game object that's at 6.25 on the x axis, and snapping for the x axis set to 0.1, affected GameObjects will snap to 6.05, 6.15, 6.25, 6.35, and so on.")]
    [SerializeField] private Transform _centerTransform = null;

    [Tooltip("If this is set to true, Objects that don't have a visible component (a renderer) will be snapped as well. By default, only objects that have a renderer are snapped.")]
    [SerializeField] private bool _snapObjectsWithoutRenderer = false;

    [Tooltip("If this is set to true, Objects that are children of GameObjects that have a renderer attached are snapped as well. By default, only the first item in the hierarchy that has a renderer is snapped.")]
    [SerializeField] private bool _snapChildrenOfObjectsWithRenderer = false;

    [Tooltip("GameObjects which have a name that contains these characters are never affected by snapping.")]
    [SerializeField] private string[] _neverSnapIfNameContains;

    [Tooltip("GameObjects which have a name that contains these characters are always affected by snapping. This is useful if you have a GameObject (for example a prefab) that you want to snap, even though it doesn't have a renderer itself.")]
    [SerializeField] private string[] _alwaysSnapIfNameContains;

    [Tooltip("GameObjects which are children of GameObjects that have a name that contains these characters are never affected by snapping. This is useful if you have a GameObject (for example a prefab) that has children that should not be snapped, even though your parent GameObject (the prefab) doesn't have a renderer but the children objects do.")]
    [SerializeField] private string[] _neverSnapIfParentNameContains;

    void Update()
    {
        if (EditorApplication.isPlaying) return;
        SnapSelectedObjects();
    }

    private void SnapSelectedObjects()
    {
        foreach (object selectedObject in Selection.objects)
        {
            GameObject selectedGameObject = selectedObject as GameObject;
            if (selectedGameObject == null) continue;

            if (ObjectShouldBeSnapped(selectedGameObject))
            {
                SnapObjectToGrid(selectedGameObject);
            }
        }
    }

    private bool ObjectShouldBeSnapped(GameObject gameObject)
    {
        // If you have a certain component that marks objects that should always snap,
        // uncomment this part and replace MyComponentThatShouldAlwaysSnap
        //if (gameObject.GetComponent<MyComponentThatShouldAlwaysSnap>() != null)
        //{
        //    return true;
        //}

        // If you have a certain component whose children should never snap,
        // uncomment this part and replace MyParentComponent.
        //foreach (MyParentComponent parentComponent in gameObject.GetComponentsInParent<MyParentComponent>(true))
        //{
        //    if (parentComponent.gameObject != gameObject)
        //    {
        //        return false;
        //    }
        //}

        // Always snap, based on name
        if (_alwaysSnapIfNameContains != null)
        {
            foreach (string name in _alwaysSnapIfNameContains)
            {
                if (gameObject.name.Contains(name))
                {
                    return true;
                }
            }
        }

        // Never snap, based on name
        if (_neverSnapIfNameContains != null)
        {
            foreach (string name in _neverSnapIfNameContains)
            {
                if (gameObject.name.Contains(name))
                {
                    return false;
                }
            }
        }

        // Never snap, based on parent(s) name
        if (_neverSnapIfParentNameContains != null)
        {
            foreach (string parentName in _neverSnapIfParentNameContains)
            {
                foreach (Transform parentTransform in gameObject.GetComponentsInParent<Transform>(true))
                {
                    if (parentTransform != gameObject.transform && parentTransform.name.Contains(parentName))
                    {
                        return false;
                    }
                }
            }
        }

        // Don't snap any objects that don't have a renderer themselves
        if (!_snapObjectsWithoutRenderer)
        {
            if (gameObject.GetComponent<Renderer>() == null)
            {
                return false;
            }
        }

        // Don't snap any objects that are children of a parent that has a renderer (only the parent needs to be snapped to the grid)
        if (!_snapChildrenOfObjectsWithRenderer)
        {
            foreach (Renderer renderer in gameObject.GetComponentsInParent<Renderer>(true))
            {
                if (renderer.gameObject != gameObject)
                {
                    return false;
                }
            }
        }

        return true;
    }

    private void SnapObjectToGrid(GameObject gameObject)
    {
        GridSnapSetting settings = GetSnapping(gameObject);
        if (settings == null) return;

        float centerX = _centerTransform == null ? 0f : _centerTransform.position.x;
        float centerY = _centerTransform == null ? 0f : _centerTransform.position.y;
        float centerZ = _centerTransform == null ? 0f : _centerTransform.position.z;

        float deltaX = gameObject.transform.position.x - centerX;
        float deltaY = gameObject.transform.position.y - centerY;
        float deltaZ = gameObject.transform.position.z - centerZ;

        if (settings.SnappingX >= Mathf.Epsilon)
        {
            float snappingX = 1 / settings.SnappingX;
            deltaX = Mathf.Round(deltaX * snappingX) / snappingX;
        }
        if (settings.SnappingY >= Mathf.Epsilon)
        {
            float snappingY = 1 / settings.SnappingY;
            deltaY = Mathf.Round(deltaY * snappingY) / snappingY;
        }
        if (settings.SnappingZ >= Mathf.Epsilon)
        {
            float snappingZ = 1 / settings.SnappingZ;
            deltaZ = Mathf.Round(deltaZ * snappingZ) / snappingZ;
        }

        gameObject.transform.position = new Vector3(centerX + deltaX, centerY + deltaY, centerZ + deltaZ);
    }

    private string GetGameObjectHierarchyPath(GameObject gameObject)
    {
        Transform transform = gameObject.transform;
        string path = transform.name;
        while (transform.parent != null)
        {
            transform = transform.parent;
            path = transform.name + "/" + path;
        }
        return path;
    }

    private GridSnapSetting GetSnapping(GameObject gameObject)
    {
        string hierarchyPath = GetGameObjectHierarchyPath(gameObject);
        foreach (GridSnapSetting snappingOverride in _snapSettings)
        {
            if (hierarchyPath.Contains(snappingOverride.PathContains))
            {
                return snappingOverride;
            }
        }

        return null;
    }

}
#endif