Incremental Grid Snapping completely unusable

Hi!

I’m trying to use just the incremental grid snapping, and this tool is completely unusable for modular level design purposes. Here are a few problems with it:

  1. When you scale objects with incremental scaling, the values are relative instead of fixed. Meaning, that instead of adding or removing .125, the scaling does some weird math relative to the current scale, which makes it impossible to line up objects. Moreover, when you incrementally scale a few times, the scale number in Transform gets all broken up into small decimals.

Solution: This tool should work as it does in Unreal Engine. With each use, it adds or removes the increment number. Example: If your snapping increment is 0.125, by scaling up, you should get numbers like 4.0, 4.125, 4.25, 4.5, 4.75, 5, etc…

  1. Incremental move is also broken. You use it a few times, and the transform gets broken up into decimals.

  2. Incremental rotation: This one works great and as intendend.

Does anyone know what are the plans for proper grid snapping? It would be great if Unity could just copy the functionality from Unreal Engine, it works great there and never gives broken-up decimals.

Thanks!

2 Likes
  1. Yup, this is super annoying. I have a custom script that scales an object with increments. But it’s not perfect, it is quite janky if the the object doesn’t have the same scale on all axis.

  2. For incremental move to work as expected, it has to be β€œon the grid” before you start moving it. The incremental move, just adds the increments to the existing values (it does not actually snap it on the grid). Also, if the object is rotated before moving, the incremental move cannot keep it β€œon the grid”.

HOWEVER, this is where grid snapping comes in!
You can use grid snapping to force an object to β€œbe on the grid”. It will always snap the object to the grid on the axis you move it, even if it starts with a random position with decimals.
For the grid to work, the β€œTool handle rotation” has to be set to global (the button second from left in the toolbar on the image).
8436290--1117502--upload_2022-9-13_15-37-34.png

Another thing I’d like unity to implement, is something like I have implemented here, but have it seamlessly integrated into the editor.
8436290--1117505--upload_2022-9-13_15-41-46.png
It is just a simple overlay, so I don’t have to open the grid/incremental snap windows all the time. Plus it is simpler to have plus/minus buttons for changing the grid, instead of adding values manually

1 Like

So I have the same problem, but it’s not a question of pivot or centre, or global or local. I choose a scale of 0.25 in the snapping tool. I have no parents, and it works differently depending on where you start with your cube. When I rescale it from 2 it goes to 1.5, to 1, but if I go back fromt 1, it will do 1.25 to 1.5 to 1.75 to 2.
The 0.25 corresponds to a quarter of where you started, not the number you added to the scale. And it’s very confusing.

So if I put 1 on in the scale on the snipping tool, I will just have my cube vanished.

using System;
using UnityEngine;
using UnityEditor;

#if UNITY_EDITOR

namespace MAST.Building
{
[Serializable]
public static class GridManager
{
// ───────────────────────────────────────────────────────────────
#region Variables
// ───────────────────────────────────────────────────────────────

    [SerializeField] public static bool gridExists = false;

    [SerializeField] private static GameObject gridGameObject;
    [SerializeField] private static Material gridMaterial;

    private const int DefaultGridSize = 300;
    private const int MinGridHeight = 0;
    private const int MaxGridHeight = 10;
    private const float CameraYOffset = 5f;

    // Optional: cinematic camera smoothing
    private const float CameraLerpSpeed = 0.2f;

    // Optional: toggle camera lock
    public static bool lockCameraToGrid = true;

    #endregion

    // ───────────────────────────────────────────────────────────────
    #region Grid Movement
    // ───────────────────────────────────────────────────────────────

    public static void MoveGridUp()
    {
        if (!gridExists) return;

        if (Settings.Data.gui.grid.gridHeight < MaxGridHeight)
        {
            Settings.Data.gui.grid.gridHeight += 1;
            MoveGridToNewHeight();
        }
    }

    public static void MoveGridDown()
    {
        if (!gridExists) return;

        if (Settings.Data.gui.grid.gridHeight > MinGridHeight)
        {
            Settings.Data.gui.grid.gridHeight -= 1;
            MoveGridToNewHeight();
        }
    }

    private static void MoveGridToNewHeight()
    {
        // Clamp grid height internally
        Settings.Data.gui.grid.gridHeight = Mathf.Clamp(Settings.Data.gui.grid.gridHeight, MinGridHeight, MaxGridHeight);

        float gridY = Settings.Data.gui.grid.gridHeight * Settings.Data.gui.grid.yUnitSize + Const.Grid.yOffsetToAvoidTearing;

        // Move the grid
        if (gridGameObject != null)
        {
            gridGameObject.transform.position = new Vector3(
                gridGameObject.transform.position.x,
                gridY,
                gridGameObject.transform.position.z);
        }

        // Move the main camera with offset β€” allow freedom above/below grid
        if (Camera.main != null && lockCameraToGrid)
        {
            Vector3 camPos = Camera.main.transform.position;
            Vector3 targetPos = new Vector3(camPos.x, gridY + CameraYOffset, camPos.z);

            // Optional: cinematic smoothing
            Camera.main.transform.position = Vector3.Lerp(camPos, targetPos, CameraLerpSpeed);
        }

        // Update placement height for prefab snapping
        PlacementSystem.SetPlacementHeight(gridY);
    }

    #endregion

    // ───────────────────────────────────────────────────────────────
    #region Grid Lifecycle
    // ───────────────────────────────────────────────────────────────

    public static bool DoesGridExist() => gridExists;

    public static void ChangeGridVisibility()
    {
        if (gridExists)
            CreateGrid();
        else
        {
            DestroyGrid();
            GUI.Palette.RemovePrefabSelection();
        }
    }

    public static void DestroyGrid()
    {
        foreach (GameObject go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
        {
            if (go.name == Const.Grid.defaultName)
                GameObject.DestroyImmediate(go);
        }

        UnityEditor.Tools.lockedLayers &= ~(1 << Const.Grid.gridLayer);
        gridExists = false;
    }

    public static void CreateGrid()
    {
        CreateLinkToGrid();
        UnityEditor.Tools.lockedLayers = 1 << Const.Grid.gridLayer;
        gridExists = true;
    }

    private static void CreateLinkToGrid()
    {
        gridGameObject = GameObject.Find(Const.Grid.defaultName);
        DestroyGrid();
        CreateNewGrid();
    }

    private static void CreateNewGrid()
    {
        // Set default grid size to 300 x 300
        Settings.Data.gui.grid.cellCount = DefaultGridSize;

        gridGameObject = GameObject.CreatePrimitive(PrimitiveType.Plane);
        gridGameObject.transform.position = Vector3.zero;
        gridGameObject.name = Const.Grid.defaultName;
        gridGameObject.layer = Const.Grid.gridLayer;

        MeshRenderer renderer = gridGameObject.GetComponent<MeshRenderer>();
        renderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
        renderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
        renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
        renderer.receiveShadows = false;

        if (gridMaterial == null)
            gridMaterial = LoadingHelper.GetGridMaterial();

        gridMaterial.SetColor("_Color", Settings.Data.gui.grid.tintColor);
        renderer.material = gridMaterial;

        UpdateGridSettings();
        MoveGridToNewHeight();

        gridGameObject.hideFlags = HideFlags.HideInHierarchy;
    }

    #endregion

    // ───────────────────────────────────────────────────────────────
    #region Grid Settings
    // ───────────────────────────────────────────────────────────────

    public static void UpdateGridSettings()
    {
        if (gridGameObject == null) return;

        float cellCount = Settings.Data.gui.grid.cellCount;
        gridGameObject.transform.localScale = new Vector3(
            cellCount * Settings.Data.gui.grid.xzUnitSize / 5f,
            1f,
            cellCount * Settings.Data.gui.grid.xzUnitSize / 5f);

        gridMaterial.SetTextureScale("_GridTexture", new Vector2(cellCount / 2f, cellCount / 2f));
        gridMaterial.SetColor("_Tint", Settings.Data.gui.grid.tintColor);

        MeshRenderer renderer = gridGameObject.GetComponent<MeshRenderer>();
        renderer.sharedMaterial = gridMaterial;
    }

    #endregion
}

// ───────────────────────────────────────────────────────────────
// Placement System Hook
// ───────────────────────────────────────────────────────────────
public static class PlacementSystem
{
    private static float currentPlacementHeight = 0f;

    public static void SetPlacementHeight(float height)
    {
        currentPlacementHeight = height;
    }

    public static Vector3 GetSnappedPosition(Vector3 rawPosition)
    {
        return new Vector3(
            Mathf.Round(rawPosition.x),
            currentPlacementHeight,
            Mathf.Round(rawPosition.z));
    }
}

}

#endif

i have been working a 3d level editor that has a grid base 300 x 300 with 10 floors. i can edit like on the ground plain .i was trying make the ultimate level editor that joins with multiple game ideas eg transversal with shootemup playing as one. Code might me usefill to get you started. i was using mast level editor as base ,its layout was quite good, i just updated the gridmaster script to do what i wanted.
the idea came from the game atmosphir.