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

[마인크래프트 모딩]#19 '궁수 타워' 만들기

kimchangmin02 2025. 7. 4. 12:51

메운디 아시려나

혹은 plant vs zombiesms?

모딩하다가, 이 마크로 이런 디펜스겜 구현할순없을까라는 생각이 들었고

이떄까지 만든 몹들을 하나씩, 고정된, 타워로 만들려고한다

 

처음은 궁수타워이다

내가 원하던건 이런 모습이였으나 타협해버림

(좀비가 벽을 공격하게 해야하는데)

그럴려면, 좀비 엔티티의 goal도 바꿔야할것같아서

 

1부: 최종 구현 원리 (그래서 어떻게 만들었는가?)

결론부터 말하면, 지금의 '궁수 타워'는 **독립적인 하나의 몹(Entity)**임.
겉보기엔 흙 두 칸 위에 주민이 서 있는 것처럼 보이지만, 사실 블록도, 일반 주민도 아님. 모든 기능이 이 하나의 엔티티 안에 담겨 있음.

  • 핵심 설계: 모든 것은 하나의 엔티티
    • 처음엔 '설치용 블록 + 제어용 데이터 + 궁수 몹'이라는 복잡한 구조를 생각했으나, 수많은 문제 끝에 포기함.
    • 최종적으로는 '궁수 타워' 자체를 하나의 엔티티로 만들어, 스폰 알로 소환하는 방식을 채택함. 이것이 모든 문제를 해결한 핵심 열쇠였음.
  • 기본 뼈대: PathfinderMob 상속
    • Villager(주민)를 상속하지 않았음. PathfinderMob이라는, AI를 가진 몹의 가장 기본적인 클래스를 상속받음.
    • 이유: 주민 클래스는 너무 똑똑해서 우리가 원치 않는 행동(직업 찾기, 잠자기, 돌아다니기)을 하는 '뇌(Brain)' 시스템이 내장되어 있음. 우리는 완벽한 통제가 필요했기에, 백지상태인 PathfinderMob에서 시작함.
  • 겉모습: 렌더러 빌려오기
    • 뼈대는 기본 몹이지만, 겉모습은 주민처럼 보여야 함.
    • 이를 위해 엔티티의 렌더링(화면에 그려주는 작업)만 바닐라의 VillagerRenderer를 사용하도록 지정함.
    • 결과적으로 우리 엔티티는 주민의 모델과 텍스처를 '빌려와서' 화면에 표시됨. 로직과 겉모습을 분리한 것임.
  • AI 설계 (행동 방식)
    • 제자리 고정: 움직임, 넉백, 다른 몹에 의해 밀리는 모든 물리 효과를 코드 레벨에서 0으로 만들거나 무시하도록 하여 완벽한 '고정 포대'로 만듦.
    • 적 탐지: NearestAttackableTargetGoal이라는 AI 목표를 사용해, 주변의 모든 Monster(좀비, 스켈레톤 등)를 적으로 인식함.
    • 공격: RangedAttackGoal이라는 AI 목표를 사용해, 탐지한 적에게 활을 쏨.
  • 레벨업과 연사
    • 데이터 저장: 엔티티 내부에 SynchedEntityData라는 기본 시스템을 사용해 레벨과 킬 수를 저장함. 이 시스템은 서버에서 값이 바뀌면 자동으로 클라이언트에 알려주는 매우 편리한 기능임.
    • 킬 카운트: LivingDeathEvent라는 '몹 사망 사건'을 감지함. 몹이 죽었을 때, 공격자가 우리 '아처 가디언'이라면 가디언의 notifyKill() 메서드를 호출하여 킬 수를 1 올림.
    • 레벨업 조건: 킬 수가 현재 레벨과 같아지면 레벨업함. (Lv.1 -> 1킬, Lv.2 -> 2킬...)
    • 연사: 활을 쏘는 로직 안에 for 반복문을 넣어, '현재 레벨'만큼 화살을 발사함. 이때 두 번째 화살부터는 약간의 부정확도를 줘서 자연스럽게 퍼져나가도록 함.
  • 좀비가 타워를 공격하게 만들기
    • 가장 까다로운 문제였음. 바닐라 좀비는 PathfinderMob을 공격하지 않음.
    • EntityJoinWorldEvent라는 '몹 스폰 사건'을 감지함.
    • 세상에 좀비가 스폰될 때마다, 그 좀비의 AI 목록에 "저기 있는 ArcherGuardianEntity도 공격해!" 라는 새로운 공격 목표를 강제로 주입해 줌.
    • 덕분에 우리 타워는 좀비에게 매력적인 공격 대상이 됨.
  • HUD (체력바와 레벨 표시)
    • 엔티티 렌더러(ArcherGuardianRenderer)가 매 프레임마다 엔티티의 데이터를 읽어 옴.
    • 위에서 설명한 SynchedEntityData 덕분에, 서버에서 변경된 체력, 레벨, 킬 수가 클라이언트로 자동 동기화됨.
    • 렌더러는 이 최신 정보를 바탕으로 엔티티의 머리 위에 텍스트와 체력바를 그려줌.

2부: 좌충우돌 개발 일지 (오류 해결의 역사)

이 간단한 타워 하나를 만드는 데 상상 이상의 오류들이 있었음. 그야말로 총체적 난국이었음.

  • 초기 설계의 함정: "블록으로 만들자!"
    • 처음엔 "흙 블록 2칸을 쌓고 그 위에 주민을 두면 타워지!" 라고 단순하게 생각함.
    • 하지만 이 구조는 최악이었음. 흙 블록의 체력과 주민의 체력을 어떻게 합칠지, 좀비는 왜 흙을 안 때리고 주민만 때리는지 등 문제가 산더미였음.
    • 결정적으로 사용자(나 자신)가 "아 그냥, 흙 2칸 없애면 되는거 아닌가?" 라는 깨달음을 얻고, 복잡한 블록 구조를 버리고 '하나의 엔티티'로 만드는 현재의 설계로 전환하게 됨.
  • 오류 1: 주민 상속의 배신
    • "좀비가 타워를 공격하게 하려면, 그냥 주민을 상속받으면 되잖아?" 라는 생각으로 Villager를 상속함.
    • 결과: 좀비가 공격은 하지만, 가디언이 주민의 '뇌(Brain)' AI 때문에 제자리에 있지 않고 마음대로 돌아다니기 시작함. 흙 블록에서 내려와 좀비와 육탄전을 벌이는 대참사가 발생.
    • 해결: PathfinderMob을 상속하여 AI를 완벽하게 통제하는 것으로 방향을 바꿈.
  • 오류 2: 보라색과 검은색의 저주
    • 블록 아이템이 보라색/검은색 체커보드 모양으로 깨져서 나옴.
    • 원인: 게임이 아이템의 텍스처를 어디서 찾아야 할지 모를 때 나오는 대표적인 오류. 리소스 파일(.json)의 경로, 이름, 내용 중 하나가 잘못된 것.
    • 해결: models/item/ 폴더에 스폰 알의 모델을 정의하는 .json 파일을 추가하고, lang/en_us.json에 아이템 이름을 등록하여 해결함.
  • 오류 3: "게임 창이 안 떠요!" (runClient vs runServer)
    • 서버 실행용인 runServer를 켜놓고, 게임 화면이 뜨기를 하염없이 기다림.
    • 원인: runServer는 원래 게임 창 없이 콘솔 로그만 띄우는 게 정상. 게임 화면을 보려면 runClient를 실행해야 함.
    • 해결: IDE의 실행 구성을 runClient로 바꾸고 나서야 익숙한 마인크래프트 화면을 볼 수 있었음.
  • 오류 4: 불친절한 EULA
    • runServer 실행 시, "EULA에 동의해야 한다"는 메시지만 남기고 서버가 꺼짐.
    • 원인: 마인크래프트 서버는 최초 실행 시 run 폴더에 eula.txt를 생성하고, 사용자가 직접 이 파일의 eula=false true로 바꿔주길 기다림.
    • 해결: 프로젝트 폴더의 run/eula.txt 파일을 찾아 값을 true로 수정하여 해결.
  • 오류 5: 보이지 않는 체력바, 오르지 않는 킬 수
    • 가디언이 좀비를 죽여도 HUD의 킬 수가 전혀 오르지 않았음.
    • 가설: 클라이언트-서버 데이터 동기화 문제라고 생각하고, 직접 네트워크 패킷(Clientbound...Packet)을 만들어야 하나 고민함.
    • 진짜 원인: 서버에서 킬 카운트 자체가 안 되고 있었음. LivingDeathEvent에서 공격자를 찾는 로직에 결함이 있어, 가디언이 킬을 해도 시스템이 인지를 못 했던 것. 서버에서 데이터가 바뀌지 않으니 클라이언트로 보낼 업데이트도 없었고, 당연히 HUD도 갱신되지 않았음.
    • 해결: DamageSource에서 공격자를 가져오는 방식을 source.getEntity()로 변경하여 서버 로직을 수정하자, SynchedEntityData 시스템이 알아서 동기화를 처리해주며 HUD 문제까지 한 번에 해결됨.
  • 최종보스: 등록되지 않은 이벤트 핸들러
    • 킬 카운트 로직을 완벽하게 수정하고 로그까지 심었는데도, 킬 수가 오르지 않고 로그도 전혀 찍히지 않음.
    • 원인: TowerInteractionHandler라는 완벽한 설계도를 만들어놓고, 정작 Forge에게 "이 설계도 좀 봐주세요!" 라고 등록하는 것을 잊음.
    • 해결: 모드 메인 클래스(MyMod.java)에 MinecraftForge.EVENT_BUS.register(new TowerInteractionHandler()); 단 한 줄을 추가. 이 코드는 Forge의 이벤트 시스템에 우리 핸들러를 등록하여, 게임 내 사건들을 감지할 수 있게 만드는 핵심적인 절차였음. 이 한 줄을 추가하자마자 모든 기능이 마법처럼 정상 작동하기 시작함.