모딩/마인크래프트 모드 개발 일지

[마크 모딩 삽질기] #8 주민아, 연애 좀 해! (feat. 뇌 제어 실패기)

kimchangmin02 2025. 6. 27. 22:10

삽질기를 하나 풀어볼까 함.
목표는 아주 심플하고 낭만적이었음.

"주민한테 빵을 던져주면, 그 빵을 향해 달려가고! 빵을 주운 주민은 운명의 상대를 찾아 또 달려가서! 둘이 만나면 하트 뿅뿅 아기를 낳게 하자!"

크으... 완벽한 시나리오 아님?
"그냥 Goal 하나 만들어서 순서대로 시키면 되겠지?" 라고 생각했던 과거의 나에게 심심한 애도를 표함.


1단계: 야심 찬 통합 AI 설계 (그리고 와장창)

처음엔 모든 걸 한 방에 해결하는 '만능 Goal'을 만들려고 했음. 이름하여 FindPartnerAndBreedGoal.
대충 이런 느낌으로 코드를 짰음. (의사 코드 주의)

Generated java

// 내 머릿속 완벽한 로직
public void tick() { // 매 틱(0.05초)마다 실행되는 부분

    // 1. 짝이 없으면? 당장 찾아!
    if (짝이_없다) {
        findNearbyPartner(); // 주변 스캔해서 짝꿍 찾기
        return; // 일단 찾았으니 이번 틱은 끝
    }

    // 2. 짝을 찾았으면? 일단 그윽하게 쳐다봐주고
    주민.시선고정(짝꿍);

    // 3. 짝을 향해 돌진!
    주민.네비게이션.이동시켜(짝꿍); // 여기가 바로 주민을 움직이는 핵심 코드!

    // 4. 거리가 3블록 안으로 가까워졌나?
    if (주민과_짝꿍_거리가_가깝다) {
        // 5. 그럼 사랑을 나눠라! (교배 시작)
        forceBreed();
    }
}
Use code with caution.Java

캬, 완벽하지 않음? 주민이 알아서 척척 움직이고 교배까지 할 것 같았음.
근데 결과는?

대참사.


2단계: 버그의 향연 (feat. 무한증식과 돌부처)

두 가지 심각한 문제가 터졌음.

문제 1: 공포의 무한증식 버그 😱

빵을 한 번 던졌을 뿐인데... 아기 주민이 10초마다 계속 태어남. 서버 로그를 까보니 가관이었음.

Generated log

[11:46:42] ... 주민이 짝을 찾았습니다!
[11:46:42] ... 주민이 교배를 시도합니다!
[11:46:42] ... 아기 주민이 태어났습니다!
(10초 후)
[11:46:52] ... 주민이 교배를 시도합니다!
[11:46:52] ... 아기 주민이 태어났습니다!
(또 10초 후)
[11:46:52] ... 주민이 교배를 시도합니다!
... (이하 반복) ...
Use code with caution.Log

원인은 간단했음.
내 코드는 주민 둘이 만나면 forceBreed()를 호출함. 근데 이 Goal 자체가 끝나질 않음.
두 주민은 계속 붙어있으니, 매 틱마다 "가깝네? 교배해!" 명령을 계속 내리고 있었던 거임.

쉽게 말해: "5초 뒤에 아기 낳아!" 라는 예약 문자를 1초에 20번씩 보내는 스팸봇을 만들어버린 셈.

문제 2: 움직이질 않는 돌부처 주민 ಠ_ಠ

더 황당한 건, 주민이 이동조차 안 함.
분명히 네비게이션.이동시켜() 코드를 넣었는데 왜?

이것도 원인은 '너무 빠른' 내 코드 때문이었음.

  1. Goal이 시작됨.
  2.  tick()에서 findNearbyPartner() 호출.
  3. 운 좋게 바로 옆에 있던 다른 주민을 짝으로 찾음.
  4. 주민이 한 발짝 떼기도 전에 "어? 거리가 가깝네? 교배해!" 조건이 바로 참이 됨.
  5. 주민은 움직일 기회도 없이 그 자리에서 교배 모드로 들어가 버림.

쉽게 말해: 소개팅 앱에서 매칭되자마자 상대방 프로필 사진 보고 "우리 집에서 1m 거리네? 바로 결혼하자!" 하는 거랑 똑같음. 과정 따위 생략.


3단계: 진짜 범인의 등장 (feat. 주민의 '뇌')

코드를 아무리 뜯어고쳐도 해결이 안 됐음. 이동은 계속 씹히고, 버그는 터지고.
그러다 진짜 범인을 찾았음. 범인은 바로...

주민의 '뇌(Brain)' 시스템이었음!

마크 AI에는 두 가지 중요한 시스템이 있음.

  • GoalSelector (행동 목록): "내가 할 수 있는 일들" 리스트. (예: 돌아다니기, 문 열기, 몬스터로부터 도망치기)
  • Brain (총사령관): "지금은 이 리스트 중에서 뭘 해야 할 시간이지?"를 결정하는 스케줄 관리자.

나는 내 FindPartnerAndBreedGoal GoalSelector에 추가했음. "자, 주민아! 이제 연애도 할 수 있어!" 하고 선택지를 준 거임.

하지만 주민의 '뇌'는 내 말을 쌩깠음.

뇌: "어? 지금은 일할 시간(Activity.WORK)인데? 연애 같은 소리 하네. 딴짓 말고 밭이나 갈아!"

뇌는 자기 스케줄에 따라 GoalSelector의 행동 목록을 지 맘대로 초기화하고, '일하기', '돌아다니기' 같은 기본 행동을 강제로 우선시킴. 내 커스텀 Goal은 실행될 틈도 없이 덮어씌워지거나 순위에서 밀려버린 거임.

이동 명령이 씹힌 이유도 이거였음. [AI DEBUG] 이동 시작! 로그가 찍힌 0.05초 뒤에, 뇌가 "이제 한가한 시간(Activity.IDLE)이니까 어슬렁거려" 라며 내 명령을 덮어써 버린 것.


4단계: 해법을 찾아서 (상태 관리와 뇌 해킹)

이 모든 삽질 끝에 두 가지 중요한 깨달음을 얻었음.

깨달음 1: 깔끔한 '상태 전이'가 핵심이다

애초에 한 개의 Goal에 모든 걸 욱여넣은 게 문제였음. AI는 바보라서, 한 번에 하나씩 시켜야 함. 이럴 때 쓰는 게 바로 '기억(Memory)'을 이용한 바통 터치임.

이게 원래 정석적인 방법임.

  1. 빵 발견! ➡️ 이벤트 핸들러가 주민의 뇌에 MEMORY_BREAD_LOCATION (빵 위치) 기억을 저장함.
  2. 빵 먹으러 가기 Goal 활성화: 이 Goal은 뇌에 빵 위치 기억이 있을 때만 작동함. 빵에 도착하면 이 기억을 지우고, 대신 MEMORY_READY_TO_FIND_PARTNER (짝 찾을 준비 완료) 기억을 true로 설정.
  3. 짝 찾기 Goal 활성화: 이 Goal은 '짝 찾을 준비 완료' 기억이 true일 때만 작동함. 짝을 찾으면 이 기억을 지우고, 찾은 짝을 MEMORY_BREED_TARGET (교배 대상) 기억에 저장.
  4. 만나서 교배하기 Goal 활성화: 이 Goal은 '교배 대상' 기억이 있을 때만 작동함. 교배가 끝나면 이 기억을 지우면서 모든 과정이 깔끔하게 끝남.

이렇게 각 Goal이 자기 할 일만 하고, 다음 Goal에게 '기억'이라는 바통을 넘겨주는 방식이 훨씬 안정적임.

깨달음 2: 뇌를 거역하지 말고, 뇌를 이용하라

하지만 이 '상태 전이' 방식도 주민의 '뇌'가 허락해야 쓸 수 있음.
그래서 진짜 해결책은 Goal이 아니라 **Behavior (행동)**을 만드는 거임.

  • 실패한 방식: GoalSelector(행동 목록)에 "이런 것도 해봐~" 하고 추가하기. (결과: 뇌가 무시함)
  • 성공할 방식: Brain(총사령관)에게 직접 "이건 새로운 '업무'의 일종입니다!" 라고 보고해서, 뇌의 스케줄 자체에 **Behavior(행동)**를 끼워 넣기.

이건 훨씬 복잡해서... 나중으로 미루기로 했음. (도망)


※ 번외: 사이드 퀘스트

Q. 주민은 왜 내가 만든 다이아몬드 좀비랑 다른가요?

  • 좀비: AI가 단순함. 뇌가 없음. 목표는 오직 "플레이어 발견 → 돌진 → 공격". GoalSelector만 건드려도 충분함.
  • 주민: AI가 복잡함. '뇌'와 '스케줄'이 있음. 아침엔 일하고, 점심엔 잡담하고, 밤엔 잠. 이 스케줄을 관리하는 뇌를 직접 다뤄야 해서 훨씬 어려움.

Q. 렌더러 등록은 왜 '클라이언트 이벤트'에서 하죠?

  • 서버 (주방장): 게임의 모든 규칙과 AI 행동을 계산함. "주민이 A에서 B로 이동했다" 같은 정보를 처리함.
  • 클라이언트 (홀 서빙 직원): 주방장이 계산한 결과를 받아서 플레이어 눈에 '보여주는' 역할만 함. 모델링, 텍스처, 파티클 효과 등.

커스텀 주민의 겉모습을 그리는 Renderer는 당연히 '보여주는' 역할이므로, 플레이어의 PC, 즉 클라이언트 측에서만 등록하면 됨. 이걸 "클라이언트 이벤트에 등록한다"고 표현하는 거임. 프로젝트 구조를 feature/custom_villager 같은 패키지로 나누는 건 아주 좋은 습관임!


결론

간단한 주민 연애 시뮬레이션인 줄 알았는데, 주민의 뇌 구조까지 파헤치는 대장정이 되어버렸음.
결론은? 마크 모딩은 쉽지 않고, 특히 주민은 더럽게 똑똑하고 까다롭다.

오늘도 처절한 삽질로 한 단계 성장했다!
다음엔 진짜로 주민 '뇌'를 해킹하는 법을 들고 오겠음. 그럼 20000