여름방학 동안 마인크래프트 모드 제작 프로젝트를 시작함.
그 과정에서 자바(Java)를 활용하며 배운 내용과, 특히 중요했던 개념들을 기록하고자 함.
2학년1학기때 자바 프로그래밍 배웟는데
공고롭게도 마크도 자바로 구성되있는 우연이 겹쳐서 시작하게됨
0. 마인크래프트 모드는 어떻게 작동하는가?
모드 개발의 가장 근본적인 원리를 요약하자면 다음과 같음.
- 모드 개발은 '서버'의 규칙을 바꾸는 행위임.
- 게임의 모든 실제 사건은 '서버'에서 일어남.
- 서버에서 바뀐 규칙과 그 결과는 '클라이언트(내 컴퓨터)'로 전달되어야만 우리 눈에 보임.
이때 많은 사람이 오해하는 부분이 있음.
Q: 모드를 만들면 서버 자체가 바뀌나요? 원본 파일을 수정하는 건가요?
A: 좋은 질문임! 결론부터 말하면, 원본 파일은 전혀 건드리지 않음.
마인크래프트 서버를 '정해진 규칙대로만 움직이는 자동차 공장'에 비유할 수 있음. 모드 개발은 이 공장의 설계도를 바꾸는 것이 아님. 대신, 모드 로더(Forge)가 허락한 합법적인 '틈', 즉 **'서비스 포트(API)'**에 우리가 만든 '커스텀 장비(모드 코드)'를 연결하는 방식임.예를 들어 '주민이 피해를 입는 순간(Event)'이라는 포트에 코드를 연결해두면, 서버는 원래 하려던 '도망가기' 규칙을 실행하기 전에 우리 코드에게 먼저 물어봄. 이때 우리 코드가 "아니, 도망가지 말고 싸워!"라고 새로운 명령을 내리며 규칙에 개입하는 것임.
1. 버그와의 사투: AI와 렌더링 문제
오늘은 모드를 만들면서 겪었던, 정말 머리를 쥐어뜯게 만들었던 버그와 그 해결 과정을 공유하고자 함. 분명 코드 로직은 완벽한데, 내가 만든 똑똑한 AI 주민이 좀비만 보면 비명을 지르며 도망가기만 하고, 야심 차게 만든 커스텀 몬스터의 체력바는 감쪽같이 사라지는 현상이었음.
이 모든 문제의 원인은 바로 '서버'와 '클라이언트'의 역할 구분을 제대로 이해하지 못했기 때문이었음.
# 사건의 발단: 서버와 클라이언트의 분리
마인크래프트는 싱글플레이조차 내부적으로 두 개의 엔진으로 돌아감. 바로 **논리적인 서버(Logical Server)**와 **논리적인 클라이언트(Logical Client)**임.
- 🧠 서버 (두뇌, 규칙 담당): 게임의 모든 실제 로직을 처리함. 몬스터의 AI, 데미지 계산, 월드 데이터 관리 등 '게임 규칙' 그 자체를 담당함.
- 👀 클라이언트 (눈과 귀, 표현 담당): 서버가 "이 위치에 좀비가 있어!"라고 알려주면, 그 정보를 받아 화면에 좀비를 그리고, "으어어" 하는 소리를 재생하는 '보여주는' 역할을 담당함.
내가 겪었던 두 가지 대표적인 문제를 통해, 이 둘을 구분하는 게 왜 중요한지 보여주고자 함.

Case 1: 겁쟁이가 된 전투 AI 주민

- 문제 상황:
좀비를 만나면 용감하게 달려들어 싸우는 '저거너트' AI를 주민에게 추가했음. 하지만 테스트만 하면, 이 주민은 내가 만든 AI를 무시하고 마인크래프트 기본 AI처럼 비명을 지르며 도망가기 바빴음. - 원인 분석: 서버와 클라이언트의 로직 충돌
문제의 코드는 onLivingDamage (생명체가 피해를 입었을 때) 이벤트를 감지하는 부분이었음. 이 이벤트 핸들러는 서버와 클라이언트 양쪽에서 모두 실행되고 있었던 것이 문제였음.
이때, 우선순위가 매우 높은 마인크래프트 기본(바닐라) AI인 **"위험하면 도망가라!"**가 서버에서 먼저 작동하려고 함. 내 코드가 서버/클라이언트 구분 없이 실행되면서 이 우선순위 싸움에서 미묘한 충돌을 일으켰고, 결국 바닐라 AI에 밀려버린 것임. - 해결책: "이 로직은 서버에서만!"
해결은 간단했음. AI 로직처럼 게임 규칙에 해당하는 코드는 오직 서버에서만 실행되도록 조건을 거는 것이었음.
Generated java
// 예시 코드
@SubscribeEvent
public void onLivingDamage(LivingDamageEvent event) {
// event.getEntity().level()은 월드(World) 객체임.
// isClientSide 변수로 지금 코드가 클라이언트에서 실행 중인지 확인함.
// '!'를 붙여 '클라이언트가 아닐 때', 즉 '서버일 때만' 이라는 조건을 검.
if (!event.getEntity().level().isClientSide()) {
// AI를 추가하고, 목표를 설정하는 모든 로직은 이 안에 작성함.
// 이제 이 코드는 오직 서버에서만 작동하여 다른 AI와 충돌하지 않음.
}
}
Case 2: 보이지 않는 다이아몬드 좀비의 체력바

- 문제 상황:
특별한 '다이아몬드 좀비'를 만들고, 이 몬스터에게만 커스텀 체력바가 보이도록 렌더링 코드를 추가했음. 하지만 어찌 된 일인지 체력바는 전혀 보이지 않았음. 로그를 확인하니, 좀비가 생성될 때 "나는 다이아몬드 좀비야!" 라는 꼬리표(NBT 태그)는 분명히 잘 붙고 있었는데도 말임. - 원인 분석: 서버와 클라이언트의 정보 불일치
이 현상 역시 서버와 클라이언트의 역할 차이 때문에 발생했음.- 이름표는 서버가: 좀비에게 "is_diamond_zombie" 라는 NBT 태그를 붙여주는 코드는 서버에서 실행됨.
- 그림은 클라이언트가: 하지만 이 좀비의 체력바를 그리는 렌더러 코드는 클라이언트에서 실행됨.
- 정보 단절: 문제는, 서버에서 붙인 NBT 태그 정보가 클라이언트로 자동으로 전달되지 않는다는 점이었음. 클라이언트는 눈앞의 좀비가 평범한 좀비인지, 다이아몬드 좀비인지 전혀 모르는 상태였던 것임.
이전에 만들었던 다른 모드의 정보 표시는 잘 됐는데, 왜 이번엔 안 됐는지 의문이 들었음. 그 이유는 이전 모드는 Capability 시스템을 사용했고, 이 데이터는 **네트워크 패킷(Packet)**을 통해 서버에서 클라이언트로 정보를 '동기화'하도록 미리 설계해두었기 때문이었음. NBT는 이런 자동 동기화 기능이 없음.
- 해결책: "이 좀비는 특별해!" 라고 쪽지(패킷) 보내기

해결책은 서버가 클라이언트에게 정보를 직접 알려주는 것임- 서버: 다이아몬드 좀비를 생성하고 NBT 태그를 붙임.
- 서버 -> 클라이언트: 직후, 주변 모든 클라이언트에게 "방금 생성된 ID 819번 좀비는 다이아몬드 좀비야!" 라는 내용의 작은 데이터 조각, 즉 네트워크 패킷을 보냄.
- 클라이언트: 패킷을 받은 클라이언트는 "아하! 819번은 특별하구나!" 라고 인지하고, 자신의 정보에도 해당 좀비가 다이아몬드 좀비임을 기록함. 이제 렌더러가 정상적으로 정보를 확인하고 멋진 체력바를 화면에 그려주게 됨.
결론: 모드 개발의 첫걸음, "지금 어디인가?"
두 가지 버그를 해결하며 얻은 가장 큰 교훈은, 코드를 작성하기 전에 항상 "이 코드는 어디에서 실행되어야 하는가?" 를 먼저 자문하는 습관임.
- 게임의 규칙, 데이터, AI를 다룬다면? 👉 서버
- 화면에 무언가를 그리거나, 소리를 내거나, 입력을 받는다면? 👉 클라이언트
- 서버의 정보가 클라이언트의 화면에 반영되어야 한다면? 👉 패킷 통신
이 간단한 원칙 하나만 기억해도 앞으로 겪게 될 수많은 논리 오류를 예방하고 디버깅 시간을 획기적으로 줄일 수 있을 것임. 삽질하며 배우는 것도 의미 있지만, 이 글을 읽는 분들은 조금 더 편한 길을 가셨으면 좋겠음
'모딩 > 마인크래프트 모드 개발 일지' 카테고리의 다른 글
| [마인크래프트 모딩] #6 "분명 데미지는 들어가는데..." 칼 안 휘두르는 주민, 범인은 의외의 곳에 있었다 (14) | 2025.06.25 |
|---|---|
| [마인크래프트 모딩] #5 주민 AI가 단체로 파업한 이유 (9) | 2025.06.24 |
| [마인크래프트 모드 개발 일지]25.6.22/25.6.23 (0) | 2025.06.23 |
| [마인크래프트 모드 개발 일지]25.06.23 (14) | 2025.06.23 |
| [마인크래프트 모드 개발 일지] 개발 환경 설정하기_모든 삽질의 진짜 범인을 찾아서 (11) | 2025.06.23 |