Placement System
Core Mechanics
At the core of this project is the TowerPlacementManager. This system enables players to intuitively place defensive towers with real-time visual feedback reflecting placement validity.
The preview dynamically toggles material colors (green for valid, red for obstructed) based on Physics collision checks, delivering a smooth user experience.
void HandlePlacementPreview()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 1000f, groundLayer))
{
Vector3 placementPos = hit.point;
// Snap coordinates to 1x1 grid
placementPos = SnapToGrid(placementPos);
towerPreview.transform.position = placementPos;
// Check if placement is valid
bool isValid = IsValidPlacement(placementPos);
UpdatePreviewColor(isValid);
}
}
Grid Snapping Algorithm
PrecisionTo ensure strict architectural alignment, the system utilizes a custom snapping algorithm. This locks all tower placements onto a precise 1x1 unit grid, which is crucial for tactical maze building and enemy pathfinding.
Vector3 SnapToGrid(Vector3 position)
{
float gridSize = 1f;
float x = Mathf.Round(position.x / gridSize) * gridSize;
float z = Mathf.Round(position.z / gridSize) * gridSize;
return new Vector3(x, position.y, z);
}
Placement Validation
Physics
Validation is calculated via Physics.OverlapSphere, detecting whether a proposed tower location overlaps existing obstacles or defensive structures, preventing stacking bugs.
bool IsValidPlacement(Vector3 position)
{
// Check if position is blocked by other towers
Collider[] colliders = Physics.OverlapSphere(position, 0.5f);
foreach (var col in colliders)
{
if (col.CompareTag("Tower"))
return false;
}
return true;
}
Full Manager Architecture
C# ScriptThe full C# component managing preview instantiation, input polling, placement confirmation, and cancellation state transitions:
using UnityEngine;
public class TowerPlacementManager : MonoBehaviour
{
[SerializeField] private LayerMask groundLayer;
[SerializeField] private GameObject towerPrefab;
[SerializeField] private int towerCost = 100;
[SerializeField] private Material previewMaterial;
[SerializeField] private Color validColor = new Color(0, 1, 0, 0.5f);
[SerializeField] private Color invalidColor = new Color(1, 0, 0, 0.5f);
private GameObject towerPreview;
private bool isPlacementMode = false;
private Renderer[] previewRenderers;
void Update()
{
if (isPlacementMode)
{
HandlePlacementPreview();
if (Input.GetMouseButtonDown(0))
{
TryPlaceTower();
}
if (Input.GetMouseButtonDown(1) || Input.GetKeyDown(KeyCode.Escape))
{
ExitPlacementMode();
}
}
}
public void EnterPlacementMode(GameObject prefab, int cost)
{
if (!GameManager.Instance.CanAfford(cost)) return;
isPlacementMode = true;
towerPrefab = prefab;
towerCost = cost;
towerPreview = Instantiate(prefab);
SetPreviewMaterial(towerPreview);
UpdatePreviewPosition();
}
private void UpdatePreviewPosition()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 1000f, groundLayer))
{
Vector3 pos = SnapToGrid(hit.point);
towerPreview.transform.position = pos;
UpdatePreviewColor(IsValidPlacement(pos));
}
}
}