2D 하이브리드 타워 디펜스 게임의 핵심 시스템 개발 과정
(지금까지의 작업물을 AI가 작성해준 개발일지)
빌드 시스템을 완성한 후, 이제 배치된 타워와 상호작용할 수 있는 시스템이 필요했습니다. 타워를 클릭하면 사거리를 표시하고, 타겟팅 정보를 디버깅할 수 있는 기능을 구현했습니다.
구현 목표:
구현 파일:
TowerSelector.cs - 타워 선택 관리 (Singleton)TowerClickHandler.cs - 타워 클릭 영역 핸들러Tower.cs - 타워 베이스 클래스 (Range Indicator 추가)TowerTargeting.cs - 타겟팅 시스템 (디버그 기능 추가)Tower GameObject (부모)
├─ CircleCollider2D (적 탐지용 - Layer: Tower)
├─ Tower Component (BasicTower, LaserTower 등)
├─ TowerTargeting Component
├─ SpriteRenderer
├─ RangeIndicator (범위 링 - 초기 숨김)
└─ ClickArea (자식 GameObject - 클릭 감지용)
├─ BoxCollider2D (Is Trigger ✅, Layer: Tower)
└─ TowerClickHandler Component
핵심 설계 원칙:
증상:
InvalidOperationException: You are trying to read Input using the UnityEngine.Input class,
but you have switched active Input handling to Input System package.
원인: Unity New Input System 프로젝트에서 Old Input System API 사용
// ❌ Old Input System (작동 안 함)
if (Input.GetMouseButtonDown(0))
해결: New Input System API로 전환
// ✅ New Input System (Unity 6)
using UnityEngine.InputSystem;
if (Mouse.current.leftButton.wasPressedThisFrame)
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
}
증상:
ClickArea의 BoxCollider2D를 클릭해도 반응 없음
CircleCollider2D를 비활성화하면 클릭이 정상 작동
원인: Physics2D.Raycast()는 첫 번째 히트만 반환 → CircleCollider2D가 먼저 감지되어 BoxCollider2D까지 도달하지 못함
초기 시도 (실패):
// ❌ 첫 번째 Collider만 감지
RaycastHit2D hit = Physics2D.Raycast(mousePos, Vector2.zero);
if (hit.collider != null)
{
TowerClickHandler handler = hit.collider.GetComponent<TowerClickHandler>();
// handler는 항상 null (CircleCollider2D가 먼저 잡힘)
}
해결: RaycastAll 사용
// ✅ 모든 Collider 감지 후 TowerClickHandler 찾기
RaycastHit2D[] allHits = Physics2D.RaycastAll(mousePos, Vector2.zero);
TowerClickHandler towerClickHandler = null;
for (int i = 0; i < allHits.Length; i++)
{
TowerClickHandler handler = allHits[i].collider.GetComponent<TowerClickHandler>();
if (handler != null)
{
towerClickHandler = handler;
break;
}
}
배운 점:
하나의 GameObject에 여러 Collider가 있을 때는 RaycastAll() 사용 필수
각 Collider의 용도를 명확히 분리 (적 탐지 vs 클릭 감지)
초기 시도 (실패):
// TowerClickHandler.cs
void OnMouseDown()
{
Debug.Log("Tower clicked!"); // 호출 안 됨
}
원인: OnMouseDown()은 Old Input System에서만 작동. New Input System에서는 비활성화됨.
해결: TowerSelector에서 Update()로 직접 처리
// TowerSelector.cs - Update()에서 직접 입력 처리
void Update()
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
// 빌드 모드 체크
if (buildManager != null && buildManager.IsBuildModeActive)
{
Debug.Log("⚠️ 빌드 모드 활성화 중 - 타워 선택 불가");
return;
}
// Raycast로 클릭 감지
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
RaycastHit2D[] allHits = Physics2D.RaycastAll(mousePos, Vector2.zero);
// TowerClickHandler 찾기
// ...
}
}
초기 구현:
// ❌ GetComponentInParent로 자동 탐색
private Tower parentTower;
void Awake()
{
parentTower = GetComponentInParent<Tower>();
}
개선된 해결책:
// ✅ Inspector에서 직접 할당 (더 명확하고 안전함)
public Tower parentTower; // public으로 변경
void Awake()
{
if (parentTower == null)
{
Debug.LogError($"❌ {gameObject.name}의 parentTower가 할당되지 않았습니다!");
}
}
TowerSelector에서 사용:
Tower tower = towerClickHandler.parentTower; // public 필드 직접 접근
배운 점:
GetComponentInParent()보다 Inspector 할당이 더 명확하고 디버깅 쉬움
복잡한 Hierarchy에서는 자동 탐색보다 명시적 참조가 안전
TowerSelector.cs (Singleton)
public class TowerSelector : MonoBehaviour
{
private static TowerSelector instance;
private Tower currentSelectedTower;
void Awake()
{
// 싱글톤 패턴
if (instance == null)
instance = this;
else
Destroy(gameObject);
}
void Update()
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
// 1. 빌드 모드 체크
BuildManager buildManager = FindObjectOfType<BuildManager>();
if (buildManager != null && buildManager.IsBuildModeActive)
return;
// 2. 마우스 위치 Raycast
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
RaycastHit2D[] allHits = Physics2D.RaycastAll(mousePos, Vector2.zero);
// 3. TowerClickHandler 찾기
TowerClickHandler towerClickHandler = null;
foreach (var hit in allHits)
{
TowerClickHandler handler = hit.collider.GetComponent<TowerClickHandler>();
if (handler != null)
{
towerClickHandler = handler;
break;
}
}
if (towerClickHandler == null)
{
DeselectCurrentTower(); // 빈 곳 클릭 시 선택 해제
return;
}
Tower tower = towerClickHandler.parentTower;
// 4. 같은 타워 클릭 시 토글
if (currentSelectedTower == tower)
{
tower.ToggleRangeIndicator();
if (!tower.isSelected)
currentSelectedTower = null;
}
else
{
// 5. 이전 타워 선택 해제 후 새 타워 선택
if (currentSelectedTower != null)
currentSelectedTower.HideRangeIndicator();
SelectTower(tower);
tower.ShowRangeIndicator();
}
}
}
public static void SelectTower(Tower tower)
{
if (instance == null) return;
instance.currentSelectedTower = tower;
}
public static void DeselectCurrentTower()
{
if (instance == null || instance.currentSelectedTower == null) return;
instance.currentSelectedTower.HideRangeIndicator();
instance.currentSelectedTower = null;
}
}
Tower.cs - Range Indicator 메서드
[Header("Visual")]
public GameObject rangeIndicator; // 범위 링 (자식 오브젝트)
public bool isSelected = false;
public void ShowRangeIndicator()
{
isSelected = true;
if (rangeIndicator != null)
rangeIndicator.SetActive(true);
}
public void HideRangeIndicator()
{
isSelected = false;
if (rangeIndicator != null)
rangeIndicator.SetActive(false);
}
public void ToggleRangeIndicator()
{
isSelected = !isSelected;
if (rangeIndicator != null)
rangeIndicator.SetActive(isSelected);
}
구현 목표
Console에 탐지된 적 목록 출력 (이름, 체력, 거리)
Scene 뷰에 Gizmos로 시각화 (사거리, 탐지된 적, 현재 타겟)
TowerTargeting.cs - 디버그 기능 추가
[Header("Debug")]
public bool showDebugInfo = true;
private List<Enemy> detectedEnemies = new List<Enemy>();
void ScanForEnemies()
{
Collider2D[] enemiesInRange = Physics2D.OverlapCircleAll(
transform.position,
tower.range,
enemyLayer
);
// 탐지된 적 리스트 업데이트
detectedEnemies.Clear();
foreach (var collider in enemiesInRange)
{
Enemy enemy = collider.GetComponent<Enemy>();
if (enemy != null)
detectedEnemies.Add(enemy);
}
// 🔍 Console 디버그 출력
if (showDebugInfo && detectedEnemies.Count > 0)
{
Debug.Log($"🎯 [{tower.towerName}] 탐지된 적: {detectedEnemies.Count}명");
for (int i = 0; i < detectedEnemies.Count; i++)
{
Enemy enemy = detectedEnemies[i];
float distance = Vector3.Distance(transform.position, enemy.transform.position);
Debug.Log($" [{i}] {enemy.name} | 체력: {enemy.currentHealth:F1}/{enemy.maxHealth} | 거리: {distance:F2}");
}
}
// 타겟 선택 로직...
}
// 🎨 Gizmos 시각화
void OnDrawGizmosSelected()
{
if (tower == null) tower = GetComponent<Tower>();
// 빨간 원: 사거리
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, tower.range);
if (Application.isPlaying && detectedEnemies.Count > 0)
{
foreach (Enemy enemy in detectedEnemies)
{
if (enemy != null)
{
// 노란 선: 타워 → 적
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, enemy.transform.position);
// 하늘색 원: 적 위치
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(enemy.transform.position, 0.3f);
}
}
// 초록 선: 현재 타겟 강조
if (tower.CurrentTarget != null)
{
Gizmos.color = Color.green;
Gizmos.DrawLine(transform.position, tower.CurrentTarget.position);
Gizmos.DrawWireSphere(tower.CurrentTarget.position, 0.5f);
}
}
}
public List<Enemy> GetDetectedEnemies()
{
return detectedEnemies;
}
Tower.cs - CurrentTarget Property 추가
protected Transform currentTarget;
// 외부에서 현재 타겟을 읽을 수 있도록 (읽기 전용)
public Transform CurrentTarget => currentTarget;
디버그 출력:
🎯 [BasicTower] 탐지된 적: 1명
[0] TestEnemy | 체력: 100.0/100 | 거리: 5.20
원인 분석:
Physics2D.OverlapCircleAll(position, range) → range 5.0으로 CircleCollider의 경계를 기준으로 감지
Enemy의 Collider 경계가 5.0 이내 → 감지됨 ✅
하지만 Enemy의 Transform 중심은 5.20 → range 체크 실패 ❌
// Tower.cs - Update()
if (currentTarget != null)
{
// ❌ 정확한 거리로 체크 → 5.20 > 5.0 → 타겟 해제
if (Vector3.Distance(transform.position, currentTarget.position) > range)
{
currentTarget = null;
}
}
해결: Tolerance 추가
// ✅ +0.3f tolerance 추가
if (Vector3.Distance(transform.position, currentTarget.position) > range + 0.3f)
{
currentTarget = null;
}
배운 점:
OverlapCircleAll()은 Collider 경계 기준 감지
Transform 중심까지의 거리는 약간 다를 수 있음
Tolerance를 통해 경계 케이스 처리 필요
New Input System 기반 클릭 감지
RaycastAll로 다중 Collider 처리
TowerClickHandler를 통한 부모 Tower 참조
타워 선택 관리
한 번에 하나의 타워만 선택
빌드 모드 중 선택 불가
같은 타워 재클릭 시 토글
Range Indicator
자식 GameObject로 구현
ShowRangeIndicator() / HideRangeIndicator() / ToggleRangeIndicator()
디버그 시스템
Gizmos 시각화:
빨간 원: 사거리
노란 선: 탐지된 적
하늘색 원: 적 위치
초록 선: 현재 타겟 (강조)
GetDetectedEnemies() API
Range Detection 개선
+0.3f tolerance로 경계 케이스 처리
Collider 경계와 Transform 중심 차이 보정
Persistent Scene (DontDestroyOnLoad)
TowerSelector (Singleton)
MainFieldScene
BuildManager
GridCursor
Tilemap (Ground, Build)
Towers (배치 시 동적 생성)
타워 건설: I 키 → 빌드 모드 ON → 1/2/3 키로 타워 선택 → 클릭으로 배치
타워 선택: 타워 클릭 → Range Indicator 표시
선택 해제: 빈 곳 클릭 or 같은 타워 재클릭
디버그: Scene 뷰에서 타워 선택 → Gizmos로 타겟팅 정보 확인
| 문제 | 원인 | 해결 |
|---|---|---|
| Input System 에러 | Old Input System API 사용 | Mouse.current.leftButton.wasPressedThisFrame |
| CircleCollider가 클릭 막음 | Raycast() 첫 히트만 반환 | RaycastAll() + 순회 |
| OnMouseDown 작동 안 함 | New Input System 비호환 | Update()에서 직접 처리 |
| 5.20 거리 공격 안 됨 | Collider 경계 vs Transform 중심 | range + 0.3f tolerance |
협업 도구: ChatGPT (글작성 컨펌), Claude (구현)
저장소: https://github.com/5P2RS5/Computer-Defense