Search Unity

Grid/Road Tool

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

  1. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    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:
    2,605
    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:
    16

    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
  4. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    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
    Monarch_Burgundy likes this.
  5. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    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. :)
     
  6. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    So, hi again. Kinda awkward to return to this topic after so long, but school work kinda got in the way to explore it further. I'm having this issue with the very first block (i.e the inititally clicked one) doesn't seem to get added to the roadCubes list.

    For example, if I (as per the attached image) click the top left corner, the result isn't a complete line.
     

    Attached Files:

  7. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    Hi again,
    forgive me if i'm a bit lazy and dont wanna re-read all the posts right now haha.

    The problem, however, seems pretty simple. At the place we are adding the cubes, we are stopping one index before the correct cube. After a quick look at the code, we are adding them at lines 16 and 20 of the last example. So the for-conditions should be the culprits. Try a <= instead of a < there. If it throws an IndexOutOfBoundsException i'll need to get into it more deeply again.

    Hope this helps :)

    Edit: Wait, you wrote it's the first block. The above would fix it if it was the last block.
    Similar issue, lemme look at the code again.

    Edit2:
    So actually, the issue can be fixed by what i wrote above, ie replacing both < with a <= comparator. This is the case, because we calculate the start position and the distance we need to travel per axis. So it does not actually matter which block is the first or last clicked, the one further from the origin of the grid is the one which will be iterated over later. This means that, despite you clicking on it first, it was actually still the last block to be added to the grid. Thus it's fixed and explained exactly as i first thought.

    However, as you will probably notice when trying this, there is actually a bigger problem with the approach itself. But for that i'll write a post instead of further editing this one.
     
    Last edited: Mar 13, 2020
  8. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Don't worry about it, laziness more than forgiven. I really appreciate the help. ;)

    Edit: Okay, now this is weird. For horizontal roads the opposite it true - the last cube won't be added instead.Red arrows indicicate starting cube.
     

    Attached Files:

    Last edited: Mar 13, 2020
  9. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    I guess i should have refreshed the site earlier, but the explanation for that behaviour is in my second Edit.

    As i mentioned there after the fix, the approach still has a flaw. Let me explain why.
    In our original approach we defined an xStart and a zStart and then just added xDiff and zDiff many blocks from there to the road array. In my testing it worked fine, however, since we add the z-Cubes along the xStart axis as well, we actually never reach the correct end-cube. Instead of xStart, it should be xStart+xDiff, since we want to start our z-Path at the end of the x-Path, so to speak. So the corrected for-loop for the z-Part of our path looks as follows:
    Code (CSharp):
    1. for (int z = zStart; z < (zStart + zDiff); z++)
    2. {
    3.     roadCubes.Add(grid[xStart+xDiff, z]);
    4. }
    Before i found this quick and easy fix, i actually completely overengineered the problem and came up with a solution that makes the loop actually start at the correct mouseDownCubePosition in the grid, and end at the correct mouseUpCubePosition as well. Since we do not know which of those contains the larger grid index (ie, we dont know for any given two cubes if we need to run the indices forwards or backwards to reach the other), i created a for-loop which defines the direction it runs in based on a bool, which is defined by which cube has the larger x and z indices on the grid.

    As i said, it's completely overengineered, but now that i did it i thought i may as well post it :p
    Not sure if i would recommend looking at it, as it may be confusing to read for a beginner. If you understand how exactly for-loops work (even outside the standard for(int i = 0; i < something; i++) format), then it should make sense.
    I'm putting it in a spoiler anyways.
    So the code i came up with is as follows. First we define a bool per loop based on which x- and z values of the blocks are larger. This allows us to dynamically define the direction in which the loop runs "forward".
    Code (CSharp):
    1. bool xDownSmallerThanUp = mouseDownCubePosition.x < mouseUpCubePosition.x;
    2. bool zDownSmallerThanUp = mouseDownCubePosition.y < mouseUpCubePosition.y;
    3.  
    4. // This is a bit of an unusual for-loop. Explanation in text.
    5. for (int x = (int)mouseDownCubePosition.x;
    6.     x != (int)mouseUpCubePosition.x + (xDownSmallerThanUp ? 1 : -1);
    7.     x += xDownSmallerThanUp ? 1 : -1)
    8. {
    9.      roadCubes.Add(grid[x, (int)mouseDownCubePosition.y]);
    10. }
    11. // Doing the same we did for X, now for Z again
    12. for (int z = (int)mouseDownCubePosition.y;
    13.     z != (int)mouseUpCubePosition.y + (zDownSmallerThanUp ? 1 : -1);
    14.     z += zDownSmallerThanUp ? 1 : -1)
    15. {
    16.       roadCubes.Add(grid[(int)mouseUpCubePosition.x, z]);
    17. }
    Afterwards we define the for-loops in a bit unusual way. Let's just look at the loop for the x-Part of our path. We start it at the position on the grid where you actually clicked the mouse down (mouseDownCubePosition.x as int). We want to run the loop until the mouseUpCubePosition.x is reached, but since we dont know if that's above or below our current position, we cannot use <, >, <= or >=. Instead i opted to use != and run the loop until our counter x is the same as mouseUpCubePosition.x. However, this is just equal to using < and >, which we know from before resulted in the problem of the last cube missing. To do the equivalent fix (using <= instead of <), here we need to add or subtract one from mouseUpCubePosition.x, based on whether is is smaller or bigger than mouseDownCubePosition.x. For that we use our precalculated bool xDownSmallerThanUp and end up with the scary looking expression of "x != (int)mouseUpCubePosition.x + (xDownSmallerThanUp ? 1 : -1)". Afterwards we need to tell the loop what to do after each iteration. Normally we would write a simple "x++" here, but since we again dont know in which direction to run, we need to make the direction depend on xDownSmallerThanUp, which results in "x += xDownSmallerThanUp ? 1 : -1".

    Yeah, it's definitely a bit more complicated. Thought it may be worth something educationally tho haha. Imagine my face as i tried to explain this and then realised i could fix the problem by just using xStart+xDiff instead of xStart in the original approach. Man i really need to stop jumping at problems before properly analyzing what is wrong :D
     
    Last edited: Mar 13, 2020
    Monarch_Burgundy likes this.
  10. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Thank you again for your help. It now properly starts and stops on the right cube. However, I'm noticing some odd behavior when it comes to L-shaped "roads": it seems only capable of drawing L's that start from the top (i.e gathering cubes down as you go) and to the left.

    For example, starting the click at the green arrow, following the green path and releasing the click at the red arrow yields these results. This is using the simplified solution - using the complex one resulted in similar L-shape problems, too but of another direction/angle.
     

    Attached Files:

  11. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    Alright, so my overkill solution is good for something after all.
    The problem does not exist with this code. Sorry for all the cofusion demmit.
    Code (CSharp):
    1.                 pressedLmbLast = false;
    2.                 mouseUpCubePosition = gridPos;
    3.                 Debug.Log("Released on Cube: " + gridPos);
    4.  
    5.                 List<GameObject> roadCubes = new List<GameObject>();
    6.  
    7.                 bool xDownSmallerThanUp = mouseDownCubePosition.x < mouseUpCubePosition.x;
    8.                 bool zDownSmallerThanUp = mouseDownCubePosition.y < mouseUpCubePosition.y;
    9.  
    10.                 // This is a bit of an unusual for-loop. Explanation in text.
    11.                 for (int x = (int)mouseDownCubePosition.x;
    12.                     x != (int)mouseUpCubePosition.x + (xDownSmallerThanUp ? 1 : -1);
    13.                     x += xDownSmallerThanUp ? 1 : -1)
    14.                 {
    15.                     roadCubes.Add(grid[x, (int)mouseDownCubePosition.y]);
    16.                 }
    17.                 // Doing the same we did for X, nor for Z again
    18.                 for (int z = (int)mouseDownCubePosition.y;
    19.                     z != (int)mouseUpCubePosition.y + (zDownSmallerThanUp ? 1 : -1);
    20.                     z += zDownSmallerThanUp ? 1 : -1)
    21.                 {
    22.                     roadCubes.Add(grid[(int)mouseUpCubePosition.x, z]);
    23.                 }
    24.  
    25.                 Debug.Log("Road consists of " + roadCubes.Count + " cubes.");
    26.  
    27.                 // All cubes that make up our road are now saved in roadCubes
    28.                 // Do what you want with them, for example delete them or change their color:
    29.                 foreach (GameObject cube in roadCubes)
    30.                 {
    31.                     cube.GetComponent<Renderer>().material.color = Color.black;
    32.                 }
    Either way, the road creation 'algorithm' itself was just intended to be an example. Me being to stupid to put the right blocks into the road array does not change the fact that it creates a road from those blocks hehe.
    As i stated in my first post, you'd ideally fill in this part with your own algorithm for road creation. Depending on what this is for, you'd probably want a diagonal road or something along those lines. As i mentioned, the code was only supposed to create a one-turn L-shaped road. Even tho.. i didnt expect it to have so many flaws :D
    I hope this intended-to-be-simple algorithm, at which i failed horribly, works now.
     
    Monarch_Burgundy likes this.
  12. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Could there be some kind of setup within the scene (GameObjects being children of one another, camera etc) that could mess things up? Because that new snippet of code makes L-shapes even wonkier than before - now they all become mirrored instead.

    EDIT: No, hol up. It is only the ones going down or up, then veering off to the left or right (as per the image) that get mirrored. The ones that start going horizontally then either up or down get drawn correctly.
     

    Attached Files:

    Last edited: Mar 14, 2020
  13. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    Not quite sure what it is you are describing. The code only guarantees (and for me it works), that the two cubes you clicked and released on get connected by a path. The direction of the L shape is undefined, even to it depends on which cube you clicked first, since that determines which way the for-loop runs.

    Just to make this clear, you can only decide on the start and end cube, not the path it creates between them. And as i stated before, this algorithm will only ever create paths with at most one turn, ie L-shapes.
     
    Monarch_Burgundy likes this.
  14. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Hmmm. I hope this makes it a bit clearer:

    Starting cube is green arrow, ending cube is red arrow. Path is indicated by the blue line. When I draw a horizontal line (A and B) and then veer off either up or down the created path follows the mouse movement. However when I draw a horizontal line (C) the resulting path is the "opposite" or mirrored version of the mouse movement.

    Is this just a quirk of your algorithm or did I somehow mess it up?
     

    Attached Files:

  15. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    If you want to call it that, it's a 'quirk' of the algorithm. As i said, the algorithm does not, ever, follow your mouse movement in any ways. After all, we only designed it to connect two cubes. It has no informations about the path inbetween. It gets two blocks and then connects them with cubes.

    You can of course adjust the algorithm to monitor the mouse movement while the mouse is pressed down, then decide on the direction of the L-shape based on this information. Or you could just add all blocks that are under the mouse while it is being clicked down (not resulting in L-shape). Or you connect the first and last cube with some line and check all the cubes that get hit by that line, then add those to create a diagonal line (obviously not L-shaped either). Or you could use a pathfinding algorithm like A* to find a path (around obstacles) from the first to the second clicked cube (again, not an L-shaped path).

    All of this, however, falls into the category of deciding on how the road should look and then implementing some way to get the desired kind of path. The solution i posted was always only intended as a mostly simple example, or a proof of concept. In your initial post you also asked about an L-shaped path, but i dont think you mentioned that the path should follow the movement of the mouse and pick the closest L-shape to that movement. This would be somewhat harder to implement.
    Then again, as long as you can define what the path should look like, what inputs are needed to decide on this shape, and how to connect the two cubes based on the other inputs with a path such that it results in what you have in mind, then you can create any kind of path you'd like.
     
    Monarch_Burgundy likes this.
  16. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Ah, yeah. Not so much a quirk in the algorithm as much it is a quirk in which of the two possible paths Unity decides to take, then. Horizontal lines it takes the one that happens to mimic the mouse movement, vertical lines it takes the “opposite” resulting it the mirroring.

    Suppose there’s no way to influence this with the current code?
     
  17. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    There is always a way to influence basically everything programmatically. Just sometimes it gets harder with pre-existing structures that were not created for that purpose. So ideally, you know the requirements beforehand.

    Technically the direction the for-loop runs in determines how the path looks. We can start at the lower or higher grid coordinate, then iterate downwards or upwards to reach the target. On both, the x-Axis and z-Axis.

    So to get a mirrored L-shape, you should just need to select the cubes in the opposite order, ie making red the first and green the second should result in the mirrored L-shape you get from having green as first and red as second (refering to your markers in the screenshot).
    So let's assume you have some bool 'flipPathShape'. If this is true, all we had to do would be to flip the contents of mouseDownCubePosition and mouseUpCubePosition and the path should be flipped as well.

    The problem is how do we determine when to set flipPathShape = true? We can gain some additional information on the situation, by for example recording the mouse movement while the button is pressed down. When releasing, we could then calculate the average position to get some point in 2D space. We could then form a vector pointing from mouseDownCubePosition to this averageMousePosition as well as from mouseDownCubePosition to mouseUpCubePosition. Compating the two using Vector2.SignedAngle we could get a positive value if the position is to the left of the line between the points, and a negative value if it was to the right.
    The problem is that this information alone is not enough, since we now only know how to flip the path, and what direction it should go in, but we'd still need to figure out which direction it would originally go in.

    With the current approach this is a bit harder to think about. With this new requirement i would probably have chosen a different approach. So i gotta ask now: before i tinker on a solution, are there any more things you'd like to add, other than creating an L-shaped path between two cubes, which should point in the direction the mouse moved along?
    Do you have a full feature description of what you have in mind? The thing is, when you are contronted with a problem you have almost infinite ways to approach it. Let's say we only had 3 ways A, B and C. So you just chose which seems to make sense for the given problem. Then later on, a new feature is required. It can often happen that you chose option B first, but now with the second required feature, option A would have been better. So knowing most / all required features beforehand is kind of important.
    Is this important, or are you just asking out of curiosity now? I dont mind creating and explaining a different approach, but originally this was about creating a structure which lets you create paths. This exists already, you just need to define what a path is and add the corresponding cubes, which is what we have been at for the past 10 or so posts hehe :D
     
  18. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Thank you again for your infinite patience with me. ;)

    The core idea/concept was basically to mimic a road-tool you’d see in early strategy/city building games. To have some kind of grid-based terrain whereupon to be able to click and drag to build a road/path.

    The desire for the L-shape is to mimic the way these early games would’ve handled making turns in this road/path as to make it a bit more advanced than just manually connecting straight vertical/horizontal paths.
     
  19. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    Algorithmically i can probably make something up, but while i know what you are trying to achieve, there is still a bit of wiggle-room as for how exactly it should behave. Things like 'only allow straight paths with one turn' are quite easy to say, but considering how many implicite expectations we have that are not mentioned in this statement, it may become quite hard to define the correct behaviour.
    What do you want to have as condition for whether the path goes one way or another? I mean, you already said that you want this to be based on the mouse. But the system needs to be robust as well. If we wanted the user to just paint straight lines, we would only allow him to do that. Since you instead want to allow for L-shaped roads, there are a lot of different approaches, but each of them will feel different and have their own strengths and weaknesses.
    Creating an L-shaped path is comparably easy, but as you noticed with our previous approach, the result may not feel as expected. When i thought about it i had a couple of ideas for potential approaches, however whenever i give the user more control, we basically end up with a 'nicer way' or making the user manually paint straight roads. And whenever i try to do it algorithmically, i feel like the result wont feel natural / as expected.

    Some (algorithmic, no code) suggestions:
    1. We could update the path in realtime and allow the user to go forward, left, or right from the previous block. We would however only allow a right / left turn once. So the user could in realtime paint a straight road, with one turn. We could also potentially add the feature to go backwards and thus get the 'free turn' back once you reach the main path again, but that would involve a bit more coding. However, effectively this is more or less the same as if he painted two straight roads after each other, just a bit more complicated to implement and potentially a bit more visually pleasing.
    2. We could do it in an approach similar to what we are doing right now, where only the start and end cubes count and the path is determined dynamically. However, we somehow need a way for the user to indicate the initial direction of the path. For this there are different approaches, but again they each have their weaknesses. We could, for example, detect the first cube after the initial cube and then determine the direction from there. As you can imagine tho, if the user slips a bit he cannot correct his choice.
    3. Instead of just allowing L-shapes you could opt in for more dynamic path shapes using, for example, A*. With buildings in the way, the resulting path would most likely always be some form of straight lines as well. But it wouldnt be guaranteed.
    4. Instead of forcing L-shapes we could chain a couple of straight paths, so the user has full control over the shape of the road. For this approach we would need to implement a sort of preview-mode in which you can add 'nodes' to the path, where each node is connected to the last in a straight line. We could only need to make sure that each node is only accepted if the path to the last node is straight. Thus the user could define more complex roads - but in the end we are pretty close to just manually adding multiple straight paths again.
    I believe option 1 would be quite close to the feeling you are going for, but then again it's pretty close to just painting two straight roads manually. You probably have a very specific game in mind when thinking about this, right? Maybe i can take a look at it, even tho unless i played it myself it's quite unlikely to get the correct behaviour just from watching a video or two.
     
    Monarch_Burgundy likes this.
  20. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    Option 1 definitely seems to be the ticket, so to speak. Doing it in realtime with allowing the path to be “undone” by doubling back would definitely give it more of a pleasing “game feel” to a prospective player as it would allow you to preview and adjust it before finalizing.

    The reason I want the “one-turn L-shape” is, as you correctly assumed, to make it just a bit more interesting than drawing just two straight roads one after another. Although that should still be an option if the player doesn’t turn left or right of course.

    As for game examples, I had this in mind:


    Well, sorta. This is a newer(ish) Sim City so the game isn’t as conformed to a square grid system as the older ones I actually took inspiration from (this newer one kinda dominates YouTube search results) since it allows things like diagonal roads at different degrees etc which, for now, I am not interested in implementing for this little project.

    edit: Here’s a video of an older SimCity (please pardon the err... enthusiasm of the lets player lol):
     
  21. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    In games like these (i played some Cities:Skylines) you usually drag straight roads tho (or if the game allows, curved ones, but mostly one road after another). And in the videos i also cannot see any L-shaped road construction tools or something along those lines. What you mostly find in these games is a visualization or preview of the path before you build it. If anything, i would probably add something like that.
    If i missed some example of what you want to do in the videos feel free to add a timestamp.

    If you plan on creating a city builder i would suggest getting some fundamental programming knowledge beforehand. Even if you 'only' want it to be on a grid, you will eventually have to come up with tools for selecting segments, placing buildings, connecting paths not just visually but also in some meaningful data structure (like a tree) for pathfinding and so on and so on. So right now i wouldnt really be concerned about details like making roads more interresting to be placed. You technically dont even have roads yet, you have rows of colored blocks on a grid. Roads need to connect things, or allow things to be placed next to them, units like cars or people need to travel over / along roads, ...

    I honestly think it would do you better to stick to single-line roads for now and instead try to improve your general understanding of the implementation, as well as how to work with grids programmatically to achieve what you want.
     
    Monarch_Burgundy likes this.
  22. Monarch_Burgundy

    Monarch_Burgundy

    Joined:
    Dec 7, 2016
    Posts:
    16
    The latter video doesn’t showcase it well enough (because as you say, he does straight roads) but at times you can sorta see him veer off-course with the mouse and it briefly shows how you could potentially use it to make a turn in the road, an L-shape (I think the calculation is that as long as the new path that veers off is shorter than the starting path, it it turns. Otherwise the path resets and it tries to draw a straight one instead. I think.) That’s really just the function I want to have. Or at least somewhat mimic/recreate to a lesser degree.

    Additional citybuilder elements I don’t really have any plans on adding. Well, yet. I guess you could further down the line randomly assign some blocks as unbuildable because they’re structures, and program some kind of walking unit/vehicle that is only allowed to travel on pieces of grid Tagged as ‘Road’.
     
    Last edited: Mar 18, 2020