If someone is looking for inspiration or ideas for how to accomplish in-game object placement, you might find this script helpful. It’s designed for a FPS game, not RTS. It doesn’t feature a grid, but I think you could tweak it to make it work for that. Note that the overlap sphere is hardcoded, and so it may not work for every situation. This should just be used as a starting place anyway, not a fully polished script to toss in a game. One could try to generalize this to walls and not just horizontal surfaces by using RaycastHit.normal.
using UnityEngine;
using UnityEngine.InputSystem;
// place this on camera
public class ObjectPlacer : MonoBehaviour
{
public GameObject ghostPrefab; // prefab displayed when finding placement
public GameObject placementPrefab; // prefab instantiated
public GameObject ghostParent; // parent of ghost (place 3 units in front of camera as child)
public bool placingObject; // set to true to see ObjectPlacer working
public Material ghostMaterialValid; // material set when placement is valid
public Material ghostMaterialInvalid;
private MyCharacterInput characterInput; // new input system
InputAction confirmAction; // confirm placement
InputAction cancelAction; // cancel placement
bool wasValidPosition = false;
int floorLayer = 14; // layer that objects should be placed on
MeshRenderer[] renderers;
SkinnedMeshRenderer[] sRenderers;
GameObject ghostInstance;
// set up player input and instantiate ghost
void Awake()
{
characterInput = new MyCharacterInput();
if (placingObject)
{
startPlacing();
}
}
private void OnEnable()
{
confirmAction = characterInput.Gameplay.Action;
confirmAction.performed += performInteract;
confirmAction.Enable();
cancelAction = characterInput.Gameplay.ClosePanel;
cancelAction.performed += stopPlacing;
cancelAction.Enable();
}
// create ghost and set material
public void startPlacing()
{
ghostInstance = GameObject.Instantiate(ghostPrefab, ghostParent.transform);
renderers = ghostInstance.transform.GetComponentsInChildren<MeshRenderer>();
sRenderers = ghostInstance.transform.GetComponentsInChildren<SkinnedMeshRenderer>();
if (wasValidPosition)
{
setGhostMaterial(ghostMaterialValid);
}
else
{
setGhostMaterial(ghostMaterialInvalid);
}
}
void setGhostMaterial(Material newMaterial)
{
for (int i = 0; i < renderers.Length; i++)
{
Material[] mats = new Material[renderers*.materials.Length];*
for (int j = 0; j < mats.Length; j++)
{
mats[j] = newMaterial;
}
renderers*.materials = mats;*
}
for (int i = 0; i < sRenderers.Length; i++)
{
Material[] mats = new Material[sRenderers*.materials.Length];*
for (int j = 0; j < mats.Length; j++)
{
mats[j] = newMaterial;
}
sRenderers*.materials = mats;*
}
}
// call to end placement process
public void stopPlacing(InputAction.CallbackContext obj)
{
placingObject = false;
GameObject.Destroy(ghostInstance);
}
// called when player confirms placement via input
private void performInteract(InputAction.CallbackContext obj)
{
if (wasValidPosition && placingObject)
{
// instantiate prefab where ghost was
GameObject.Instantiate(placementPrefab, ghostInstance.transform.position, ghostInstance.transform.rotation);
}
}
// core functionality is here
void FixedUpdate()
{
if (placingObject)
{
int detectLayer = 1 << floorLayer;
RaycastHit hit;
// check if ray hits floor
if (Physics.Raycast(transform.position, transform.TransformDirection(Vector3.forward), out hit, 3.0f, detectLayer))
{
// move placement according to hit
ghostInstance.transform.position = hit.point;
ghostInstance.transform.rotation = Quaternion.identity;
// use overlap sphere to check if placement is invalid due to overlap with colliders
// sphere is positioned a little above the floor
Vector3 spherePosition = new Vector3(
ghostInstance.transform.position.x,
ghostInstance.transform.position.y + .6f,
ghostInstance.transform.position.z
);
Collider[] hitColliders = Physics.OverlapSphere(spherePosition, .3f);
bool validPlacement = true;
foreach (var hitCollider in hitColliders)
{
// if the name isn’t one that we should ignore and it isn’t a trigger
if (!(nameMatchesPrefab(hitCollider.name)) && !(hitCollider.GetComponent().isTrigger))
{
validPlacement = false;
Debug.Log("collider overlap " + hitCollider.name);
}
}
// if placement is valid
if (validPlacement)
{
// if it wasn’t previously a valid placement, it is now
if (!wasValidPosition)
{
setGhostMaterial(ghostMaterialValid);
wasValidPosition = true;
}
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * hit.distance, Color.blue);
}
else
{
Debug.Log("cannot place because of collider overlap ");
// if it was previously a valid placement, it isn’t anymore
if (wasValidPosition)
{
setGhostMaterial(ghostMaterialInvalid);
wasValidPosition = false;
}
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * hit.distance, Color.magenta);
}
}
// else raycast didn’t hit any floor layer colliders
else
{
// if it was previously a valid placement, it isn’t anymore
if (wasValidPosition)
{
setGhostMaterial(ghostMaterialInvalid);
ghostInstance.transform.localPosition = Vector3.zero;
wasValidPosition = false;
}
Debug.DrawRay(transform.position, transform.TransformDirection(Vector3.forward) * 3.0f, Color.red);
}
}
// does collider name match any colliders in prefab
bool nameMatchesPrefab(string colliderName)
{
bool match = false;
if (colliderName.StartsWith(ghostPrefab.name))
{
match = true;
}
Collider[] colliders = ghostPrefab.GetComponentsInChildren();
for (int i = 0; i < colliders.Length; i++)
{
if (colliderName == colliders*.gameObject.name)*
{
match = true;
}
}
return match;
}
}
private void OnDisable()
{
confirmAction.Disable();
cancelAction.Disable();
}
}