How to Snap to Custom Grid shape

Hi, i finished the Glitch Garden course and i was tinkering and looking at ways to do some new stuff before going to the next step of the course.
I stepped into a problem : i made a simple training level with only 2 lanes and i made them a little bit rectangular in shape rather than Squares, but due to the way we implemented the snap to grid mechanic i can’t figure out a way to snap my Defenders into a grid that is not “regular” in shape or simply not like the basic unity Grid size-

At first my grid was not super precise as the one Rick used so i implemented 2 offset variables to give a little bit of manualty to the snapping and it worked so i tried to modify a bit the parameters of the code we used in the course but it didn’t worked and I only encounter more and more bugs ahahah i really need help

i’ll show an example


this is the grid i used for the course, which is exactly 111x111 squares so it was perfectly alligned

but what if i want to allign my defenders to something like this

 private Vector2 GetSquareClicked()
    {
        Vector2 clickPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
       // Debug.Log(clickPos);
        Vector2 worldPos = Camera.main.ScreenToWorldPoint(clickPos);
       // Debug.Log(worldPos);
        Vector2 gridPos = SnapToGrid(worldPos);
        return gridPos;
    }

    private Vector2 SnapToGrid(Vector2 rawworldPos)
    {
        float newX = Mathf.Floor(rawworldPos.x+SnapXOffset);
        float newY = Mathf.Floor(rawworldPos.y+SnapYOffset);

        return new Vector2(newX,newY);
    }

PS: ( i do not have problems in the number of rows or columns i already figured out how to prevent the player from placing objects outside of a collider i placed above the grid)

Hi Shinos,

If the collider matches exactly the area on which the player is supposed to click, you could calculate the grid from the dimensions of the collider. For instance, if the collider is 2.5f high, each lane would be (2.5f / 2) high. Log the bounds values of the Collider2D object into your console to see if they make sense to you.

Did this help?


See also:

1 Like

Really interesting, getting the collider size could be a great idea but how can i get the center of each rectangle on the grid ? do i have to do an array storing the center coordinates point of each ? and also if i am using world coordinates how can i snap the defender based on the dimensions i get from this ? mhhh i feel this is the right path but i do not quite understand how to achieve it

If you know the height of your collider and the number of rows, you know that the height of each row is (height of the collider / number of rows). The centre of a tile on the y-axis is (height of the collider / number of rows) divided by 2.

And it works the same way for the width.

Yes how to get the center of the rectangles was a thing that i got also at first by myself , what i was trying to ask is : how can i use these information? i’ll get this dimensional information but will they be based on the local coordinates of the collider or will they be global? and also how can i make them to be useful when i click on the place i want to snap on ? this is confusing me a lot.

I’ll explain myself better

 private Vector2 GetSquareClicked()
    {
        Vector2 clickPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
       // Debug.Log(clickPos);
        Vector2 worldPos = Camera.main.ScreenToWorldPoint(clickPos);
       // Debug.Log(worldPos);
        Vector2 gridPos = SnapToGrid(worldPos);
        return gridPos;
    }

    private Vector2 SnapToGrid(Vector2 rawworldPos)
    {
        float newX = Mathf.Floor(rawworldPos.x+SnapXOffset);
        float newY = Mathf.Floor(rawworldPos.y+SnapYOffset);

        return new Vector2(newX,newY);
    }

as you can see from this code I simply get the position of the mouse on screen and then i floor it to match it with the (0,0) ecc of unity grid, so even if i get the “dimensional” information of the new grid and rectangles i did how can i get it to be useful

Maybe there is a way to get the coordinates of a mouse input based and local to a collider box? if so i feel really dumb
i already used a way to check if the mouse was inside the box before , to avoid spawning out of bounds, maybe i could get something by this

  public bool CheckIfInCollisionBounds(Vector3 mousePos)
    {  
      return (collider.bounds.Contains(mousePos)) ; 
            
    }

I would treat the collider as a separate space and add an offset depending on the position of the collider in the World Space. This way, you won’t have to do any complex maths for each tile. The local position of the first tile would always be at, for example, (0, 0), no matter where the actual game object is. (0, 0) + offset is easier to grasp than “random” coordinates. The computer does not care, though.

Since you mentioned the mouse coordinates, you could get the mouse position and convert it to World Space coordinates. Then you take the converted mouse space coordinates subtract the offset from them, then round those new coordinates and add the offset again.

1 Like

Thank you Nina, i understood what you’re saying and i’ll try even if i do not know if i will succed in it hahahah i’ll update soon with my results

The only problem i’m dealing with is that even doing so with the offset when i click the system still recognizes the input coordinates based on a “square” shape units
image

even doing the offset thing, what can i do to “trick” the mouse click input?

image

this is the whole picture to not confuse you hahaha

i really can’t get out of this , it is slowly driving me insane :scream:

i’ll explain myself better, i managed to get the offset working right but i get this
image

i get it to be 0,0 from where i want but it still gets it’s “boundaries” measures by Unity squares, so when i click on it gives a 1,0 at the position of a “square” which i drew green here

by luck the height is exact, but how can i get the units to be based on my rectangles rather than by this squares

i think i’m close to the solution and maybe it’s a stupid thing i’m asking but pls help :c

i would only need to extend the unit by this little offset and it would all work :sob:
image
but how

I think these will work for you, as far as converting the coordinates back and forth from your grid. When you click a space, you can get the world position if the space you click and convert it to the grid position.

To be honest I’m not sure what there is to do with that information other than convert it back into a world position. But when you convert it back, instead of getting the world position of the space you originally clicked on, you will be getting the world position at the center of the closest grid cell.

(This is not functional, tested code. It may need a little cleaning up.)

float gridRows = 2f;
float gridColumns = 7f;
float gridCellHeight;
float gridCellWeidth;
[SerializeField] Collider2D myCollider;
   gridCellHeight = myCollider.boundaries.y / gridRows; 
   gridCellWidth = myCollider.boundaries.x / gridColumns; 
public Vector2 (WorldPosToGridPos (Vector3 worldPos) 
{
   Vector3 localPos = myCollider.transform.position - worldPos;
   Vector2 gridCoord.
   gridCoord.x = Mathf.Round(localPos.x / gridCellWidth) * gridCellWidth; 
   gridCoord.y = Mathf.Round(localPos.y / gridCellHeight) * gridCellHeight;
  
   return gridCoord;
}
public Vector3 (GridPosToWorldPos (Vector2 gridPos) 
{
    Vector3 worldPos = myCollider.transform.position;
    worldPos.x += (gridCellWidth * gridPos.x); 
    worldPos.y += (gridCellHeight * gridPos.y);
    return worldPos; 
}

Hi, thank you for you time in giving a shot to solve my problem.
I’m was trying to understand the code you posted but there were some things i couldn’t get

this is what i had till now (i cleaned the code from unnecessary functions etc…)

  private void ColliderInfo()
    {
        size = collider.bounds.size; 
        offset = transform.position;
        Debug.Log(size);


    }
        private Vector2 SetPositionInGrid()
        {

            Vector2 clickPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
          
            
            Vector2 worldPos = Camera.main.ScreenToWorldPoint(clickPos);   
            worldPos = worldPos - offset;


            Vector2 roundedgridPos = RoundToGridUnit(worldPos);
            return roundedgridPos;

        }


private Vector2 RoundToGridUnit(Vector2 rawworldPos)
{
    float newX = Mathf.Floor(rawworldPos.x+SnapXOffset); //the snap offset was to adjust little differences when i had a scenario with squares because i drew the tiles not precisely
    float newY = Mathf.Floor(rawworldPos.y+SnapYOffset);

    return new Vector2(newX,newY);
}

and then a simple instantiate function where i’ll put the right position

  private void SpawnDefender(Vector2 roundedPos)
    {
        Defender newDefender = Instantiate(Defender, roundedPos, Quaternion.identity) as Defender;
}

i don’t understand why you did the 2 functions World to Grid and Grid to World and how would they work, could you explain your process?

The WorldPosToGridPos() seems pretty similiar to what i did but didn’t understand why you go back and forth with the GridPosToWorldPos() and how you utilize the columns and row in the math function calculation
(why divide and multiply? like if i have a local pos of 2,6 if i multiply it by * 2 rows = 5.2 ) rounded is 5 * 2 is equal to 10 , what am i missing, why should it be needed)

maybe after this i should convert again to World position with the GridPosToWorldPos() ?

maybe i’m just tired but please have patience

So I like the SetPositionInGrid(), but I’m not too crazy about the RoundToGridUnit method.

Let’s put some numbers here and pretend the collider size is 20 units across and 3 units large. Let’s also say the collider’s offset is (10, 1). If you were to click on a spot that had a world coordinate of 17,5, 3.25, and you subtracted the offset, you’d have a coordinate of (7.5, 2.5).

Your “Snap To Grid” method simply takes these numbers and rounds them down to (7, 2).
As you noted, that puts it evenly on Unity’s grid system, but not on yours.

If you want it to convert to your grid system, you have to take into account the total size of your grid and the size of each cell, so that you can determine which cell was clicked on. And this may not necessarily line up into perfect Unity units, so rounding off the numbers is not the answer.

(Following this up with another post in a bit.)

Okay, let’s go back and look at this:

Let’s put some numbers here and pretend the collider size is 20 units across and 3 units large. Let’s also say the collider’s offset is (10, 1). If you were to click on a spot that had a world coordinate of 17,5, 3.25, and you subtracted the offset, you’d have a coordinate of (7.5, 2.5).

As a quick side example: To get a vector to someplace, you subtract it’s position from your current position. So, to go to a tree at tree.transform.position, my character would go to tree.transform.position - character.transform.position. This gets a vector from the character to the tree. From the character’s perspective, the character is at 0,0 and the tree’s coordinates are this vector.

So if the character is at (3, 7) and the tree is at (5, 9), then the vector between them is (2, 2). That is, from the character’s position, the tree is located two units high and two units to the right.

Now, going back to the grid, we have a position (the mouse click), and we are subtracting the position of the collider. Essentially, this is (click position - collider’s position), or a vector from the collider to the mouse click. If the mouse click is at 17,5 and the collider is located at 10,1, then when we subtract the collider’s position (the offset), we’re left with (7.5, 2.5). This is, from the collider’s perspective, the click is located 7.5 units to the right of the collider’s center and and 2.5 units higher than its center. These are essential the mouse click’s position at coordinates local to the collider.

That’s useful to us because we have a bunch of other coordinates local to the collider already, most notably it’s height and width. And if we know the height and width and number of rows and columns, then we also have the height and width of each grid cell.

Even just judging on the x-axis for the moment, there are (nearly) infinite number of spaces to click on from the left side of the collider to the right side of the collider. But there’s only seven spaces, seven distinct points that we care about. These are the center of our grid cells. Since they are evenly spaced throughout the collider’s width, we can determine what these points are by dividing the total width (20) by the number of cells along that axis (7). So basically, with any mouse result we get, we want to round to the nearest (20 / 7 = ) 2.857.

If we clicked on local x-axis position of 7.5, we can see that does not land on a perfect multiple of 2.857, so it has not landed perfectly on the grid. We can adjust this number by dividing by the width of a grid width (to see how many grid widths far away it is from the center) and get a result of 2.62513. That is, it’s gone exactly 2 full grid spaces to the right, and about 63% of another grid space. So when we “snap to grid”, we should be snapping to the 3rd grid space. So we round this 2.62513 result to 3, and then we multiply by the grid space.

If our grid coordinate at 0,0 is also the center of a cell grid at 0,0, then the center of the cell grid to the right is one full cell grid width away from the center. That is if the cell has a width of 2.857, then the grid position one space to the right should have an x coordinate of 2.857. Two spaces to the right should have a local coordinate of 5.714. Three to the right should have 8.571. That’s why we multiply the cell width when we convert back from grid coordinates to world coordinates. Since the number has been rounded to the nearest whole number, when we convert back, we should land perfectly in the middle of one of these spots.

All of this assumes that 0,0 – the center of the collider – is also the center of a grid space, which does not appear to be true. You’ve already noticed the results – hence the need for a SnapXOffset and SnapYOffset. You need some sort of offset from the center of the collider to the center of a grid space.


The collider stuff kind of confuses things and adds some extra steps, but it’s useful for converting the picture into numerical data. My first thought would have been to use transforms, but it’s the same overall result. You need some way to get the width of a cell (cell size x), the height of a cell (cell size y), and the offset from 0,0 to the center of a cell. As long as you have those three pieces of information, you can make a grid by:

  1. Take a world position, and add the offset to it.
    (This makes it a local position based on your starting cell.)
  2. Divide the x and y of your local position by the cell size x and y.
    (This tells you far away the position is, in number of cells, in each axis)
  3. Round the x and y to whole numbers.
    (This rounds the “distance in cells” to the nearest whole amount of cells, in each axis.). These are effectively your grid coordinates. (The number of cells horizontally and vertically from the starting grid space.)
  4. Multiply the distance by the cell size x and y.
    (This converts the “grid coordinates” or “distance in cells from the center” back to the local coordinates of the starting grid cell. Remember these are not derived from the initial world position coordinates, but rather the coordinates of the center of the nearest cell.)
  5. Subtract the original offset.
    (Converting back from local coordinates of the starting cell to Unity world coordinates (or distance from world origin 0,0). )

Thank you for all the effort you put to help me, i’ll try to understand/use this :smiley: i’ll update with further information, hoping i will solve it.

The only thing that leaves me confused is that there isn’t a simple way to adjust the dimension of the basic unity units system, if only i could just stretch that square
image

also i think that in finding the center of the grid tiles we should divide by 2.
the mouse input still concerns me, when i click if i cannot change the unity metric system it will always count as 0 to 1 or 1 to 2 in a square perimeter when clicking :thinking: i’ll try to read better your response

The physics simulation works with the grid as well. If we were able to modify the grid, the physics simulation would become way more complex. Since your idea is something one would not need in each game, it is more convenient to write a custom solution for it than to have the creators of Unity make the engine even more complex.

By the way, you could create a simple test scene to make your idea work. Set the collider game object to (0, 0, 0). Once your idea works, you could move the collider object. And once this works, too, you implement it in the actual scene. Approach this problem step by step, then you’ll eventually get to the solution. :slight_smile:

And don’t hesitate to use a pen and a paper to visualise your ideas and to do the maths. You are not obliged to stare at your screen until things starting to make sense. :wink:

i have to say i’m trying to implement what you tried to explain to me but i’m going through several issues, one of em is the center "assuming that 0,0 the center of the collider – is also the center of a grid space " i made it to be that the 0,0 is at the bottom left so i don’t know if it radically changes something but however i did a video to show what i am getting

this is what i used from the code u posted

  private void OnMouseDown()
    {
        SpawnDefender(SetPositionInGrid());
      
    }
private void ColliderInfo ()
{
    size = collider.bounds.size; 
    offset = transform.position;
    Debug.Log(size);
    gridCellHeight = collider.size.y / gridRows;
    gridCellWidth = collider.size.x / gridColumns;


}

here i switched it from your "offset- worldPo"s to worldPos-offset it gave me negative results instead

    public Vector2 WorldPosToGridPos (Vector2 worldPos)
 {
        Vector2 localPos = worldPos - offset ;  
        Vector2 gridCoord;
        gridCoord.x = Mathf.Round(localPos.x / gridCellWidth) * gridCellWidth;
        gridCoord.y = Mathf.Round(localPos.y / gridCellHeight) * gridCellHeight;

        return gridCoord;
    }

  public Vector2 GridPosToWorldPos (Vector2 gridPos)
    {
        Vector2 worldPos = collider.transform.position;
        worldPos.x += (gridCellWidth * gridPos.x);
        worldPos.y += (gridCellHeight * gridPos.y);
        return worldPos;
    }

i have to be honest i don’t still really know for sure how i should have used GridPosToWorldPos so i assumed it would have been a final “conversion” and i did so

    public Vector2 SetPositionInGrid(Vector2 gridPos)
    {
        Vector2 clickPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
        Vector2 worldPos = Camera.main.ScreenToWorldPoint(clickPos);
        Vector2 GridCord= WorldPosToGridPos(worldPos);

        Debug.Log(GridPosToWorldPos(GridCord));

        Vector2  GridCellCenter= GridPosToWorldPos(GridCord);

  
        return GridCellCenter;
       
    }

but all of this does not work, i’ll post a video to show my behavior

Ok so in the video i show a first attempt by leaving the object collider at the imaginary “0,0” of my grid and a second attempt by switching the 0,0 of the object it at the center of the background grid as i think you suggested , in the second attempt it seems to work slightly better but other than an increased offset in placing when going forward with the cells( like the first 3 rows seems to almost work) there is also this issue where sometimes when clicking on a rectangle there are like 2 (or 3/4 in the first attempt) different placing outcomes
this issue is like the one i had before when at click it would detect the unity metrics and at the end of the square it would go to the next integer
image

but here somehow (i still didn’t quite understood why) it has a more rectangular shape(?) Which is good but it is like i didn’t placed it right to match my pixel grid mh
i don’t know if what i said made sense to you, let me know

you can see in the debug console also the log of the coordinates

So HAPPY UPDATES :sob: i got it to work! (well…almost) and even if i don’t fully know how… :thinking:
i’ll explain myself better, at first i tried to remove the random implementation of the re-conversion function you gave me (GridPosToWorldPos()) the one i which i put before in SetPositionInGrid(),
and it began to work “BETTER” but this was my outcome:

the position of the Instantiated Objects was sligthly off in the x axis and y axis but it was working right, they were distanciated in the correct way, just had to adjust them, so i added a simple offset variable (i reused the one i did at the beginning) and adjusting it by the unity editor i found out they were off by a
image


YAY!
and doing this it works! BUUT there are 3 MAJOR ISSUES :sob: :sob: :sob: :sob: :sob: :sob::sob:

Number 1: why it works only by manually adjusting it ? i don’t understand why they are spawned with the right distance between each other but their position is off by a “-5” in the x axis and a “0.4” in the y axis from the collider, why? there is something in the positioning algoritm we made that i still don’t understand, maybe this is an unexpected behavior and it should be working even without me manually adjusting the offset

Number 2: So i by doing all of this i COMPLETELY didn’t used the GridPosToWorldPos function , when i try to implement it all of this goes completely messed up, so my question is why do i need it, all the worldPos.x += (gridCellWidth * gridPos.x); etc… maybe again i come to a solution by doing a wrong path ahahahah

NUMBER 3: the only issue i have here that is a bug and not a “knowledge” problem, is that sometimes when i click almost at the bottom half of the collider it detects that the Y axis is “-0.4” and it gives me A THIRD LINE of instance, i don’t understand why this is happening, at the top it doesn’t happen, maybe it is something related to the round and multiplication of a really small number (?) i need to fix this

image

this is how my code looks like now

  private void OnMouseDown()
    {
        SpawnDefender(SetPositionInGrid2());
      
    }
   
 private Vector2 SetPositionInGrid2()
    {
        Vector2 clickPos = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
        Debug.Log(clickPos);

        Vector2 worldPos = Camera.main.ScreenToWorldPoint(clickPos);
        Debug.Log(worldPos);

        Vector2 GridCord= WorldPosToGridPos(worldPos);
        Debug.Log(GridCord);
               
        return GridCord;
       

    }
 private void ColliderInfo()
    {
        size = collider.bounds.size; //dimensioni collider
        offset = collider.transform.position;
        Debug.Log(size);
        Debug.Log(offset);
        gridCellHeight = collider.size.y / gridRows;
        gridCellWidth = collider.size.x / gridColumns;


    }
  public Vector2 WorldPosToGridPos (Vector2 worldPos)
 {
        Vector2 localPos = worldPos - offset ;
        Vector2 gridCoord;
        gridCoord.x = Mathf.Round(localPos.x / gridCellWidth) * gridCellWidth;
        gridCoord.y = Mathf.Round(localPos.y / gridCellHeight) * gridCellHeight;
        gridCoord.x = gridCoord.x + SnapXOffset;
        gridCoord.y = gridCoord.y + SnapYOffset ;
        return gridCoord;
    }

as you can see i completely discarded GridPosToWorldPos(), maybe i did wrong(?) but it’s the only way i can get it to work by adding the offset variables otherwhise the behavior is the one i described in the previous messages

Good job so far. This looks really nice. :slight_smile:

#1: The sprites are getting rendered relative to the pivot point. Check their pivot point in the Sprite Editor.

#3: That might be a rounding problem. What happens when you click at the top edge? Add Debug.Logs to your code to figure out how the input values are processed in your current algorithm. Guessing around is a waste of time. :wink: