PROJECT REPORT // FILE_TOWER_DEFENSE
File Tower Defense 프로젝트의 코어 시스템 설계를 담당하며, 유니티의 좌표계 특성을 분석한 아키텍처 재설계부터 대규모 객체의 입력 처리, 동적 버프 시스템, 그리고 그리드 기반 배치 로직까지 전반적인 시스템 개선 과정을 기록한 리포트입니다.
1. 시스템 리팩토링 및 개선
1.1 UI(RectTransform)에서 GameObject(Transform) 기반 전환
초기 설계에서는 윈도우 바탕화면의 아이콘 느낌을 살리기 위해 모든 파일 유닛을 UI 시스템(RectTransform)으로 구축했습니다. 그러나 프로젝트가 고도화됨에 따라 다음과 같은 한계에 직면했습니다.
- 좌표계 종속성: 캔버스의 앵커/피벗 설정 및 해상도에 따라 월드 좌표가 상대적으로 변하여 게임 월드 내 정밀한 위치 계산과 사거리 판정이 매우 까다로움.
- 연산 오버헤드 (Canvas Rebuilding): 유닛의 움직임, 호버 효과, 드래그 등의 UI 요소가 갱신될 때마다 Unity 내부에서 Canvas Rebuild가 강제로 유발되어 CPU 프레임 드랍 발생.
- 물리 엔진의 부재: 월드 좌표 기반의 바이러스(적)와 UI 유닛 간 물리 충돌 판정을 위해 매 프레임
Camera.WorldToScreenPoint등의 물리-화면 좌표 변환 함수 호출 비용 발생.
- Anchor/Pivot Dependent
- Frequent Canvas Rebuilds
- Heavy Screen-to-World conversions
- Absolute 2D Coordinates
- Zero Canvas Overhead
- Native Physics 2D & Colliders
이를 해결하기 위해 모든 유닛을 GameObject(Transform) 기반으로 전면 교체하여 월드 좌표계로 통일하였으며, Physics2D와의 직접적인 호환성을 확보하여 불필요한 연산을 제거하고 성능을 극적으로 최적화했습니다.
1.2 GameObjectGridLayout: 커스텀 레이아웃 엔진
화면 해상도에 맞춰 그리드의 간격과 셀 크기를 동적으로 계산하는 레이아웃 엔진을 구축했습니다. 유니티의 GridLayoutGroup은 UI 전용이므로, 일반 월드 공간 오브젝트를 위해 화면 크기에 맞게 자동으로 셀 크기를 스케일링하는 컴포넌트를 직접 개발했습니다.
cellSize = (screenSize - padding*2 - spacing*(n-1)) / n
코드 보기: FitToScreen 로직
// GameObjectGridLayout.cs: 해상도 대응 셀 크기 계산
public void FitToScreen() {
Camera cam = Camera.main;
float height = cam.orthographicSize * 2f; // 오르토그래픽 카메라 높이
float width = height * cam.aspect; // 카메라 종횡비 기반 너비
// 가용 영역에서 패딩과 스페이싱을 제외한 실제 셀 크기 도출
cellSize.x = (width - (padding.x * 2) - (spacing.x * (columns - 1))) / columns;
cellSize.y = (height - (padding.y * 2) - (spacing.y * (rows - 1))) / rows;
// 그리드의 좌상단 시작 월드 좌표 계산
StartPos = new Vector2(-(width / 2) + padding.x + (cellSize.x / 2),
(height / 2) - padding.y - (cellSize.y / 2));
}
2. 파일/바이러스/UI 입력 및 상호작용 시스템
수많은 유닛이 각자 Update()에서 마우스 충돌을 검사하면 연산량이 기하급수적으로 늘어납니다. 이를 방지하기 위해 Mediator 패턴을 활용하여 InputManager가 모든 마우스 입력을 통합 관리하고, IInteractable 인터페이스를 구현한 대상 객체에게만 이벤트를 전송하도록 설계했습니다.
2.1 중앙 집중식 입력 아키텍처
InputManager는 마우스의 상태 머신(InputState)을 기반으로 클릭, 드래그, 다중 선택(Ctrl+드래그)을 판단하여 InteractionHandler를 통해 이벤트를 전파합니다.
graph TD
A[Mouse / Touch Input] --> B[InputManager]
B -->|1. Raycast & UI Check| C{Is Pointer Over UI?}
C -->|Yes| D[Ignore World Interaction / Handle UI]
C -->|No| E[GetInteractableUnderMouse]
E -->|2. Sort by Layer/Order| F[Identify Target IInteractable]
B -->|3. Update InputState| G{InputState}
G -->|Pressing| H[Check Drag Threshold]
G -->|DraggingObject| I[OnDragHandler]
G -->|DraggingBox| J[Multi-Select Drag Box]
B -->|4. Dispatch Events| K[InteractionHandler]
K -->|Trigger Interface Methods| L[IInteractable Objects]
style B fill:#007bff,stroke:#0056b3,color:#fff
style K fill:#28a745,stroke:#1e7e34,color:#fff
style L fill:#17a2b8,stroke:#117a8b,color:#fff
[DEMO] InputManager 기반 마우스 호버 및 클릭 감지
객체 중첩 판정 알고리즘
마우스 아래에 여러 유닛이나 바이러스, 혹은 UI 판정이 겹쳐있을 때, 다음과 같은 우선순위 기준을 사용하여 모호함을 해결합니다.
- UI 레이어 우선:
EventSystem.current.IsPointerOverGameObject()를 통해 UI 상호작용 우선 처리. - 물리 레이어 필터링:
Physics2D.RaycastNonAlloc을 활용하여 쓰레기 메모리(GC Alloc) 없이 감지된 콜라이더 배열을 가져옴. - 정렬 레이어(Sorting Order) 정렬: 감지된 객체들의
SpriteRenderer내Sorting Layer ID및Order in Layer값을 비교하여 카메라와 가장 가까운(최상단에 렌더링된) 오브젝트를 타깃으로 최종 선택.
2.2 IInteractable 인터페이스 기반 확장성
클릭 및 드래그 상호작용이 필요한 모든 인게임 오브젝트(유닛 파일, 바이러스 등)는 IInteractable 인터페이스를 상속받아 유연하게 확장할 수 있습니다.
[DEMO] IInteractable 상속 객체의 실시간 마우스 드래그 & 배치 흐름
코드 보기: IInteractable 인터페이스
public interface IInteractable {
GameObject targetObj { get; }
bool IsSelectable { get; }
bool IsDraggable { get; }
void OnHoverEnter();
void OnHoverExit();
void OnClick();
void OnBeginDrag();
void OnDrag(Vector2 mouseDelta);
void OnEndDrag();
void OnSelected(bool isSelected);
}
3. 그리드 기반 오브젝트 배치 시스템
3.1 FileGrid: 데이터 중심의 지능형 셀 매니저
FileGrid는 단순한 위치 정보 홀더가 아니라, 유닛 배치 상태와 해당 셀에 작용 중인 오라(버프) 목록을 독자적으로 관리하는 지능형 컨테이너입니다.
- HashSet 기반 버프 소스 관리: 현재 그리드 공간에 영향을 주는 버프 제공 유닛의 목록을
HashSet<File_Base>로 관리하여 버프의 중복 적용을 제거하고 $O(1)$의 빠른 조회 속도를 유지합니다. - 유닛 탈부착 시 자동 스탯 갱신: 유닛이 배치되거나 이탈할 때 그리드에 축적된 버프 목록을 분석하여 대상 유닛의 스탯을 실시간으로 갱신해 줍니다.
3.2 원자적 배치 트랜잭션 (Transactional Pattern)
드래그 앤 드롭으로 유닛의 그리드 위치를 옮길 때 발생할 수 있는 데이터 불일치 및 예외 상황을 원자적으로 보장하기 위해 배치 트랜잭션 흐름을 탐색/검증, 성공(Commit), 실패(Rollback)의 3가지 흐름으로 분할 설계하여 안전성을 확보했습니다.
1. 탐색 및 검증 단계 (Search & Validation)
배치 위치의 그리드를 감지하고, 해당 그리드가 배치 가능한 지역인지(장애물이나 다른 유닛 유무) 검사합니다.
sequenceDiagram
autonumber
actor Player
participant IM as InputManager
participant IH as InteractionHandler
participant FGM as FileGridManager
Player->>IM: Drag & Release Unit
IM->>IH: End Drag Event
IH->>FGM: GetGrid(Release Position)
FGM-->>IH: Return Target Grid (New Grid)
IH->>FGM: Validate Placement (New Grid, No Obstacles)
2. 트랜잭션 확정 단계 (Commit - 검증 성공 시)
검증에 성공하면 기존 그리드의 점유를 해제하고, 새 그리드에 유닛을 정식 등록한 후 스탯(버프) 정보를 동적으로 갱신합니다.
sequenceDiagram
autonumber
participant IH as InteractionHandler
participant FG_Old as Old FileGrid
participant FG_New as New FileGrid
participant Unit as File Unit (File_Base)
Note over IH, Unit: Validation Success (Commit)
IH->>FG_Old: RemoveFileUnit() (Clear occupant & remove old buffs)
IH->>FG_New: SetFileUnit(Unit) (Assign occupant & apply new buffs)
IH->>Unit: Commit Position (Smooth Lerp / Snap)
3. 트랜잭션 복구 단계 (Rollback - 검증 실패 시)
새 그리드가 유효하지 않거나 장애물이 감지된 경우, 상태를 복구하고 유닛을 드래그 시작 전 원래의 그리드 위치로 되돌립니다.
sequenceDiagram
autonumber
participant IH as InteractionHandler
participant Unit as File Unit (File_Base)
Note over IH, Unit: Validation Fails (Rollback)
IH->>Unit: Rollback (Return to Old Grid position)
3.3 동적 양방향 좌표 변환 시스템
마우스의 월드 스페이스 좌표와 그리드의 인덱스(Row, Column) 좌표계를 상호 변환하기 위해 오프셋 기반의 연산 로직을 정밀하게 구현했습니다.
3.4 공간 분할 기반 탐색 최적화 (O(1))
마우스를 드래그할 때 가장 가까운 그리드를 찾기 위해 전체 그리드($N \times M$개)를 전수 검사하는 것은 낭비입니다. 이를 극복하기 위해 공간 분할(Spatial Partitioning) 개념을 도입했습니다. 마우스 월드 좌표를 기반으로 연산 $O(1)$ 만에 예상되는 타깃 그리드 인덱스를 수학적으로 산출하고, 해당 인덱스를 중심으로 인접한 3x3 그리드 셀(총 9개)만 가중치(거리 제곱) 계산을 수행하여 탐색 성능을 획기적으로 개선했습니다.
Spatial Partitioning: Searching 9 cells instead of Entire Grids
코드 보기: 공간 분할 탐색 알고리즘
// FileGridManager.cs: 공간 분할 기반 탐색 최적화 로직
public FileGrid GetGrid(Vector2 worldPos) {
// 1. 월드 좌표를 그리드 인덱스로 수학적 변환 (O(1))
if (!WorldToGridIndex(worldPos, out int xCenter, out int yCenter)) return null;
// 2. 인덱스가 유효하면 주변 3x3 영역만 빠르게 검사 (O(1))
if (IsValidGridIndex(xCenter, yCenter)) {
return FindClosestGridInRange(worldPos, xCenter, yCenter);
}
// 화면 영역 밖의 비정상 예외 상황에서만 전체 탐색 수행 (Safety Net)
return FindGlobalClosestGrid(worldPos);
}
private FileGrid FindClosestGridInRange(Vector2 worldPos, int centerX, int centerY) {
float minDistSqr = float.MaxValue;
FileGrid closestGrid = null;
// 중심 기준 3x3 루프
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
int x = centerX + dx;
int y = centerY + dy;
if (IsValidGridIndex(x, y)) {
FileGrid candidate = gridArray[x, y];
// 제곱 거리(sqrMagnitude) 사용으로 비용이 큰 제곱근(Sqrt) 연산 생략
float distSqr = ((Vector2)candidate.transform.position - worldPos).sqrMagnitude;
if (distSqr < minDistSqr) {
minDistSqr = distSqr;
closestGrid = candidate;
}
}
}
}
return closestGrid;
}
3.5 플래그 기반 확장 가능한 검색 시스템
그리드를 검색할 때 ‘비어 있는 곳’, ‘장애물이 없는 곳’, ‘이미 아군이 배치된 곳’ 등 다양한 복합 조건을 비트 플래그 형태로 손쉽게 검색할 수 있도록 가변 플래그 검색 시스템을 설계했습니다.
| 플래그 | 설명 | 사용 예시 및 기대 결과 |
|---|---|---|
Occupied |
현재 파일 유닛이 배치되어 점유 중인 그리드 셀만을 탐색 대상으로 한정합니다. | 이미 설치된 특정 타워의 위치를 추적하거나, 인접한 유닛의 시너지를 계산할 때 활용됩니다. |
NotOccupied |
파일 유닛이 배치되지 않은 비어있는 상태의 그리드 셀만을 필터링하여 검색합니다. | 플레이어가 새로운 파일 유닛을 드래그하여 설치 가능한 빈 공간을 유효성 검사할 때 필수적으로 사용됩니다. |
Obstacle |
시스템 장애물(땅굴 등)이 생성되어 일반적인 유닛 배치가 불가능한 그리드만을 검색합니다. | 맵 파괴 이벤트나 바이러스의 특수 공격으로 인해 생성된 장애물 객체의 위치를 파악할 때 사용됩니다. |
NotObstacle |
장애물이 존재하지 않아 물리적으로 객체 배치가 가능한 클린한 상태의 그리드만을 검색합니다. | 장애물을 피해 안전하게 유닛을 배치하거나, 투사체가 지나갈 수 있는 경로를 계산할 때 필터로 활용됩니다. |
None |
별도의 필터 조건을 적용하지 않고 그리드 레이아웃 내의 모든 셀을 탐색 범위에 포함합니다. | 전체 그리드의 초기화, 일괄 색상 변경, 또는 모든 셀에 대한 거리 기반 전수 조사가 필요할 때 사용됩니다. |
FindFlagGridWorld(pos, NotOccupied, NotObstacle)➔ "유닛이 없고 + 동시에 장애물도 없는" 가장 인접한 유효 그리드를 즉시 검색해 줍니다.
4. 다형성 기반 동적 버프 시스템
4.1 Observer 패턴 기반 버프 자동 전파
파일 유닛이 그리드 상에 배치되거나 이동할 때, 버프 영역을 동적으로 계산하고 전파하기 위해 Observer 패턴 구조를 활용했습니다.
graph TD
A[Place Unit on Grid] --> B{Is Unit a Buff Source?}
B -->|Yes| C[ApplyBuffActive]
C -->|Register Aura Area| D[Skill_BuffMain.ApplyBuffArea]
D -->|For each grid in range| E[Grid.AddBuffSource]
E -->|If grid has occupant| F[ApplyBuffFromSource]
F -->|Apply BuffStat| G[Target Unit.ApplyBuff]
B -->|No| H[ApplyGridBuffs]
H -->|For each source in Grid._activeBuffSources| I[ApplyBuffFromSource]
I -->|Apply BuffStat| J[Placed Unit.ApplyBuff]
style C fill:#007bff,stroke:#0056b3,color:#fff
style H fill:#28a745,stroke:#1e7e34,color:#fff
style G fill:#17a2b8,stroke:#117a8b,color:#fff
style J fill:#17a2b8,stroke:#117a8b,color:#fff
- 버프 전파 흐름:
- 버프 특성을 가진 파일 유닛(예:
.mp3힐링 버프)이 그리드에 배치되면, 자신의 사거리(Aura Area) 내에 존재하는 모든 주변FileGrid셀들을 찾아 자신을 버프 소스로 등록(AddBuffSource(this))합니다. - 이후 새로운 유닛이 해당 버프 영향권 내부의 빈 그리드로 들어올 경우, FileGrid가 즉각 감지하여 보관하고 있던 버프 데이터 목록(
_activeBuffSources)을 진입한 유닛에게 자동으로 갱신/적용(ApplyBuffFromSource)합니다. - 버프 유닛이 죽거나 이동하면, 영향권 내의 모든 그리드에서 버프 소스를 제거(
RemoveBuffSource)하고 즉시 피적용 유닛의 스탯을 원복시킵니다.
- 버프 특성을 가진 파일 유닛(예:
4.2 다형성을 활용한 틱(Tick) 기반 버프 아키텍처
단순 스탯 증가 버프 외에도 주기적인 리젠(HP 회복), 지속 피해(도트 데미지) 등을 단일 코루틴 남발 없이 처리하기 위해 MyBuff 추상 클래스와 BuffData 구조를 결합한 중앙 업데이트 방식을 채택했습니다.
코드 보기: 다형적 버프 실행 루프
// BuffData_Base.cs: 다형적 버프 업데이트 루프
public virtual void Tick(float deltaTime) {
// 딕셔너리 순회 중 요소 삭제 에러 방지를 위해 복사본 리스트로 순회
foreach (int key in new List<int>(activeBuffs.Keys)) {
// BuffData 내부에서 누적 시간을 체크하여 틱 주기 도달 시 true 반환
if (activeBuffs[key].Tick(deltaTime)) {
OnTick(activeBuffs[key]); // 다형성에 의해 상속받은 자식 클래스의 고유 로직 실행
}
}
}
// Buff_MP3.cs: 구체적인 힐링(HP 지속 회복) 버프 클래스 구현
public class Buff_MP3 : MyBuff {
public override void Init(Transform _target) {
buffTarget = _target;
buffParticleType = Define.ParticleType.Buff_MP3;
}
protected override void OnTick(BuffData buffData) {
// 대상 오브젝트의 IHealth 인터페이스를 찾아 체력 회복
var health = buffTarget.GetComponent<IHealth>();
if (health != null) {
health.Heal(buffData.Amount);
}
}
}
5. 전체 시스템 기술적 특징 요약
- 성능 최적화 (Optimization): UI 구조에서 물리 공간 오브젝트 구조로 전면 교체하여 Canvas Rebuilding 부하를 0으로 격리시켰으며, 마우스 드래그 시 인접 3x3 영역만 거리를 탐색하는 공간 분할 알고리즘으로 불필요한 연산을 대폭 감축했습니다.
- 지능형 상호작용 (Mediator Pattern): 개별 유닛의 업데이트 루프에 의존하지 않는 중앙 집중식
InputManager를 구축하고 렌더링 우선순위 판정을 수식화하여, 깔끔하게 설계된IInteractable객체 이벤트 구조를 실현했습니다. - 데이터 무결성 (Transaction-Safe): 배치 취소, 배치 위치 이동, 장애물 충돌 등으로 발생 가능한 데이터 오차를 예방하기 위해 사전 예외 검증과 배치 롤백이 보장되는 원자적 배치 단계를 구현했습니다.
- 유연한 확장성 (Extensibility): 비트 필터링 느낌의 플래그 기반 그리드 검색 시스템과 다형성 버프 구조를 구현하여, 향후 새로운 유닛 유형이나 기믹이 추가되더라도 기존 코드를 크게 수정하지 않고 확장할 수 있는 견고한 OOP 기반 아키텍처를 확보했습니다.
기술적 성과: 윈도우 바탕화면의 파일 정렬 및 압축(Folder ZIP) 등의 조작 감성을 유니티 엔진 상에서 고성능으로 재해석하였으며, 대규모 유닛과 바이러스가 중첩되는 난전 상황 속에서도 지속적인 60 FPS 이상을 안정적으로 방어하는 고품질 인디 게임 코어 시스템을 완성했습니다.