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

[마인크래프트 모딩] #7 침대 없이 강제 번식시키는 마법의 빵 만들기 (Feat. 무한 버그)

kimchangmin02 2025. 6. 26. 13:51

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

 

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, 렌더링 리소스까지 다 훑어보게 됨.
이게... 모딩...? 이게... 성장의 맛...? 도파민 터짐.
아무튼 오늘은 여기까지. 이제 진짜 됨. 완벽함.

 

빵을 던지면, 주민이 아기 낳음(아직 빵을 줍는 동작은 구현못함)