DEV NOTE · ECHO

소리를 듣는 놈과 빛을 보는 놈: ECHO 사냥꾼 AI 설계

ECHO에서 플레이어가 세상을 보는 수단은 두 가지다. 음파(핑)와 손전등. 사냥꾼 AI는 이 두 수단이 각각 대가를 갖도록 설계했다. 핑을 쓰면 귀 사냥꾼이 듣고, 손전등을 켜 두면 눈 사냥꾼이 본다. 이 글은 그 규칙이 코드에서 어떻게 계산되는지, 왜 그렇게 정했는지를 정리한다.

핑 하나가 세 가지 일을 한다

핑을 보내면 플레이어 위치에서 원형 파면이 초속 14칸으로 퍼진다. 최대 반경은 9칸, 쿨다운은 0.8초다. 시뮬레이션(world.js)은 핑을 보내는 순간 반경 안의 모든 타일에 대해 DDA로 시야를 검사하고, 보이는 타일마다 "파면이 도착하는 시각"을 미리 계산해 기록한다. 이후 매 프레임 현재 시각이 그 값을 넘은 타일만 밝아진다. 그래서 가까운 벽부터 먼 벽 순서로 윤곽이 켜지고, 9칸 끝까지 가는 데 약 0.64초가 걸린다.

같은 계산이 사냥꾼에게도 적용된다. 시야가 닿는 사냥꾼은 파면이 닿은 순간의 위치와 방향을 잔상으로 남긴다. 그리고 시야와 무관하게, 반경 안의 사냥꾼은 그 소리를 "듣는다". 듣는 시각 역시 즉시가 아니라 발신 시각 + 거리 / 14다. 멀리 있는 사냥꾼일수록 늦게 반응하고, 그 차이가 화면에서 파면이 지나가는 타이밍과 정확히 일치한다. 파면이 사냥꾼 쪽에 닿는 순간 그 사냥꾼의 눈빛이 붉어지고 경계음이 울리기 때문에, 플레이어는 "내 소리가 저기까지 갔구나"를 눈으로 확인할 수 있다.

// world.js 발췌: 소리를 들을 사냥꾼과 들을 시각을 예약
for(const h of w.hunters){
  const d=Math.hypot(h.x-x,h.y-y);
  if(d>r*(h.kind==='eye'?EYE_HEAR:1)) continue;
  const at=t0+d/PING_V;
  if(at<h.hearAt||h.hearAt<=w.t){ h.hearAt=at; h.hearPos={x,y}; }
}

시야 검사 없이 거리만 보는 것은 의도한 부분이다. 벽은 빛을 막지만 소리는 막지 못한다는 직관을 그대로 따랐다. 벽 너머에서 보낸 핑에도 사냥꾼이 달려오기 때문에, 플레이어는 핑을 보내기 전에 주변 9칸에 누가 있는지 먼저 생각해야 한다.

상태기계: 순찰, 경계, 수색

사냥꾼의 상태는 세 가지뿐이다. 순찰(PATROL)은 같은 방 안의 바닥 2~4칸을 경유지로 삼아 초당 2.0칸으로 돌아다니고, 경유지마다 0.8~2.0초씩 멈춘다. 멈추는 시간은 사냥꾼 번호와 경유지 순번으로 정해져서 여러 사냥꾼이 같은 박자로 움직이지 않는다.

소리를 들으면 경계(ALERT)가 되어 소리가 난 칸까지 BFS 경로로 달려간다. 경계 속도는 1층 초당 3.1칸에서 층마다 0.15씩 빨라져 최대 4.2칸이다. 플레이어 속도가 초당 3.4칸이므로 3층에서 속도가 같아지고 4층부터는 사냥꾼이 더 빠르다. 초반 두 층에서는 들켜도 달려서 거리를 벌릴 수 있지만, 그 뒤로는 애초에 들키지 않는 쪽이 유일한 해법이 된다.

목표 지점에 도착하면 수색(SEARCH)으로 바뀌어 2.5초 동안 제자리에서 기다린다. 그 사이 새 소리가 들리면 다시 경계가 되고, 아무 소리도 없으면 순찰로 돌아간다. 여기에 두 가지 예외 규칙이 있다. 1.5칸 안에서 시야가 닿으면 소리 없이도 즉시 경계가 되어 플레이어 칸을 목표로 잡는다. 그리고 0.6칸 안으로 들어오면 잡힌다. 1.5칸 규칙은 핑을 전혀 쓰지 않고 사냥꾼 바로 옆을 스쳐 지나가는 플레이를 막는 장치다. 소리를 내지 않아도 코앞까지 다가가면 들킨다.

사냥꾼은 플레이어를 직접 추적하지 않는다. 경계 상태에서도 목표는 "마지막으로 들은 소리의 위치"다. 핑을 보낸 직후 자리를 옮기면 사냥꾼은 빈자리에 도착해 2.5초를 헤맨다. 이 단순한 규칙 덕분에 "소리로 유인하고 반대쪽으로 빠지는" 전술이 성립한다.

두 번째 사냥꾼이 필요했던 이유

2.0에서 손전등이 생기자 균형이 무너질 위험이 있었다. 손전등은 소리를 내지 않으니, 귀 사냥꾼만 있다면 손전등으로 앞만 보고 걸으면서 핑을 거의 쓰지 않는 플레이가 최적이 된다. 그러면 에코로케이션 게임이라는 정체성이 흐려진다. 설계 문서의 첫 측정 항목이 "손전등이 있어도 층당 평균 핑 수가 1.0의 60% 이상 유지되는가"인 것도 이 걱정 때문이다.

답으로 넣은 것이 빛을 보는 눈 사냥꾼이다. 눈 사냥꾼은 귀가 어두워 소리를 귀 사냥꾼의 55% 거리까지만 듣는다(핑이라면 약 5칸). 대신 손전등이 켜져 있고, 자기가 손전등 원뿔(8칸, 반각 0.5rad) 안에 있고, 플레이어와 시야가 닿으면 "보고 있는 시간"이 쌓인다. 이 값이 0.35초를 넘으면 경계로 바뀌어 플레이어 위치로 달려온다. 원뿔 밖으로 나가면 쌓인 시간이 같은 속도로 줄어든다.

// world.js 발췌: 눈 사냥꾼의 시각 판정
else if(h.kind==='eye' && w.flash && inCone(w,h.x,h.y)
        && LEVEL.visible(L,px,py,Math.floor(h.x),Math.floor(h.y))){
  h.seesT+=dt;
  if(h.seesT>0.35){ /* 경계 진입, 플레이어 칸을 목표로 */ }
}
else h.seesT=Math.max(0,h.seesT-dt);

0.35초는 짧은 유예다. 눈 사냥꾼이 원뿔 안에서 플레이어를 보기 시작하면 눈빛이 흰색으로 바뀌는데, 이것이 끄라는 신호다. 손전등을 끄면 앞이 손 닿는 거리와 핑으로만 줄어들고, 그 상태에서 핑을 쓰면 이번엔 근처의 귀 사냥꾼이 듣는다. 두 사냥꾼이 같은 층에 있으면 플레이어는 어느 쪽 위험을 감수할지 계속 고르게 된다.

배치 규칙은 level.js의 한 줄로 정해진다. 2층 이상에서 사냥꾼 번호 i와 층 f에 대해 (i+f)%2===0이면 눈, 아니면 귀다. 사냥꾼 수는 min(5, 1 + floor((f−1)/2))이라 2층에는 한 마리뿐인데, 계산하면 그 한 마리가 눈 사냥꾼이 된다. 그래서 2층에서는 새로 등장한 눈 사냥꾼을 다른 사냥꾼 없이 단독으로 만나게 된다. 3층부터는 두 종류가 섞인다.

보이지 않는 적을 어떻게 보여 줄까

규칙만큼 중요한 것은 표현이다. 사냥꾼이 훤히 보이면 공포가 사라지고, 아예 안 보이면 억울한 죽음이 된다. ECHO는 사냥꾼의 몸을 조명을 무시하는 순수 검정 재질(MeshBasicMaterial)로 그린다. 손전등을 받아도 밝아지지 않고, 빛이 닿은 바닥이나 벽을 등지고 섰을 때만 윤곽이 드러난다.

대신 정보를 주는 채널을 세 개 열어 두었다. 첫째는 눈빛이다. 시야가 닿고 9칸 안에 있을 때만 보이며 가까울수록 밝다. 순찰 중엔 마젠타, 경계 중엔 빨강, 손전등을 보고 있는 눈 사냥꾼은 흰색이다. 둘째는 사냥꾼이 스스로 내는 파동이다. 순찰 중엔 1.6초, 경계 중엔 0.6초마다 바닥에 링이 퍼지는데, 이 링은 벽을 통과해 보인다. 링 색과 간격만으로 "어디에 있고 나를 쫓고 있는지"를 알 수 있다. 셋째는 핑 잔상이다. 핑에 맞은 순간의 위치에 반투명 마젠타 덩어리가 남아 2.5초 시정수로 사라진다. 잔상은 과거의 위치라서, 플레이어는 그 사이 사냥꾼이 어디로 갔을지 추측해야 한다.

ECHO 3D 화면. 어두운 던전에서 랜턴을 든 캐릭터 주변으로 시안색 벽 윤곽과 바닥 링이 보인다
사냥꾼의 몸은 검정이라 직접 보이지 않는다. 바닥의 링과 눈빛이 위치를 알려 주는 주요 단서다.

측정할 것과 조정 기준

이 규칙들은 아직 가설이다. 설계 문서의 측정 계획에는 사냥꾼과 관련된 기준이 두 개 있다. 층당 평균 핑 수가 1.0 대비 60% 아래로 떨어지면 손전등 거리를 8칸에서 6칸으로 줄인다. 그리고 눈 사냥꾼과 마주쳤을 때 플레이어가 손전등을 끄는 비율이 30%에 못 미치면, 0.35초 유예를 0.6초로 늘려 반응할 시간을 더 준다. 끄는 행동 자체에 점수를 주는 방안도 검토했지만 넣지 않았다. 눈 사냥꾼 앞에서 끄면 살아남는다는 것이 이미 충분한 보상이고, 점수를 붙이면 의미 없이 껐다 켜는 행동을 부추길 수 있기 때문이다.

정리

ECHO의 사냥꾼 AI는 복잡하지 않다. 상태 세 개와 감지 규칙 몇 줄이 전부다. 그 대신 모든 감지가 플레이어가 화면에서 확인할 수 있는 물리량, 즉 파면의 도착 시각과 손전등 원뿔에 묶여 있다. 사냥꾼이 왜 나를 쫓아왔는지 플레이어가 항상 설명할 수 있어야 한다는 것이 이 설계의 기준이다. 다음 단계는 실제 플레이 기록으로 핑 수와 손전등 OFF 비율을 측정해 위 두 수치를 검증하는 일이다.