├── .gitattributes ├── GridData.cs ├── IBuildingState.cs ├── InputManager.cs ├── LICENSE ├── ObjectPlacer.cs ├── ObjectsDatabaseSO.cs ├── PlacementState.cs ├── PlacementSystem.cs ├── PreviewSystem.cs ├── README.md ├── RemovingState.cs └── SoundFeedback.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /GridData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | public class GridData 7 | { 8 | Dictionary placedObjects = new(); 9 | 10 | public void AddObjectAt(Vector3Int gridPosition, 11 | Vector2Int objectSize, 12 | int ID, 13 | int placedObjectIndex) 14 | { 15 | List positionToOccupy = CalculatePositions(gridPosition, objectSize); 16 | PlacementData data = new PlacementData(positionToOccupy, ID, placedObjectIndex); 17 | foreach (var pos in positionToOccupy) 18 | { 19 | if (placedObjects.ContainsKey(pos)) 20 | throw new Exception($"Dictionary already contains this cell positiojn {pos}"); 21 | placedObjects[pos] = data; 22 | } 23 | } 24 | 25 | private List CalculatePositions(Vector3Int gridPosition, Vector2Int objectSize) 26 | { 27 | List returnVal = new(); 28 | for (int x = 0; x < objectSize.x; x++) 29 | { 30 | for (int y = 0; y < objectSize.y; y++) 31 | { 32 | returnVal.Add(gridPosition + new Vector3Int(x, 0, y)); 33 | } 34 | } 35 | return returnVal; 36 | } 37 | 38 | public bool CanPlaceObejctAt(Vector3Int gridPosition, Vector2Int objectSize) 39 | { 40 | List positionToOccupy = CalculatePositions(gridPosition, objectSize); 41 | foreach (var pos in positionToOccupy) 42 | { 43 | if (placedObjects.ContainsKey(pos)) 44 | return false; 45 | } 46 | return true; 47 | } 48 | 49 | internal int GetRepresentationIndex(Vector3Int gridPosition) 50 | { 51 | if (placedObjects.ContainsKey(gridPosition) == false) 52 | return -1; 53 | return placedObjects[gridPosition].PlacedObjectIndex; 54 | } 55 | 56 | internal void RemoveObjectAt(Vector3Int gridPosition) 57 | { 58 | foreach (var pos in placedObjects[gridPosition].occupiedPositions) 59 | { 60 | placedObjects.Remove(pos); 61 | } 62 | } 63 | } 64 | 65 | public class PlacementData 66 | { 67 | public List occupiedPositions; 68 | public int ID { get; private set; } 69 | public int PlacedObjectIndex { get; private set; } 70 | 71 | public PlacementData(List occupiedPositions, int iD, int placedObjectIndex) 72 | { 73 | this.occupiedPositions = occupiedPositions; 74 | ID = iD; 75 | PlacedObjectIndex = placedObjectIndex; 76 | } 77 | } -------------------------------------------------------------------------------- /IBuildingState.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public interface IBuildingState 4 | { 5 | void EndState(); 6 | void OnAction(Vector3Int gridPosition); 7 | void UpdateState(Vector3Int gridPosition); 8 | } -------------------------------------------------------------------------------- /InputManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.EventSystems; 6 | 7 | public class InputManager : MonoBehaviour 8 | { 9 | [SerializeField] 10 | private Camera sceneCamera; 11 | 12 | private Vector3 lastPosition; 13 | 14 | [SerializeField] 15 | private LayerMask placementLayermask; 16 | 17 | public event Action OnClicked, OnExit; 18 | 19 | private void Update() 20 | { 21 | if(Input.GetMouseButtonDown(0)) 22 | OnClicked?.Invoke(); 23 | if(Input.GetKeyDown(KeyCode.Escape)) 24 | OnExit?.Invoke(); 25 | } 26 | 27 | public bool IsPointerOverUI() 28 | => EventSystem.current.IsPointerOverGameObject(); 29 | 30 | public Vector3 GetSelectedMapPosition() 31 | { 32 | Vector3 mousePos = Input.mousePosition; 33 | mousePos.z = sceneCamera.nearClipPlane; 34 | Ray ray = sceneCamera.ScreenPointToRay(mousePos); 35 | RaycastHit hit; 36 | if (Physics.Raycast(ray, out hit, 100, placementLayermask)) 37 | { 38 | lastPosition = hit.point; 39 | } 40 | return lastPosition; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Peter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ObjectPlacer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | public class ObjectPlacer : MonoBehaviour 7 | { 8 | [SerializeField] 9 | private List placedGameObjects = new(); 10 | 11 | public int PlaceObject(GameObject prefab, Vector3 position) 12 | { 13 | GameObject newObject = Instantiate(prefab); 14 | newObject.transform.position = position; 15 | placedGameObjects.Add(newObject); 16 | return placedGameObjects.Count - 1; 17 | } 18 | 19 | internal void RemoveObjectAt(int gameObjectIndex) 20 | { 21 | if (placedGameObjects.Count <= gameObjectIndex 22 | || placedGameObjects[gameObjectIndex] == null) 23 | return; 24 | Destroy(placedGameObjects[gameObjectIndex]); 25 | placedGameObjects[gameObjectIndex] = null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ObjectsDatabaseSO.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | [CreateAssetMenu] 7 | public class ObjectsDatabaseSO : ScriptableObject 8 | { 9 | public List objectsData; 10 | } 11 | 12 | [Serializable] 13 | public class ObjectData 14 | { 15 | [field: SerializeField] 16 | public string Name { get; private set; } 17 | [field: SerializeField] 18 | public int ID { get; private set; } 19 | [field: SerializeField] 20 | public Vector2Int Size { get; private set; } = Vector2Int.one; 21 | [field: SerializeField] 22 | public GameObject Prefab { get; private set; } 23 | } -------------------------------------------------------------------------------- /PlacementState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using static Unity.VisualScripting.Member; 5 | 6 | public class PlacementState : IBuildingState 7 | { 8 | private int selectedObjectIndex = -1; 9 | int ID; 10 | Grid grid; 11 | PreviewSystem previewSystem; 12 | ObjectsDatabaseSO database; 13 | GridData floorData; 14 | GridData furnitureData; 15 | ObjectPlacer objectPlacer; 16 | SoundFeedback soundFeedback; 17 | 18 | public PlacementState(int iD, 19 | Grid grid, 20 | PreviewSystem previewSystem, 21 | ObjectsDatabaseSO database, 22 | GridData floorData, 23 | GridData furnitureData, 24 | ObjectPlacer objectPlacer, 25 | SoundFeedback soundFeedback) 26 | { 27 | ID = iD; 28 | this.grid = grid; 29 | this.previewSystem = previewSystem; 30 | this.database = database; 31 | this.floorData = floorData; 32 | this.furnitureData = furnitureData; 33 | this.objectPlacer = objectPlacer; 34 | this.soundFeedback = soundFeedback; 35 | 36 | selectedObjectIndex = database.objectsData.FindIndex(data => data.ID == ID); 37 | if (selectedObjectIndex > -1) 38 | { 39 | previewSystem.StartShowingPlacementPreview( 40 | database.objectsData[selectedObjectIndex].Prefab, 41 | database.objectsData[selectedObjectIndex].Size); 42 | } 43 | else 44 | throw new System.Exception($"No object with ID {iD}"); 45 | 46 | } 47 | 48 | public void EndState() 49 | { 50 | previewSystem.StopShowingPreview(); 51 | } 52 | 53 | public void OnAction(Vector3Int gridPosition) 54 | { 55 | 56 | bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjectIndex); 57 | if (placementValidity == false) 58 | { 59 | soundFeedback.PlaySound(SoundType.wrongPlacement); 60 | return; 61 | } 62 | soundFeedback.PlaySound(SoundType.Place); 63 | int index = objectPlacer.PlaceObject(database.objectsData[selectedObjectIndex].Prefab, 64 | grid.CellToWorld(gridPosition)); 65 | 66 | GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ? 67 | floorData : 68 | furnitureData; 69 | selectedData.AddObjectAt(gridPosition, 70 | database.objectsData[selectedObjectIndex].Size, 71 | database.objectsData[selectedObjectIndex].ID, 72 | index); 73 | 74 | previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), false); 75 | } 76 | 77 | private bool CheckPlacementValidity(Vector3Int gridPosition, int selectedObjectIndex) 78 | { 79 | GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ? 80 | floorData : 81 | furnitureData; 82 | 83 | return selectedData.CanPlaceObejctAt(gridPosition, database.objectsData[selectedObjectIndex].Size); 84 | } 85 | 86 | public void UpdateState(Vector3Int gridPosition) 87 | { 88 | bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjectIndex); 89 | 90 | previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), placementValidity); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /PlacementSystem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Unity.VisualScripting; 5 | using UnityEngine; 6 | 7 | public class PlacementSystem : MonoBehaviour 8 | { 9 | [SerializeField] 10 | private InputManager inputManager; 11 | [SerializeField] 12 | private Grid grid; 13 | 14 | [SerializeField] 15 | private ObjectsDatabaseSO database; 16 | 17 | [SerializeField] 18 | private GameObject gridVisualization; 19 | 20 | [SerializeField] 21 | private AudioClip correctPlacementClip, wrongPlacementClip; 22 | [SerializeField] 23 | private AudioSource source; 24 | 25 | private GridData floorData, furnitureData; 26 | 27 | [SerializeField] 28 | private PreviewSystem preview; 29 | 30 | private Vector3Int lastDetectedPosition = Vector3Int.zero; 31 | 32 | [SerializeField] 33 | private ObjectPlacer objectPlacer; 34 | 35 | IBuildingState buildingState; 36 | 37 | [SerializeField] 38 | private SoundFeedback soundFeedback; 39 | 40 | private void Start() 41 | { 42 | gridVisualization.SetActive(false); 43 | floorData = new(); 44 | furnitureData = new(); 45 | } 46 | 47 | public void StartPlacement(int ID) 48 | { 49 | StopPlacement(); 50 | gridVisualization.SetActive(true); 51 | buildingState = new PlacementState(ID, 52 | grid, 53 | preview, 54 | database, 55 | floorData, 56 | furnitureData, 57 | objectPlacer, 58 | soundFeedback); 59 | inputManager.OnClicked += PlaceStructure; 60 | inputManager.OnExit += StopPlacement; 61 | } 62 | 63 | public void StartRemoving() 64 | { 65 | StopPlacement(); 66 | gridVisualization.SetActive(true) ; 67 | buildingState = new RemovingState(grid, preview, floorData, furnitureData, objectPlacer, soundFeedback); 68 | inputManager.OnClicked += PlaceStructure; 69 | inputManager.OnExit += StopPlacement; 70 | } 71 | 72 | private void PlaceStructure() 73 | { 74 | if(inputManager.IsPointerOverUI()) 75 | { 76 | return; 77 | } 78 | Vector3 mousePosition = inputManager.GetSelectedMapPosition(); 79 | Vector3Int gridPosition = grid.WorldToCell(mousePosition); 80 | 81 | buildingState.OnAction(gridPosition); 82 | 83 | } 84 | 85 | //private bool CheckPlacementValidity(Vector3Int gridPosition, int selectedObjectIndex) 86 | //{ 87 | // GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ? 88 | // floorData : 89 | // furnitureData; 90 | 91 | // return selectedData.CanPlaceObejctAt(gridPosition, database.objectsData[selectedObjectIndex].Size); 92 | //} 93 | 94 | private void StopPlacement() 95 | { 96 | soundFeedback.PlaySound(SoundType.Click); 97 | if (buildingState == null) 98 | return; 99 | gridVisualization.SetActive(false); 100 | buildingState.EndState(); 101 | inputManager.OnClicked -= PlaceStructure; 102 | inputManager.OnExit -= StopPlacement; 103 | lastDetectedPosition = Vector3Int.zero; 104 | buildingState = null; 105 | } 106 | 107 | private void Update() 108 | { 109 | if (buildingState == null) 110 | return; 111 | Vector3 mousePosition = inputManager.GetSelectedMapPosition(); 112 | Vector3Int gridPosition = grid.WorldToCell(mousePosition); 113 | if(lastDetectedPosition != gridPosition) 114 | { 115 | buildingState.UpdateState(gridPosition); 116 | lastDetectedPosition = gridPosition; 117 | } 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /PreviewSystem.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnityEngine; 3 | 4 | public class PreviewSystem : MonoBehaviour 5 | { 6 | [SerializeField] 7 | private float previewYOffset = 0.06f; 8 | 9 | [SerializeField] 10 | private GameObject cellIndicator; 11 | private GameObject previewObject; 12 | 13 | [SerializeField] 14 | private Material previewMaterialPrefab; 15 | private Material previewMaterialInstance; 16 | 17 | private Renderer cellIndicatorRenderer; 18 | 19 | private void Start() 20 | { 21 | previewMaterialInstance = new Material(previewMaterialPrefab); 22 | cellIndicator.SetActive(false); 23 | cellIndicatorRenderer = cellIndicator.GetComponentInChildren(); 24 | } 25 | 26 | public void StartShowingPlacementPreview(GameObject prefab, Vector2Int size) 27 | { 28 | previewObject = Instantiate(prefab); 29 | PreparePreview(previewObject); 30 | PrepareCursor(size); 31 | cellIndicator.SetActive(true); 32 | } 33 | 34 | private void PrepareCursor(Vector2Int size) 35 | { 36 | if(size.x > 0 || size.y > 0) 37 | { 38 | cellIndicator.transform.localScale = new Vector3(size.x, 1, size.y); 39 | cellIndicatorRenderer.material.mainTextureScale = size; 40 | } 41 | } 42 | 43 | private void PreparePreview(GameObject previewObject) 44 | { 45 | Renderer[] renderers = previewObject.GetComponentsInChildren(); 46 | foreach(Renderer renderer in renderers) 47 | { 48 | Material[] materials = renderer.materials; 49 | for (int i = 0; i < materials.Length; i++) 50 | { 51 | materials[i] = previewMaterialInstance; 52 | } 53 | renderer.materials = materials; 54 | } 55 | } 56 | 57 | public void StopShowingPreview() 58 | { 59 | cellIndicator.SetActive(false ); 60 | if(previewObject!= null) 61 | Destroy(previewObject ); 62 | } 63 | 64 | public void UpdatePosition(Vector3 position, bool validity) 65 | { 66 | if(previewObject != null) 67 | { 68 | MovePreview(position); 69 | ApplyFeedbackToPreview(validity); 70 | 71 | } 72 | 73 | MoveCursor(position); 74 | ApplyFeedbackToCursor(validity); 75 | } 76 | 77 | private void ApplyFeedbackToPreview(bool validity) 78 | { 79 | Color c = validity ? Color.white : Color.red; 80 | 81 | c.a = 0.5f; 82 | previewMaterialInstance.color = c; 83 | } 84 | 85 | private void ApplyFeedbackToCursor(bool validity) 86 | { 87 | Color c = validity ? Color.white : Color.red; 88 | 89 | c.a = 0.5f; 90 | cellIndicatorRenderer.material.color = c; 91 | } 92 | 93 | private void MoveCursor(Vector3 position) 94 | { 95 | cellIndicator.transform.position = position; 96 | } 97 | 98 | private void MovePreview(Vector3 position) 99 | { 100 | previewObject.transform.position = new Vector3( 101 | position.x, 102 | position.y + previewYOffset, 103 | position.z); 104 | } 105 | 106 | internal void StartShowingRemovePreview() 107 | { 108 | cellIndicator.SetActive(true); 109 | PrepareCursor(Vector2Int.one); 110 | ApplyFeedbackToCursor(false); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grid Placement System Unity 2022 2 | [![Tutorial Sectiont 1](http://img.youtube.com/vi/l0emsAHIBjU/hqdefault.jpg)](https://youtu.be/l0emsAHIBjU) 3 |

How to create 3D grid placement system in Unity using the native Grid component (works for 2D as well) 4 | 5 |

Attribution: 6 | Made by Sunny Valley Studio 7 | -------------------------------------------------------------------------------- /RemovingState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | public class RemovingState : IBuildingState 7 | { 8 | private int gameObjectIndex = -1; 9 | Grid grid; 10 | PreviewSystem previewSystem; 11 | GridData floorData; 12 | GridData furnitureData; 13 | ObjectPlacer objectPlacer; 14 | SoundFeedback soundFeedback; 15 | 16 | public RemovingState(Grid grid, 17 | PreviewSystem previewSystem, 18 | GridData floorData, 19 | GridData furnitureData, 20 | ObjectPlacer objectPlacer, 21 | SoundFeedback soundFeedback) 22 | { 23 | this.grid = grid; 24 | this.previewSystem = previewSystem; 25 | this.floorData = floorData; 26 | this.furnitureData = furnitureData; 27 | this.objectPlacer = objectPlacer; 28 | this.soundFeedback = soundFeedback; 29 | previewSystem.StartShowingRemovePreview(); 30 | } 31 | 32 | public void EndState() 33 | { 34 | previewSystem.StopShowingPreview(); 35 | } 36 | 37 | public void OnAction(Vector3Int gridPosition) 38 | { 39 | GridData selectedData = null; 40 | if(furnitureData.CanPlaceObejctAt(gridPosition,Vector2Int.one) == false) 41 | { 42 | selectedData = furnitureData; 43 | } 44 | else if(floorData.CanPlaceObejctAt(gridPosition, Vector2Int.one) == false) 45 | { 46 | selectedData = floorData; 47 | } 48 | 49 | if(selectedData == null) 50 | { 51 | //sound 52 | soundFeedback.PlaySound(SoundType.wrongPlacement); 53 | } 54 | else 55 | { 56 | soundFeedback.PlaySound(SoundType.Remove); 57 | gameObjectIndex = selectedData.GetRepresentationIndex(gridPosition); 58 | if (gameObjectIndex == -1) 59 | return; 60 | selectedData.RemoveObjectAt(gridPosition); 61 | objectPlacer.RemoveObjectAt(gameObjectIndex); 62 | } 63 | Vector3 cellPosition = grid.CellToWorld(gridPosition); 64 | previewSystem.UpdatePosition(cellPosition, CheckIfSelectionIsValid(gridPosition)); 65 | } 66 | 67 | private bool CheckIfSelectionIsValid(Vector3Int gridPosition) 68 | { 69 | return !(furnitureData.CanPlaceObejctAt(gridPosition, Vector2Int.one) && 70 | floorData.CanPlaceObejctAt(gridPosition, Vector2Int.one)); 71 | } 72 | 73 | public void UpdateState(Vector3Int gridPosition) 74 | { 75 | bool validity = CheckIfSelectionIsValid(gridPosition); 76 | previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), validity); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /SoundFeedback.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public class SoundFeedback : MonoBehaviour 6 | { 7 | [SerializeField] 8 | private AudioClip clickSound, placeSound, removeSound, wrongPlacementSound; 9 | 10 | [SerializeField] 11 | private AudioSource audioSource; 12 | 13 | public void PlaySound(SoundType soundType) 14 | { 15 | switch (soundType) 16 | { 17 | case SoundType.Click: 18 | audioSource.PlayOneShot(clickSound); 19 | break; 20 | case SoundType.Place: 21 | audioSource.PlayOneShot(placeSound); 22 | break; 23 | case SoundType.Remove: 24 | audioSource.PlayOneShot(removeSound); 25 | break; 26 | case SoundType.wrongPlacement: 27 | audioSource.PlayOneShot(wrongPlacementSound); 28 | break; 29 | default: 30 | break; 31 | } 32 | } 33 | } 34 | 35 | public enum SoundType 36 | { 37 | Click, 38 | Place, 39 | Remove, 40 | wrongPlacement 41 | } 42 | --------------------------------------------------------------------------------