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:
[SerializeField] private int height = 10;
[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:
[SerializeField] private GameObject cubePrefab;
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.
for (int x = 0; x < width; x++)
{
for (int z = 0; z < height; z++)
{
// you could also add a padding /space between the cubes here
grid[x, z] = Instantiate(cubePrefab, new Vector3(x, 0, z), Quaternion.identity);
// For convenience we can name the cubes based on their position:
grid[x, z].name = "Cube (" + x + ", " + z + ")";
}
}
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:
private Vector2 mouseDownCubePosition;
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:
void Update()
{
RaycastHit hit;
// If you have more than one camera, assign the right one through inspector to some variable and use that
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
Vector3 hitPos = hit.point;
Vector3 gridPos = new Vector2(Mathf.Round(hitPos.x), Mathf.Round(hitPos.z));
if (Input.GetMouseButtonDown(0)) // lmb pressed
{
pressedLmbLast = true; // <-- explaining this later
mouseDownCubePosition = gridPos;
Debug.Log("Clicked on Cube: " + gridPos);
//Destroy(grid[(int)gridPos.x, (int)gridPos.y]);
} else if (Input.GetMouseButtonUp(0) && pressedLmbLast) // lmp released AND pressed before
{
pressedLmbLast = false;
mouseUpCubePosition = gridPos;
Debug.Log("Released on Cube: " + gridPos);
// We later continue coding: HERE
}
}
}
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:
List<GameObject> roadCubes = new List<GameObject>();
// We want the difference, so only the positive value
int xDiff = Mathf.Abs((int)mouseDownCubePosition.x - (int)mouseUpCubePosition.x);
int zDiff = Mathf.Abs((int)mouseDownCubePosition.y - (int)mouseUpCubePosition.y); // remember this is z
// We know how many blocks, but not where to start. Find the smaller of the two values:
int xStart = mouseDownCubePosition.x < mouseUpCubePosition.x ?
(int)mouseDownCubePosition.x : (int)mouseUpCubePosition.x;
int zStart = mouseDownCubePosition.y < mouseUpCubePosition.y ?
(int)mouseDownCubePosition.y : (int)mouseUpCubePosition.y;
// Iterate over all cubes in the defined range to get cubes on our path
// Note: dont use nested loops, or you get the entire area, not its outline/path
for (int x = xStart; x < (xStart+xDiff); x++)
{
roadCubes.Add(grid[x, zStart]);
}
for (int z = zStart; z < (zStart + zDiff); z++)
{
roadCubes.Add(grid[xStart, z]);
}
Debug.Log("Road consists of " + roadCubes.Count + " cubes.");
// All cubes that make up our road are now saved in roadCubes
// Do what you want with them, for example delete them or change their color:
foreach (GameObject cube in roadCubes)
{
cube.GetComponent<Renderer>().material.color = Color.black;
}
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:
FullCode
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GridManager : MonoBehaviour
{
[SerializeField] private int height = 10;
[SerializeField] private int width = 10;
[SerializeField] private GameObject cubePrefab;
private GameObject[,] grid;
private Vector2 mouseDownCubePosition;
private Vector2 mouseUpCubePosition;
private bool pressedLmbLast = false;
// Start is called before the first frame update
void Start()
{
grid = new GameObject[width, height];
for (int x = 0; x < width; x++)
{
for (int z = 0; z < height; z++)
{
// you could also add a padding /space between the cubes here
grid[x, z] = Instantiate(cubePrefab, new Vector3(x, 0, z), Quaternion.identity);
// For convenience we name the cubes after their position:
grid[x, z].name = "Cube (" + x + ", " + z + ")";
}
}
}
// Update is called once per frame
void Update()
{
RaycastHit hit;
// If you have more than one camera, assign the right one through inspector to some variable and use that
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
Vector3 hitPos = hit.point;
Vector3 gridPos = new Vector2(Mathf.Round(hitPos.x), Mathf.Round(hitPos.z));
if (Input.GetMouseButtonDown(0)) // lmb pressed
{
pressedLmbLast = true;
mouseDownCubePosition = gridPos;
Debug.Log("Clicked on Cube: " + gridPos);
//Destroy(grid[(int)gridPos.x, (int)gridPos.y]);
} else if (Input.GetMouseButtonUp(0) && pressedLmbLast) // lmp released
{
pressedLmbLast = false;
mouseUpCubePosition = gridPos;
Debug.Log("Released on Cube: " + gridPos);
List<GameObject> roadCubes = new List<GameObject>();
// We want the difference, so only the positive value
int xDiff = Mathf.Abs((int)mouseDownCubePosition.x - (int)mouseUpCubePosition.x);
int zDiff = Mathf.Abs((int)mouseDownCubePosition.y - (int)mouseUpCubePosition.y); // remember this is z
// We know how many blocks, but not where to start. Find the smaller of the two values:
int xStart = mouseDownCubePosition.x < mouseUpCubePosition.x ?
(int)mouseDownCubePosition.x : (int)mouseUpCubePosition.x;
int zStart = mouseDownCubePosition.y < mouseUpCubePosition.y ?
(int)mouseDownCubePosition.y : (int)mouseUpCubePosition.y;
// Iterate over all cubes in the defined range to get cubes on our path
// Note: dont use nested loops, or you get the entire area, not its outline/path
for (int x = xStart; x < (xStart+xDiff); x++)
{
roadCubes.Add(grid[x, zStart]);
}
for (int z = zStart; z < (zStart + zDiff); z++)
{
roadCubes.Add(grid[xStart, z]);
}
Debug.Log("Road consists of " + roadCubes.Count + " cubes.");
// All cubes that make up our road are now saved in roadCubes
// Do what you want with them, for example delete them or change their color:
foreach (GameObject cube in roadCubes)
{
cube.GetComponent<Renderer>().material.color = Color.black;
}
}
}
}
}