What to expect from this blog post

There are very few resources on this topic and most of them actually suck or include too many bulky scripts. So, we’re going to write a simple script that will be less than 50 lines long which can be placed in any scene of any 3d project that you make in Unity. The script will basically allow you to preview and place a GameObject of your choice (for e.g, a Cube) at a position determined by the placement of your cursor. The script can be expanded upon to use more than 1 GameObject and switch between those. This will primarily be a guide for mobile (because I primarily publish Android games, duh) but I’ll explain the changes to make to allow full compatibility with desktop/console.

Here’s something to give you an idea of what an implementation of it looks like in a game:

The name of the game is SquareVille (yeah, shameless promotion smh).

Time to code

The code will be explained just below if that’s of any interest to you. Else, just make a script named sandbox and copy and paste the code in it. Just drag it on an empty GameObject in your scene. To test if the script works, create a Cube in your scene, drop it on the targetObject field of the script component, assign a material in the opaqueMat field, create a UI button in your scene and reference its OnClick() function to call the PlaceObject() function of the sandbox component on the GameObject it’s on.

Basically, when you pan around (if your Camera Controller can do that), the GameObject referenced in the targetObject field will be dragged and snapped around and upon clicking the UI button that calls the PlaceObject() function, a copy of that GameObject will be made and it will be assigned the material you referenced in the opaqueMat field, just so you know, to have a different color and some visual feedback.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sandbox : MonoBehaviour {

    Vector3 targetPosition, mousePosition;

    public Transform targetObject;
    public float distance = 10f;

    public Material opaqueMat;

    List<Vector3> placedPositions = new List<Vector3>();

    void Start() {
        mousePosition = new Vector3(Screen.width / 2, Screen.height / 1.5f, 0);
    }

    void Update () {
        // mousePosition = Input.mousePosition;
        targetPosition = Camera.main.ScreenToWorldPoint(new Vector3(mousePosition.x, mousePosition.y, distance));

        if (targetPosition.y < 1) targetPosition.y = 1;
        targetPosition = Vector3Int.RoundToInt(targetPosition);
        
        if (!placedPositions.Contains(targetPosition)) {
            targetObject.position = targetPosition;
        }
    }

    public void PlaceObject() {
        Transform newInstance = Instantiate(targetObject, targetObject.transform.position, targetObject.transform.rotation);

        Material[] intMaterials = new Material[newInstance.GetComponent<Renderer>().sharedMaterials.Length];
        for (int i = 0; i < intMaterials.Length; i++)
            intMaterials[i] = opaqueMat;
        newInstance.GetComponent<Renderer>().sharedMaterials = intMaterials;
        
        placedPositions.Add(targetObject.position);
    }
}

I don’t think I should be explaining the 3 first lines and the class declaration statement. In the sandbox class, I’m declaring 2 (private) variables of the Vector3 type, a public variable holding a float value named distance (pretty self-explanatory) and a Vector3 List named placedPositions which you’ll understand the utility of later. The other variables and their use have already been stated in the paragraph(s) before the code was given.

In the Start() function (which runs once per script when the game starts), we will process and store a Vector3 value in the mousePosition variable. This value corresponds to a position slightly above the center point of the screen that the game is being played on. As screens are normally 2D, the Z-Axis value is assigned a value of 0.

In the Update() function (which runs every frame), you can uncomment the first line to allow for desktop input by actually moving the cursor. If you’re making the game for mobile, keep this commented so that the script always ‘believes’ that the cursor is always a bit above the center point of the screen, allowing you to pan your Camera Controller to change the position where you want objects to be spawned. For console, you just have to change the x and y positions of the mousePosition variable depending on a joystick input.

Based on the value held in mousePosition, we are going to translate and store actual 3D world coordinates in the targetPosition variable and at a Z-Position determined by the value in our distance variable.

Next, we check whether the Y-Position of the Vector3 value held in targetPosition is less than 1, which is not something we want since then our object will be spawned below the ground (assuming you have one). We will also want to round all the values held in targetPosition to the closest integer so that our object always snaps to some kind of invisible grid made up of 1 x 1 x 1 units. You can comment this line to disable snapping or modify the algorithm to use a custom grid snap value.

Then, we check if the placedPositions list contains a Vector3 value equal to that of targetPosition. If not (hence the “!” sign), then we want to move our template GameObject (or the Cube that we used as example in the paragraph(s) above the code) at that position. Else, the template GameObject will remain where it was previously. This check is done so that situations where objects are placed at the same position or overlap edges/faces of other objects never occur (big brain huh, jk).

Finally, PlaceObject() can be called using a reference in the OnClick() function of a simple UI Button. That function will instantiate (spawn) a copy of targetObject (our template GameObject) at its current position and rotation. The renderer of that new instance will also be referenced and assigned the material of opaqueMat (named that variable like that because I was using a semi-transparent one for the template). Lastly, we are adding the position where newInstance was spawned to the placedPositions list for the reason discussed in the previous paragraph.

Hope I could help you! Drop me a mail if you didn’t understand something.