2026. 3. 10. 18:35ㆍUnity
안녕하세요!
최근 소속된 인디 게임 개발 팀 'Unitrio'에서 2D 탑다운 게임 프로젝트를 한창 진행하고 있습니다.
요즘 개발을 하면서 가장 골머리를 앓았던 이슈가 하나 있었는데요.
바로 맵에 몬스터를 수십 마리 풀었더니 프레임이 뚝뚝 떨어지는 최적화 문제였습니다.
원인을 분석해 보니, 몬스터들이 플레이어를 쫓아가기 위해 매 프레임마다 무거운 A* 알고리즘을 돌리고 있더라고요.
"어떻게 하면 몬스터를 바보로 만들지 않으면서 연산량을 줄일 수 있을까?"
치열한 리서치와 테스트 끝에, 이번 프로젝트에 최종 적용하기로 한 실무 표준 최적화 기법,
FSM + Raycast + A* 하이브리드 추적 AI 구축 과정을 제 개인 개발일지에 공유해 봅니다.
1. A* 알고리즘은 생각보다 무겁다.
A* 알고리즘은 타일맵 기반의 2D 게임에서 목적지까지의 최단 경로를 찾아주는 훌륭한 알고리즘입니다.
하지만 몬스터 수십 마리가 매 프레임마다 길찾기 연산을 수행한다면 CPU는 금방 비명을 지르게 됩니다.
그래서 몬스터의 역할을 아래와 같이 뇌, 눈, 다리 세 가지로 완벽하게 분업화하기로 했습니다.
- FSM (뇌): 몬스터의 현재 상태(순찰, 추적 등)를 통제하는 뼈대.
- Raycast (눈): 몬스터와 플레이어 사이의 장애물을 판별하는 가장 가벼운 연산.
- A* & Vector (다리): 시야 확보 여부에 따라 '단순 직선 이동'을 할지 'A* 우회'를 할지 결정.
2. 보이면 뛰고, 안 보이면 길을 찾아라
제가 고안한 하이브리드 AI의 목표는 단 하나입니다.
무거운 A 알고리즘을 최대한 안 쓰는 것이죠.
몬스터가 플레이어를 발견해 추적 상태에 돌입하면,
무작정 길찾기를 시작하는 대신 플레이어를 향해 투명한 레이저를 쏩니다.
- 상황 A (시야가 뚫림): 중간에 벽이 없다면 A* 연산을 아예 끄고, 방향 벡터만 구해서 직선으로 뛰어갑니다. 연산량이 사실상 상수 시간 O(1)에 수렴합니다.
- 상황 B (벽에 가려짐): 플레이어가 코너로 숨어 레이저가 벽에 막히면, 그때서야 비로소 A*를 호출해 우회 경로를 계산합니다.
저희 게임 플레이의 80% 이상은 시야가 트인 공간에서 이루어집니다.
이 구조를 도입하기만 해도 프로젝트의 CPU 오버헤드를 눈에 띄게 줄일 수 있게 되었습니다.
3. 실제 프로젝트에 적용한 C# 핵심 코드
매 프레임마다 Update를 통해 시야를 검사하는 것도 낭비라고 판단하여,
코루틴을 사용해 0.2초마다 AI가 상황을 판단하도록 최적화했습니다.
제가 다듬은 뼈대 코드를 공개합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterAI : MonoBehaviour
{
public enum State { Idle, Patrol, Chase, Attack }
public State currentState = State.Idle;
public Transform player;
public LayerMask targetLayerMask;
public float moveSpeed = 3f;
private bool isPathfinding = false;
void Start()
{
StartCoroutine(UpdateChaseLogicRoutine());
}
IEnumerator UpdateChaseLogicRoutine()
{
while (true)
{
if (currentState == State.Chase && player != null)
{
PerformHybridChase();
}
yield return new WaitForSeconds(0.2f); // 0.2초마다 추적 판단을 가동하는 코루틴
}
}
void PerformHybridChase()
{
Vector2 dirToPlayer = (player.position - transform.position).normalized;
float distToPlayer = Vector2.Distance(transform.position, player.position);
RaycastHit2D hit = Physics2D.Raycast(transform.position, dirToPlayer, distToPlayer, targetLayerMask);
if (hit.collider != null && hit.collider.CompareTag("Player"))
{
isPathfinding = false;
MoveTowards(player.position);
}
else
{
if (!isPathfinding)
{
isPathfinding = true;
// AStarManager.Instance.GetPath(transform.position, player.position); 호출
}
FollowAStarPath();
}
}
void MoveTowards(Vector2 targetPos)
{
transform.position = Vector2.MoveTowards(transform.position, targetPos, moveSpeed * Time.deltaTime);
}
void FollowAStarPath()
{
// A* 경로를 순차적으로 따라가는 로직 구현부
}
}
4. 개발 과정에서 얻은 소소한 팁
몬스터의 떨림 현상 방지: 코너를 돌 때 1프레임 단위로 시야가 닿았다 안 닿았다를 반복하면 몬스터가 덜덜 떱니다.
위 코드처럼 코루틴 주기를 .2초 정도로 두어 딜레이를 주면 훨씬 묵직하고 자연스러운 움직임이 완성됩니다.
이 방식 덕분에 Unitrio 프로젝트의 몬스터 AI 최적화 큰 산을 하나 넘었습니다.
긴 글 읽어주셔서 감사합니다!
Unitrio가 출시할 게임을 기대해주세요!