마인크래프트 모딩 삽질기: "주민 자동 교배빵" 만들려다 코딩의 신세계를 맛봄

1. 야심찬 시작: "빵 하나로 마을을 인구 100만 도시로!"
마인크래프트 모딩에 손을 댐.
첫 목표는 거창했음.
"특수한 빵을 땅에 던지면, 주민 하나가 그걸 보고 눈이 뒤집혀서(?) 짝을 찾아 헤매다 침대 없이도 강제 교배하게 만들자!"
단순히 아이템 추가하는 건 시시하잖음? 엔티티의 AI를 건드려보고 싶었음.

2. 첫 번째 벽: "그래서... 어떻게...?"
일단 바닐라 코드를 뜯어보기로 함.
"음식을 먹으면 배가 부르다"는 로직은 대체 어디에 있을까?
eat, food 같은 단어로 무작정 검색 시작.
- 1차 시도: Items.java에서 BREAD를 찾음.
public static final Item BREAD = register("bread", new Item(new Item.Properties().food(Foods.BREAD)));
오, Foods.BREAD에 뭔가 있나 봄. - 2차 시도: Foods.BREAD를 따라가니 Foods.java 파일로 이동.
public static final FoodProperties BREAD = (new FoodProperties.Builder()).nutrition(5).saturationMod(0.6F).build();
아... 여긴 그냥 '설정값' 창고였음. 영양가 5, 포만감 0.6. 진짜 '먹는 행위'는 여기가 아님. - 3차 시도: "먹는 주체는 플레이어(LivingEntity)겠지!" 라는 생각으로 LivingEntity.java를 열고 eat을 검색.
메소드를 따라가고, 또 따라가니 마침내 FoodData.java에서 플레이어의 허기 수치를 직접 올리는 코드를 발견함.
결론: 기능의 '정의'와 '실행'은 완전히 다른 곳에 있음을 깨달음.
잠깐, 근데 Builder 패턴은 왜 쓰는 거임?
new FoodProperties(5, 0.6F, false, true, ...) 이렇게 생성자를 직접 호출하면, 파라미터가 많아질수록 뭐가 뭔지 헷갈림.
(new FoodProperties.Builder()).nutrition(5).saturationMod(0.6F).build()
이렇게 빌더를 쓰면, .nutrition(5) 처럼 메소드 이름만 봐도 "아, 영양가를 5로 설정하는구나" 하고 명확하게 알 수 있음. 가독성이 하늘과 땅 차이.
.build()는 이 모든 설정을 종합해서 최종적으로 FoodProperties 객체를 "건축(Build)"하라는 명령임. 우리가 BREAD 변수를 선언하는 그 줄에서 바로 호출해서 객체를 완성시키는 것.

3. 두 번째 벽: "읽기 전용인 주민 AI를 어떻게 조종함?"
내 계획은 바닐라 주민의 행동을 바꾸는 것.
근데 Villager.java 파일은 당연히 읽기 전용. if (우리 빵을 먹었으면) { ... } 같은 코드를 추가할 수가 없음. 여기서 1차 멘붕.
두 가지 방법이 떠오름.
- A안: 주민을 상속받는 MyVillager를 만들어서 AI를 뜯어고친다.
- 장점: 깔끔함.
- 단점: 이미 세상에 있는 모든 바닐라 주민은 바보인 채로 남음. 내 모드의 주민만 똑똑해짐. 몰입감 와장창.
- B안: 어떻게든 외부에서 바닐라 주민에게 개입한다.
- 장점: 모든 주민에게 적용 가능!
- 단점: 어떻게...?
바로 여기서 '모드 로더(Forge/Fabric)'의 위대함을 알게 됨.
모드 로더는 마인크래프트 원본 코드 곳곳에 **'이벤트'라는 이름의 '튜닝 포트'**를 심어뒀음.
우리는 EventHandler라는 '센서'를 만들어서, 특정 사건이 터지길 기다리면 됨.
- "땅에 아이템이 떨어지는 사건" (EntityJoinWorldEvent) 발생!
- 내 센서가 "이거 교배 빵인데?" 하고 감지.
- 센서는 즉시 주변 주민의 public으로 열린 goalSelector.addGoal(...)이라는 튜닝 포트에 접속.
- "지금부터 다른 거 다 멈추고 내 명령(Goal)을 들어!" 라고 AI를 강제 주입함.
결론: 바닐라 코드를 뜯어고치는 게 아니라, 공개된 포트를 통해 '명령'을 내리는 거였음. 이게 모딩의 마법이었음.

4. 세 번째 벽: "왜 내 맘대로 안 움직여!" (끝없는 디버깅)
코드를 다 짰다고 생각하고 실행. 하지만 현실은 냉혹했음.
- 문제 1: 빵을 버리면 무한으로 복제됨.
- 원인: 빵을 감지 -> 주민 유혹 -> 빵 제거 -> 제거 과정에서 빵이 다시 스폰 -> 다시 감지... 무한 루프!
- 해결: 빵에 NBT 태그로 "처리 완료" 딱지를 붙여서, 한 번 처리한 빵은 두 번 다시 건드리지 않도록 수정.
- 문제 2: 주민이 빵을 보고도 미동도 안 함.
- 원인: 내 AI는 "교배 가능한(canBreed) 짝"을 찾고 있었음. 근데 바닐라 주민은 인벤토리에 빵이 없으면 canBreed()가 false임. 즉, 세상에 교배 가능한 주민이 단 한 명도 없었던 것.
- 해결: 짝을 찾는 조건에서 canBreed()를 삭제. "나 아니고 아기 아니면 일단 와봐!" 로 변경.
- 문제 3: 드디어 짝을 찾았는데, 하트도 안 띄우고 아기도 안 낳음.
- 원인: setInLove(), setLoveTicks() 같은 교배 메소드가 전부 private이거나, 더 이상 사용되지 않는 옛날 방식이었음. 문이 다 잠겨있었음.
- 최종 해결: 발상을 전환함. "교배해!" 라고 소리치는 대신, 교배가 일어나는 '조건'을 만들어주기로 함. 1.18.2의 주민 AI는 Goal이 아니라 Brain과 Memory(기억) 기반으로 움직임.
- 두 주민의 Brain에 접근해서, setMemory()라는 공개된 메소드로 서로를 BREED_TARGET(교배 대상)으로 기억하도록 조작함.
- "얘가 네 짝이야" 라고 뇌에 직접 속삭여주니, 바닐라 AI가 "아! 내 짝이구나!" 하고 알아서 하트 띄우고 아기 낳는 모든 과정을 진행함.
- 문제 4: 아기가 아니라 어른이 태어남. 하트도 순식간에 사라짐.
- 원인: 교배 과정이 너무 빨랐고, 아기 나이를 설정해주지 않았음.
- 해결: '강제 아기 스폰' 로직으로 변경. 하트 파티클을 강제로 띄우고, TickTask로 5초 뒤에 아기를 스폰시키는 '예약'을 걸음. 스폰될 아기에게 setAge(-24000) 코드로 "넌 아기다" 라고 명확히 지정해줌.

5. 오늘의 교훈
모딩은 버그와의 싸움임. 그리고 그 과정은 탐정 놀이와 같음.
- 단서: 로그(Log)는 거짓말을 안 함.
- 용의자: 내 코드, 그리고 내가 잘못 이해한 바닐라 코드.
- 해결책: 직접 코드를 뜯어고칠 수 없을 땐, 그 코드가 작동하는 '조건'을 만들어주거나, 공개된 '튜닝 포트(public 메소드, 이벤트)'를 찾아내면 됨.
단순한 빵 하나 추가하려다 마인크래프트의 이벤트 시스템, AI, Brain, NBT, 렌더링 리소스까지 다 훑어보게 됨.
이게... 모딩...? 이게... 성장의 맛...? 도파민 터짐.
아무튼 오늘은 여기까지. 이제 진짜 됨. 완벽함.
'모딩 > 마인크래프트 모드 개발 일지' 카테고리의 다른 글
| [마인크래프트 모딩]#9 주민 ai수정의 어려움에 대해서 (9) | 2025.06.28 |
|---|---|
| [마크 모딩 삽질기] #8 주민아, 연애 좀 해! (feat. 뇌 제어 실패기) (11) | 2025.06.27 |
| [마인크래프트 모딩] #6 "분명 데미지는 들어가는데..." 칼 안 휘두르는 주민, 범인은 의외의 곳에 있었다 (14) | 2025.06.25 |
| [마인크래프트 모딩] #5 주민 AI가 단체로 파업한 이유 (9) | 2025.06.24 |
| [마인크래프트 모드 개발 일지]25.6.22/25.6.23 (0) | 2025.06.23 |