Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

Grid/Road Tool

Discussion in 'Scripting' started by Monarch_Burgundy, Jul 31, 2019.

  1. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    7
    Hello, if it is in the wrong sub-forum I apologize. I’m still not sure what goes where around here...

    I need some advice/help to get me started on a project: Creating a grid based road tool. The player should be able to left click on the ground and drag his mouse, when the player releases the mouse a road is created. If the player changes row and column with the mouse the road should make a turn. Only orthogonal roads supported so if you “turn” it should drawn an L-shape, for example.

    I was thinking of making a grid made up out of simple 3D rectangles an positioning the camera above them as to create the grid visual, then simply change the texture colour of the selected ones.

    Now the first issue is that I’ve not really done anything with the mouse as input before, mostly keyboard/controller. Secondly not sure how to do the actual selection: I’d imagine the ones you mouse through would have to be added to an array/list which upon releasing the mouse button would colour them in..?
     
  2. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    52
    I dont know what this will be used for, so i may be making some simplifications that dont fit your criteria.

    Let's start with your grid. Your seem to intend a 2D (top-down) sort of grid, so we will need some fixed values for its height and width. We can use these to instantiate a multidimensional or jagged array of some type. The type depends on what you wanna do with it, but since you said something about 3D rectangles (i'll assume you meant 1x1x1 cubes) we will go with that.
    Code (CSharp):
    1. GameObject grid[,] = new GameObject[width, height];
    Now we need to fill that grid with content. Since you plan on using 3D cubes, you will habe to make a prefab of a cube, add a public (or [SerializeField] private) GameObject variable in your script, and add the prefab through the inspector.

    You can then use two for loops to iterate over each position in your grid (0 to width and 0 to height) and use Instantiate() to instantiate your prefab cube at that position. Now each time you call grid[x,y] you will get the cube that is at position (x,y) in your grid.

    Now we need to figure out the start- and endpoint of the mouse interaction. When you detect that the user is pressing the left mouse button (or whatever button you want to use) down, you will have to cast a ray (RayCast) through the screen towards the gameworld. This raycast will hit with one of your blocks. Now we have a coordinate.
    Ignoring the y-value of the hit, the absolute values of the x- and z-coordinates pretty much relate to your grid coordinates. Now you have your startposition (-cube). Do the same for the endposition (when lmb is released).

    Now you have a start- and endposition within your grind. Based on whatever road design you want, you can now calculate the desired path (to me it seemed like you only wanted one curve at max, so straight lines or a single L shape).

    After you found all the cubes making up your road (and added them to a list), do with them whatever you want. For example, change the material the renderer uses from the default material to a new material, so your road-cubes have a different color and thus can get recognized as a road.

    Too much detail, too little detail? Hope this helped anyways,
    Fabian
     
    Monarch_Burgundy likes this.
  3. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    7

    Hi

    Thanks a bunch for your reply. As to what it will be used for, nothing really. I’m a game design student and this is just a little standalone thing I’m trying to do. What you’re suggesting sounds like just the thing.

    Maybe it is a bit too much to ask, but is there a chance you could provide some code to get me started? It seems a bit more advanced than what I had thought I’d have to do. The loops in particular (I had initially thought just to have a s script on each individual block that checked if the mouse had entered it and if the mouse button was held or not).
     
    Last edited: Aug 13, 2019 at 6:23 AM
  4. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    52
    It's not like you couldnt possibly do something along those lines, but:
    • it seems you plan to manually create the blocks / grid, in which case it's a lot of manual work. A 10x10 grid is already 100 objects, scaling up to 100x100 or from that down to 50x50 is a ton of manual creation / placement.
    • when each block uses its own script to detect the mouseover/click, then you dont really have any context for creating specific desired road-patterns. So unless you really just want to paint blocks under your mouse this wont work out
    And sure, i'll help you out with the code. However, please understand that i'm mainly doing this to help you get a grasp of the ideas involved (as in basic algorithmic thinking) and not so that you get some finished code to copy, paste and use. As such i'd recomment you watch a couple basic C# tutorials on variables, arrays, if-statements, loops and so on, should you not be too familiar with coding in general. This is not meant in any negative or offensive way at all by the way.

    So let's get started. We will go for a simple "GridManager" type class. This script is then assigned to a (single) empty gameobject and will instantiate a grid of blocks of a predefined size for us and then update it.

    We need some variables to keep track of our grid-size:
    Code (CSharp):
    1. [SerializeField] private int height = 10;
    2. [SerializeField] private int width = 10;
    The [SerializeField] tag makes it so that the variable shows up in the inspector for that script, so you can simply change it there. Now we want to keep track of all the cube GameObjects we will have in our grid. To do that we need a multidimensional array of GameObjects. We will also need a way to spawn our cubes from script later on:
    Code (CSharp):
    1. [SerializeField] private GameObject cubePrefab;
    2. private GameObject[,] grid = new GameObject[width, height]; // needs to be instantiated in the Start() method, put it here for convenience!
    At this point you need to create a prefab of the cube object you want to use. To do that, create a cube in the scene, drag it into a project folder, and delete the cube in the scene again. You now have a prefab of the cube in the project folder you dragged it into. Assign this prefab to our "cubePrefab" variable through the inspector.

    Now we are ready to instantiate our grid of cubes. Since we only need to do this once on startup, we do so in the Start() method we inherit from Monobehavior. We use two nested for-loops to instantiate a cube on each index of our grid. We then set the coordinates of each instantiated block to its x and z coordinates inside the grid, so it actually looks like a grid of cubes as well.
    Code (CSharp):
    1.  
    2.         for (int x = 0; x < width; x++)
    3.         {
    4.             for (int z = 0; z < height; z++)
    5.             {
    6.                 // you could also add a padding /space between the cubes here
    7.                 grid[x, z] = Instantiate(cubePrefab, new Vector3(x, 0, z), Quaternion.identity);
    8.                 // For convenience we can name the cubes based on their position:
    9.                 grid[x, z].name = "Cube (" + x + ", " + z + ")";
    10.             }
    11.         }
    12.  
    With this you should already be able to run the script and create a visual grid of cubes that is "width" wide and "height" high. Our advantage, compared to creating the grid manually by placing each cube yourself, is that we can create any grid size we want, simply by changing the "width" and "height" values in the inspector. We also have the advantage that the grid of cubes we see reflects our datastructure "grid", such that "grid[0,0]" is the 0'th cube that got spawned on the x-Axis, as well as the 0'th cube that got spawned on the z-Axis and so on (reminder: we start counting at 0 in computer science). Also, our cubes' positions in the world are the same as their position on the grid array, such that "grid[0,0]" not only is the bottom-most left-most cube we spawned, but it's also the cube at position (x=0,y=0,z=0).

    Now we need to find out which cubes were clicked on (and released on) by the mouse. For this we first need new variables to keep track of these two special cubes. We could either save a reference to the actual cube, or simply their position in our grid. Here i'll go with the latter. The position of our cubes in the grid is a pair of two numbers, so we can simply save their position in a Vector2 as follows:
    Code (CSharp):
    1. private Vector2 mouseDownCubePosition;
    2. private Vector2 mouseUpCubePosition;
    Now we need to somehow detect which cube we clicked on. To do that we will cast a ray through the screen, or rather from the position of the camera to the world position of the mouse cursor, meaning the ray hits whatever is "behind" the cursor. For that we can borrow the code from https://docs.unity3d.com/Manual/CameraRays.html.

    We eventually want to get the position of the block below the curser, when the mouse is either pressed or released, and then convert this position to get the actual cubes' position in our grid. To do just that we call the borrowed code in the Update() method and check if the ray hits something. If that's the case we calculate the position of the cube.
    Remember how the position of our cubes in the gameworld reflects their position in the grid? So if we have a rayhit position of, for example, Vector3(25.3f, 0.5f, 12.8f), then we know that it hit the cube closest to the position on both the x and z coordinate. For this example it would be the cube at grid[25,13]. Since we cant just convert the float coordinate to int to get this result, we will use Mathf.Round(). We only assign this value (to the appropriate variable) if the mouse button was either pressed or released tho:
    Code (CSharp):
    1.  
    2.     void Update()
    3.     {
    4.         RaycastHit hit;
    5.         // If you have more than one camera, assign the right one through inspector to some variable and use that
    6.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    7.  
    8.         if (Physics.Raycast(ray, out hit))
    9.         {
    10.             Vector3 hitPos = hit.point;
    11.             Vector3 gridPos = new Vector2(Mathf.Round(hitPos.x), Mathf.Round(hitPos.z));
    12.             if (Input.GetMouseButtonDown(0)) // lmb pressed
    13.             {
    14.                 pressedLmbLast = true; // <-- explaining this later
    15.                 mouseDownCubePosition = gridPos;
    16.                 Debug.Log("Clicked on Cube: " + gridPos);
    17.                 //Destroy(grid[(int)gridPos.x, (int)gridPos.y]);
    18.             } else if (Input.GetMouseButtonUp(0) && pressedLmbLast) // lmp released AND pressed before
    19.             {
    20.                 pressedLmbLast = false;
    21.                 mouseUpCubePosition = gridPos;
    22.                 Debug.Log("Released on Cube: " + gridPos);
    23.  
    24.  
    25.                 // We later continue coding: HERE
    26.             }
    27.         }
    28.     }
    29.  
    At this point, when you press play and click on one of the cubes, you should see the coordinate of that cube printed out in the console. It's rather hard to see, unless your textures make borders between the cubes somewhat visible.
    To see that it's actually working, you could call "Destroy(grid[(int)gridPos.x, (int)gridPos.y])" and see the cube in question vanishing. Also, keep in mind that it's gridPos.y here, not z, since gridPos is a Vector2. It only saves 2 values: x and y. We put our z-value in the second value, which is named y. May be a bit confusing, but that's why i'm mentioning it here.

    Now we have the position of our start- and end-cube within our grid.
    However, note that the code as i just explained it would not be entirely safe. If you pressed LMB (= left mouse button) outside the grid and released it inside the grid, we would end up with an end-cube position, but no start-cube position (or rather the default value, which should be 0,0, as start position, which is not what we want).
    To prevent this i introduced a "bool pressedLmbLast = false", which is set true whenever LMB is pressed and false whenever LMB is released. It is also required to be true before we can enter the release-codeblock at all.
    This should guarantee that both values are assigned, with fresh values, in the right order, every time we enter the release-codeblock.

    At the spot i marked with "HERE" we would now build your road, which is basically a subset of blocks from our grid, that we can save in a list for easier access. The interresting part here is how we define this subset / road.
    This highly depends on what you think a road should be, but i'll add an example for a simple road with max 1 turn, which i believe is what you want:
    Code (CSharp):
    1.                 List<GameObject> roadCubes = new List<GameObject>();
    2.                 // We want the difference, so only the positive value
    3.                 int xDiff = Mathf.Abs((int)mouseDownCubePosition.x - (int)mouseUpCubePosition.x);
    4.                 int zDiff = Mathf.Abs((int)mouseDownCubePosition.y - (int)mouseUpCubePosition.y); // remember this is z
    5.  
    6.                 // We know how many blocks, but not where to start. Find the smaller of the two values:
    7.                 int xStart = mouseDownCubePosition.x < mouseUpCubePosition.x ?
    8.                     (int)mouseDownCubePosition.x : (int)mouseUpCubePosition.x;
    9.                 int zStart = mouseDownCubePosition.y < mouseUpCubePosition.y ?
    10.                     (int)mouseDownCubePosition.y : (int)mouseUpCubePosition.y;
    11.  
    12.                 // Iterate over all cubes in the defined range to get cubes on our path
    13.                 // Note: dont use nested loops, or you get the entire area, not its outline/path
    14.                 for (int x = xStart; x < (xStart+xDiff); x++)
    15.                 {
    16.                     roadCubes.Add(grid[x, zStart]);
    17.                 }
    18.                 for (int z = zStart; z < (zStart + zDiff); z++)
    19.                 {
    20.                     roadCubes.Add(grid[xStart, z]);
    21.                 }
    22.  
    23.                 Debug.Log("Road consists of " + roadCubes.Count + " cubes.");
    24.  
    25.                 // All cubes that make up our road are now saved in roadCubes
    26.                 // Do what you want with them, for example delete them or change their color:
    27.                 foreach (GameObject cube in roadCubes)
    28.                 {
    29.                     cube.GetComponent<Renderer>().material.color = Color.black;
    30.                 }
    Again, defining the road is mostly subjective and the above is only meant as an example. It's also arguably the most tricky part. If you got any questions, or any explanations were unclear feel free to ask.

    This actually got pretty long and took a decent while to write. I hope it helps you grasp the basics of how to approach a problem like this and how to reach a solution step by step. I just hope it wasnt a homework or anything.

    Also, here the full code in one working file in case i introduced typos above:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class GridManager : MonoBehaviour
    6. {
    7.     [SerializeField] private int height = 10;
    8.     [SerializeField] private int width = 10;
    9.     [SerializeField] private GameObject cubePrefab;
    10.     private GameObject[,] grid;
    11.     private Vector2 mouseDownCubePosition;
    12.     private Vector2 mouseUpCubePosition;
    13.     private bool pressedLmbLast = false;
    14.  
    15.     // Start is called before the first frame update
    16.     void Start()
    17.     {
    18.         grid = new GameObject[width, height];
    19.  
    20.         for (int x = 0; x < width; x++)
    21.         {
    22.             for (int z = 0; z < height; z++)
    23.             {
    24.                 // you could also add a padding /space between the cubes here
    25.                 grid[x, z] = Instantiate(cubePrefab, new Vector3(x, 0, z), Quaternion.identity);
    26.                 // For convenience we name the cubes after their position:
    27.                 grid[x, z].name = "Cube (" + x + ", " + z + ")";
    28.             }
    29.         }
    30.     }
    31.  
    32.     // Update is called once per frame
    33.     void Update()
    34.     {
    35.         RaycastHit hit;
    36.         // If you have more than one camera, assign the right one through inspector to some variable and use that
    37.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    38.  
    39.         if (Physics.Raycast(ray, out hit))
    40.         {
    41.             Vector3 hitPos = hit.point;
    42.             Vector3 gridPos = new Vector2(Mathf.Round(hitPos.x), Mathf.Round(hitPos.z));
    43.             if (Input.GetMouseButtonDown(0)) // lmb pressed
    44.             {
    45.                 pressedLmbLast = true;
    46.                 mouseDownCubePosition = gridPos;
    47.                 Debug.Log("Clicked on Cube: " + gridPos);
    48.                 //Destroy(grid[(int)gridPos.x, (int)gridPos.y]);
    49.             } else if (Input.GetMouseButtonUp(0) && pressedLmbLast) // lmp released
    50.             {
    51.                 pressedLmbLast = false;
    52.                 mouseUpCubePosition = gridPos;
    53.                 Debug.Log("Released on Cube: " + gridPos);
    54.  
    55.                 List<GameObject> roadCubes = new List<GameObject>();
    56.                 // We want the difference, so only the positive value
    57.                 int xDiff = Mathf.Abs((int)mouseDownCubePosition.x - (int)mouseUpCubePosition.x);
    58.                 int zDiff = Mathf.Abs((int)mouseDownCubePosition.y - (int)mouseUpCubePosition.y); // remember this is z
    59.  
    60.                 // We know how many blocks, but not where to start. Find the smaller of the two values:
    61.                 int xStart = mouseDownCubePosition.x < mouseUpCubePosition.x ?
    62.                     (int)mouseDownCubePosition.x : (int)mouseUpCubePosition.x;
    63.                 int zStart = mouseDownCubePosition.y < mouseUpCubePosition.y ?
    64.                     (int)mouseDownCubePosition.y : (int)mouseUpCubePosition.y;
    65.  
    66.                 // Iterate over all cubes in the defined range to get cubes on our path
    67.                 // Note: dont use nested loops, or you get the entire area, not its outline/path
    68.                 for (int x = xStart; x < (xStart+xDiff); x++)
    69.                 {
    70.                     roadCubes.Add(grid[x, zStart]);
    71.                 }
    72.                 for (int z = zStart; z < (zStart + zDiff); z++)
    73.                 {
    74.                     roadCubes.Add(grid[xStart, z]);
    75.                 }
    76.  
    77.                 Debug.Log("Road consists of " + roadCubes.Count + " cubes.");
    78.  
    79.                 // All cubes that make up our road are now saved in roadCubes
    80.                 // Do what you want with them, for example delete them or change their color:
    81.                 foreach (GameObject cube in roadCubes)
    82.                 {
    83.                     cube.GetComponent<Renderer>().material.color = Color.black;
    84.                 }
    85.             }
    86.         }
    87.     }
    88.  
    89. }
    90.  
     
    Last edited: Aug 14, 2019 at 1:44 AM
    Monarch_Burgundy likes this.
  5. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    7
    Woah, that’s a lot of text! I appreciate the time you took to write it, and don’t worry: I’m well aware of my limitations and your suggestion to look up those tutorials are not at all rude. Furthermore don’t worry about doing my homework for me, this is just something I wanted to try for myself.

    I’m currently out on lunch, I will look further into your post once I get home and maybe I’ll have further questions once I get into it.

    Thank you again for all your help thus far. :)