[2025_12_04]탑뷰 2d 게임 유닛 이동 AI개선
2025. 12. 4. 22:09ㆍTIL
목표
- 아군 유닛끼리 충돌 회피하며 자연스럽게 이동
- 앞 유닛이 막혀있어도 우회 가능
- 이동 중 타겟 발견 시 즉시 공격
1. 초기 코드 (기본 이동)
구조:
csharp
// UnitMovement.cs
- targetPosition 있으면 추적
- 없으면 moveDirection으로 직진
- Stop()/Resume()으로 전투 제어
문제:
- 유닛들이 서로 막힘
- 앞 유닛이 Kinematic(공격 중)이면 뒤 유닛 이동 불가
- 우회 로직 없음
2. Flocking(Boids) 알고리즘 시도
핵심 개념:
Boids = Bird-oid objects (Craig Reynolds, 1986)
3가지 규칙:
- Separation (분리): 너무 가까우면 떨어지기
- Alignment (정렬): 이웃과 같은 방향으로
- Cohesion (응집): 그룹 중심으로 모이기
구현:
csharp
Vector2 flockingForce =
separation * separationWeight +
alignment * alignmentWeight +
cohesion * cohesionWeight +
target * targetWeight;
rb.velocity = flockingForce.normalized * unit.Data.MoveSpeed;
문제:
- `unit == null` 에러 -> Init() 전에 FixedUpdate() 실행됨
- 해결: `isInitialized` 플래그 추가
결론:
- 군집 이동은 되지만 막혔을 때 해결 안 됨
- 방향 전환이 원하는 대로 안 됨
3. 8방향 Raycast + 가중치 회피 (최종 사용)
왜 Flocking을 버렸나?
- 게임에서는 "어디가 막혔고, 어디로 갈 수 있는지" 명확한 판단 필요
- 튜닝이 어렵고 예측 불가능한 동작 발생
핵심 알고리즘: Context Steering
방향별로 점수를 매겨서 최적 경로를 선택하는 방식
동작 원리:
1. 8방향(0°, 45°, 90°, ..., 315°) Raycast
2. 각 방향마다:
- 아군 있으면: 막힘 (가중치 -1)
- 비어있으면: 목표 방향과의 유사도 계산 (내적)
3. 가중치 높은 순서대로 정렬
4. 최고 가중치 방향 선택
구현:
csharp
// 8방향 초기화
for (int i = 0; i < 8; i++)
{
float angle = i * 45f * Mathf.Deg2Rad;
directions[i] = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
}
// 아군만 체크 (적/넥서스는 무시)
RaycastHit2D[] hits = Physics2D.CircleCastAll(
transform.position,
rayThickness,
directions[i],
rayDistance,
allyLayer // PlayerUnit 또는 EnemyUnit
);
// 가중치 계산 (내적 활용)
float similarity = Vector2.Dot(directions[i], desiredDirection);
weights[i] = (similarity + 1f) / 2f; // -1~1 -> 0~1
핵심 설정:
- rayDistance: 1.5f
- rayThickness: 0.3f (CircleCast로 두께 있는 Ray)
- allyLayer: 아군만 감지 (적은 그냥 통과)
핵심 요소: 아군만 회피
- 적 유닛/넥서스: Raycast 무시 -> 그냥 통과
- 아군 유닛: Raycast 감지 -> 회피
- 이유: 전투는 UnitCombat에서 처리 (사거리 체크 -> Stop)
장점:
- 명확한 동작, 예측 가능
- 디버깅 쉬움 (8방향 Gizmos)
- 게임에 적합한 즉각적인 반응
남은 문제:
- 최고 가중치 방향이 막혀있으면 계속 못 움직임
- -> 4번 Stuck Detection으로 해결
4. Stuck Detection + 차선책 시스템
문제:
- 최고 가중치 방향이 막혀있으면 계속 못 움직임
해결책: 가중치 순차 시도
csharp
// 0.3초마다 이동 거리 체크
if (distanceMoved < 0.1f)
{
currentFallbackLevel++; // 차선책 레벨 증가
}
// 가중치 순서대로 정렬
var sortedIndices = Enumerable.Range(0, 8)
.Where(i => weights[i] >= 0) // 막히지 않은 것만
.OrderByDescending(i => weights[i])
.ToList();
// 차선책 레벨에 따라 선택
int selectedIndex = Mathf.Min(currentFallbackLevel, sortedIndices.Count - 1);
int bestIndex = sortedIndices[selectedIndex];
동작:
[프레임 1-10] 최고 가중치(0.9) 방향 시도
[막힘 감지] currentFallbackLevel = 1
[프레임 11-20] 2번째 가중치(0.8) 방향 시도
[막힘 감지] currentFallbackLevel = 2
[프레임 21-] 3번째 가중치(0.7) 방향 시도
[이동 성공] currentFallbackLevel = 0 (리셋)
핵심 파라미터:
- stuckCheckInterval: 0.3초 (체크 주기)
- stuckThreshold: 0.1f (이동 거리)
- maxFallbackAttempts: 3 (최대 시도)
4. 타겟팅 시스템 개선
문제:
- 처음 타겟한 적만 계속 공격
- 더 가까운 적이 나타나도 타겟 변경 안 됨
해결: 항상 가장 가까운 적
csharp
// 0.2초마다 스캔
scanTimer += Time.deltaTime;
if (scanTimer >= scanInterval)
{
scanTimer = 0f;
FindClosestTarget(); // 매번 가장 가까운 적 찾기
}
// 우선순위: 유닛 -> 넥서스
private void FindClosestTarget()
{
FindClosestUnit(); // 유닛 먼저
if (currentTargetUnit == null)
{
FindClosestNexus(); // 유닛 없으면 넥서스
}
}
```
핵심:
- scanInterval: 0.2초
- 매 스캔마다 전체 재평가
- 단순하고 명확함
핵심 배운 점
1. Flocking vs Context Steering
- Flocking: 자연스럽지만 복잡
- Context Steering: 명확한 동작, 제어 가능
2. Physics2D 팁
- Physics2D.queriesStartInColliders = false (자기 자신 제외)
- CircleCast > Raycast (두께 있는 충돌 감지)
- CircleCastAll + 자기 필터링 (가장 안전)
3. 디버깅 요소
- OnDrawGizmos (실시간 시각화)
최종 코드 구조
UnitMovement.cs
├─ 8방향 Raycast 체크
├─ 가중치 계산 (목표 방향 유사도)
├─ Stuck Detection
└─ 차선책 시스템
UnitCombat.cs
├─ 0.2초마다 타겟 스캔
├─ 가장 가까운 적 찾기
├─ 유닛 우선 -> 넥서스
└─ Stop()/Resume() 제어
최적화 고려사항
프로토타입에서의 구현이기에 최적화에 대한 부분은 제외하고 구현을 하였다. 유닛의 움직임은 어느 정도 개선되었지만... 반대급부로 프레임이 많이 떨어졋다. 최종 프로젝트나 향후 프로젝트에서는 최적화에 대한 고민이 무조건 추가되야한다고 생각한다.
참고 알고리즘
- Boids/Flocking
- Context Steering
'TIL' 카테고리의 다른 글
| [2025_12_08]최종 프로젝트에서 팀 약속 및 기획 설정 (0) | 2025.12.08 |
|---|---|
| [2025_12_05]프로토타입 발표 및 최종 팀 빌딩 (1) | 2025.12.05 |
| [2025_12_03]Flocking(Boids)알고리즘 (0) | 2025.12.03 |
| [2025_12_02]뒷 유닛이 앞 유닛을 미는 문제(2d 물리) (0) | 2025.12.02 |
| [2025_12_01]기획 문서의 해석 (0) | 2025.12.01 |