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

마크 모딩) 26.1.31

kimchangmin02 2026. 1. 31. 08:00

 

타겟팅 ai vs  반격ai

vs 좀비인지 확인하는 ai(즉 법사가 공격대상 찾을때

타겟팅 AI  registerGoals  적을 검색할 때 아군은 필터링해서 제외함 시력 필터 (아군은 안 보임)
반격 AI   ZombieAlly...Goal  아군이 날 때려도 보복하지 않음  인내심 (친구면 참음)
데미지 핸들러 AllyDamageHandler  혹시라도 맞았을 때 피가 깎이지 않게 함 방탄복 (맞아도 안 아픔)

생성자 (public ZombieAllyAware...) vs canUse()의 차이

  • 생성자 (super(mob)): 이건 단순히 "이 AI는 이 좀비가 사용할 거야"라고 등록만 하는 단계입니다. (준비 단계)
  • canUse(): 마인크래프트 엔진은 매 순간(매 틱마다) 좀비에게 물어봅니다. "너 지금 이 행동(반격) 시작해도 돼?"

 

 

canUse()는 우리가 호출하지 않아도 알아서 호출되나요?

네, 맞습니다! 이게 마인크래프트(및 대부분의 게임 엔진) AI 시스템의 핵심입니다.

  • 동작 원리: 마인크래프트 엔진은 1초에 20번(20틱) 게임을 업데이트합니다. 이때 엔진은 좀비의 머릿속(goalSelector, targetSelector)에 들어있는 모든 AI Goal들을 훑으면서 **"지금 이 행동 할 수 있어?"**라고 물어봅니다.
  • 우리의 역할: 우리는 canUse() 안에 "이럴 때만 행동해!"라고 조건만 적어두면 됩니다. 그러면 게임 엔진이 매 순간 그 조건을 체크해서 실행 여부를 결정합니다.
  • 비유: 우리가 "배고프면 밥 먹어"라는 규칙을 뇌에 저장해두면, 뇌가 알아서 수시로 위장 상태를 체크하다가 배가 고픈 순간 밥을 먹는 것과 같습니다.

 

 

 

 

this.targetSelector.addGoal(1, ...)은 무슨 역할인가요?

이 코드는 좀비의 'AI 리스트'에 방금 만든 규칙을 등록하는 것입니다.

  • new ZombieAllyAware...: 말씀하신 대로 객체를 생성하는 것입니다. "반격 규칙"이라는 문서 한 장을 새로 만든 셈입니다.
  • addGoal(1, ...): 좀비의 **반격 전담 부서(targetSelector)**에 이 문서(규칙)를 제출하는 것입니다.
    • 숫자 1은 우선순위입니다. 숫자가 작을수록 "가장 먼저 고려하라"는 뜻입니다.
  • 결론: 이 코드를 작성해야만, 게임 엔진이 위에서 말한 "매 순간 체크하는 리스트"에 이 AI를 포함시킵니다. 등록 안 하면 엔진은 이런 AI가 있는지조차 모릅니다.

 

 

 

 

ZombieAllyAwareHurtByTargetGoal extends HurtByTargetGoal

상속을 받았으니깐 

 

 

 

 

 

근데 새로운 행동 추가하려고 할때, 그걸 canuse안에는 무슨 행동할지를 적어둬야하잖아

근데 이건 상속 받은거라서, 그런 로직을 적을 필요가 없엇던건가 

가령 낚시하는 행동 추가는 코드가 훨씬 길것같은데 

이 반격 개상은, 대상인지 아닌지만 판단하고, 나머지는 super로 부토 로직<무조건 반격하도록 하는건가 

 

 

 

 

public HurtByTargetGoal(PathfinderMob p_26039_, Class<?>... p_26040_)

여기서 핵심은 ... (점 세 개)입니다.

Class<?>... (가변 인자)란 무엇인가?

자바에서 타입... 변수명은 **"이 자리에 인자를 0개 넣어도 되고, 1개 넣어도 되고, 여러 개 넣어도 된다"**는 뜻입니다.

  • super(mob) 이라고만 써도 되는 이유: 뒤에 오는 p_26040_ 자리에 **아무것도 안 보낸 것(0개)**으로 처리되기 때문입니다.
  • 만약 super(mob, IronGolem.class) 라고 썼다면: "아이언 골렘이 날 때려도 무시해라"라는 뜻이 됩니다.

 

 

 왜 부모 생성자는 이 인자를 받나요?

HurtByTargetGoal은 기본적으로 "누가 날 때리면 반격한다"는 AI입니다. 그런데 **"이런 놈들한테 맞았을 때는 예외적으로 참아라"**라고 명단을 주고 싶을 때 저 자리에 클래스 이름을 적습니다.

  • 예: super(mob, Skeleton.class, Spider.class) -> 스켈레톤이나 거미가 실수로 날 맞춰도 반격하지 마라.

 

 

왜 로직이 짧은가? (상속의 힘)

만약 낚시 AI를 처음부터 만든다면, canUse뿐만 아니라 다음과 같은 것들을 직접 다 코딩해야 합니다:

  • start(): 낚싯대를 꺼내는 동작
  • tick(): 찌가 가라앉는지 감시하고 기다리는 로직
  • stop(): 낚시를 그만두고 낚싯대를 집어넣는 동작

하지만 지금 만드신 것은 **"기존에 이미 잘 만들어진 반격 AI(HurtByTargetGoal)"**를 가져와서 **"대상 확인 필터"**만 살짝 바꾼 것입니다.

  • 부모(HurtByTargetGoal)의 역할: "대상을 추적하고, 복수 리스트에 올리고, 화를 내는 실제 행동"을 이미 다 가지고 있습니다.
  • 자식(작성하신 코드)의 역할: "그 복수 리스트에 올리기 전에, 우리 팀인지 아닌지 검사하는 문지기" 역할만 합니다.

 

 

 

 

 

 

 

1. AllyDamageHandler (물리적 방어 - "몸")

이 클래스는 데미지 이벤트를 가로챕니다.

  • 역할: "실제로 피가 깎이는가?"를 결정합니다.
  • 작동 시점: 칼이 몸에 닿는 그 순간!
  • 코드의 의미: event.setAmount(0.0F); event.setCanceled(true);
    • 이 코드가 실행되면, 때린 사람은 휘둘렀지만 맞은 좀비는 피가 전혀 깎이지 않습니다. (데미지 0)
  • 이것만 있다면?
    • 아군이 좀비를 때려도 피는 안 깎입니다. (좋음)
    • 하지만! 좀비는 "누가 날 때리긴 했어!"라고 인식합니다. 피는 안 깎였지만 기분은 나쁜 상태죠. 그래서 좀비 AI는 아군을 하루 종일 졸졸 쫓아다니며 째려보게 됩니다. (내분 발생)

2. ZombieAlly...Goal (행동 지능 - "마음")

이 클래스는 반격 여부를 결정합니다.

  • 역할: "나를 때린 놈을 적으로 등록하고 팰 것인가?"를 결정합니다.
  • 작동 시점: 누군가에게 맞아서 화를 내기 직전!
  • 코드의 의미: return false; (아군이면 반격 AI를 켜지 마라)
  • 이것만 있다면?
    • 아군이 좀비를 때려도 좀비가 화를 내거나 쫓아오지 않습니다. (좋음)
    • 하지만! 좀비의 피는 그대로 깎여버립니다. 아군이 계속 때리면 좀비는 반격도 못 하고 묵묵히 죽어버리는 비극이 발생합니다.

 

 

 

 

 

 

 

 

 

다른 종족 입장 (마법사 등 - 공격 대상이 한정됨)

마법사(Wizard)는 좀비와 대립 관계라서, 아무나 공격하는 게 아니라 **"좀비"**라는 특정 종족만 사냥하고 싶어 합니다.

  • 공격 대상: "오직 좀비만!" (스켈레톤, 크리퍼, 소, 돼지는 무시)
  • 필터링: "일단 **좀비인지 확인(isEnemyZombie)**하고, 그중에서도 혹시 우리 편 좀비가 있다면 패지 말자(shouldAttack)."
  • 사용 코드: 그래서 isEnemyZombie가 조준경 역할을 하는 것입니다.

 

 

 

public static void onLivingHurt(LivingHurtEvent event) { // <-- 여기서 '종이(event)'를 받음
    ...
    float damage = event.getAmount(); // <-- 종이에 적힌 '7.5'라는 숫자를 읽음
    tempestZombieOwner.addExperience(damage); // <-- 그 숫자만큼 경험치를 줌
}

 

  • 게임 엔진: "공격력, 아이템, 버프 다 따져보니 데미지는 10이네. 자, 여기 10이라고 적힌 보고서(event) 보낸다!"
  • 사용자님 코드: "오, 보고서에 10이라고 적혀 있구나. 그럼 경험치도 10만큼 줘야지."

 

 

 

 

아 데미지 준만큼 경험치 올리라고 해서, 저렇게 설계한거구나 

 

 

 

 

단계 1: 삼지창 좀비의 레벨 기준을 '보스 데이터'로 변경

TridentTempestZombieEntity2.java에서 자체 레벨링 코드를 지우고, 보스 레벨을 가져오도록 수정합니다.

 

 

 

 

경험치 부분:어떻게 올려주고있는지: 보스에서만 경험치 올려주도록:

일반몹, ㅁ보스몹 구분햇음

 

 

 

근데 특성 뽑으면 화면에  어떤 특성 뽑앗는지도 보여주는데, 그러면 사전 형식으로 저장해야하는거 아닌가 

 

 

 

일반몹은 굳이 레벨업하는 기능 넣을필요없으려나 ㅇㅇ

 

 

 

 

좀 더 똑똑하게 대상을 찾는 로직[   ]

 

 

 

아 북극곰은 공격하네

단지 아군 좀비만 공격안하는거엿네 

 

 

 

 

일단, 잔혹함 표시는 제대로 되는데, 특성의 갯수가 많아지면, 현재는, 계속 가로로 나열되는건가 [   ]

 

 

 

 

 

 

잔혹함이면, 아군이 반격안하고있음

데미지는 제대로 받고있는데 

 

 

  • 기존 문제: shouldAttack이 "공격자"의 상태만 체크했기 때문에, 맞는 사람 입장에서의 정당방위 로직이 없었습니다.

 

 

잔혹한 보스 **[좀비 A]**와 일반 아군 **[좀비 B]**가 있다고 가정합시다.

상황 1: 보스(A)가 아군(B)을 때릴 때 (공격 상황)

이때 호출은 shouldAttack(A, B)가 됩니다.

  • 1번 체크 (attacker인 A가 잔혹한가?): YES!
    • 보스 A는 잔혹하니까 "응, 넌 미쳤으니까 아군이라도 패도 돼"라고 true를 반환합니다.
    • 결과: 보스가 아군을 때립니다.

상황 2: 아군(B)이 보스(A)에게 복수할 때 (반격 상황)

이때 호출은 shouldAttack(B, A)가 됩니다. (B가 공격하려 하니까요!)

  • 1번 체크 (attacker인 B가 잔혹한가?): NO. (B는 착한 일반 좀비입니다.)
  • 2번 체크 (target인 A가 잔혹한가?): YES!
    • 비록 B는 제정신이지만, 때리려는 대상(A)이 나를 괴롭히는 "잔혹한 미친놈"이기 때문에 정당방위로 패는 것을 허용합니다.
    • 결과: 아군이 보스에게 반격합니다.

 

 

 

 

shouldAttack(공격하려는_놈, 얻어맞을_놈)

  • A가 선제공격할 때: attacker = A / target = B
  • B가 반격할 때: attacker = B / target = A (B가 공격자로 변신!)

3. 요약 답변

  1. A 입장에서도 호출되는가? -> 네. (B를 적으로 조준할 때, 그리고 실제로 데미지를 줄 때)
  2. B 입장에서도 호출되는가? -> 네. (맞은 후 A에게 다시 복수 주먹을 날릴지 결정할 때)

 

 

 

 

특성 4개인 몹 생성할수있는 명령어있나

우리가 만들어줘야하나 

[     ]

 

 

현재 방식은 모든 스킬을 하나의 긴 문자열로 합쳐서(String.join) 한 줄에 가로로 쭉 출력하는 방식입니다. 그래서 스킬이 많아지면 화면 옆으로 뚫고 나가게 됩니다.

질문하신 대로 **"위로 줄바꿈(새로운 줄이 위로 쌓임)"**을 구현하려면, Y 좌표를 마이너스(-) 방향으로 계산하면서 한 줄씩 그려주면 됩니다. 마인크래프트 렌더링 시스템에서 Y값이 작아질수록 하늘 쪽으로 올라가기 때문입니다.

 

 

지금, 뭐있더라, 등급등

 

 

예시스킬 클래스 < 걍 로직을 넣지않고, 그냥 등록 클래스만 작성해주면 되네 

 

 

 

BrutalBossBuilder.applyBossStats 메서드 안에서 강제로 모든 스킬을 다 넣어주는 코드를 임시로 작성해서 확인해 볼 수도 있습니다! (예: data.addOrUpgradeAbility("fire_aura"); 등을 여러 줄 써보기)

 

 

 

 

 

스킬이 추가되면 제한 없이 계속해서 위로 쌓여 올라갑니다.

그 이유는 렌더러 코드에 있는 이 반복문(Loop) 때문입니다:

Java
 
// BossLabelRenderer.java 내부 로직
for (int i = 0; i < skillLines.size(); i++) {
    // i가 0이면 Y = -12
    // i가 1이면 Y = -22
    // i가 2이면 Y = -32 ...
    int currentY = startSkillY - (i * 10); 
    drawCenteredText(..., currentY, ...);
}

 

 

 

 

 

클래스 파일 자체는 프로젝트에 그대로 두셔도 아무런 문제가 없습니다.

BossAbilityRegistry에서 register(...) 코드만 주석 처리하면 게임 시스템은 해당 능력이 존재하지 않는 것으로 간주합니다.

1. 왜 등록만 해제하면 되나요?

현재 보스 시스템은 다음과 같은 흐름으로 작동하기 때문입니다:

  1. 보스가 능력을 뽑을 때 **BossAbilityRegistry.ALL_ABILITIES**라는 지도(Map)를 보고 "어떤 능력이 있지?"라고 물어봅니다.
  2. register를 주석 처리하면 그 지도에서 해당 능력이 지워집니다.
  3. 따라서 클래스 파일이 존재하더라도, 시스템은 그 클래스를 불러오거나 사용할 일이 없게 됩니다.

 

 

 

 


현재 공격력도 증가하고있나, 레벨업할때마다 

 

 

 

 

모든 능력이 한꺼번에 발동하는 문제 해결

현재 구조는 for 루프를 돌며 조건에 맞는 모든 능력을 실행하므로, ON_ATTACK 능력이 5개면 5개가 동시에 터집니다. 이를 막기 위해 "글로벌 쿨타임(Global Cooldown)" 시스템이 필요합니다.

해결책: BossData에 쿨타임 변수 추가

  1. BossData 클래스 abilityCooldown (int) 변수를 추가합니다.
  2. **BrutalBossLogic**에서 매 틱마다 이 숫자를 1씩 줄입니다.
  3. 능력을 발동하기 전에 **"쿨타임이 0인가?"**를 확인하고, 발동 후에는 쿨타임을 설정합니다.

 

 

 

거미줄을 그냥 던지는 것보다 **"조건"**을 걸면 더 재미있을 것 같아요.

  • 도망자 사냥: 만약 유저와의 거리가 5블록 이상 멀어지면(유저가 도망가면) 거미줄을 던져서 발을 묶음.
  • 근접전 방해: 유저가 너무 가까이 붙어서 때리면 내 발밑에 거미줄을 깔아서 유저를 느리게 만들고 나는 거리를 벌림.

 

 

 

 

 

"개별 쿨타임(Internal Cooldown)" 방식 (가장 추천)

전체 쿨타임 하나로 모든 능력을 막는 게 아니라, 각 능력마다 개별적인 타이머를 주는 방식입니다.

  • 작동 원리: 삼지창은 3초마다 체크해서 던지고, 거미줄은 5초마다 체크해서 던집니다.

 

 

 

 

 

계산 공식 (밸런스 조절 필수): 대상의 최대 체력을 100% 다 가져오면 보스가 금방 무적이 됩니다. 그래서 "대상의 최대 체력의 5~10%" 정도만 내 최대 체력에 더하는 방식을 추천합니다.

 

 

IBossAbility 인터페이스 수정 (ON_KILL 추가)

먼저, 대상을 죽였을 때 발동하는 트리거가 필요하므로 인터페이스에 한 줄을 추가해야 합니다.

 

 

최대체력 늘어나면 오류생기나

뭐 딱히 그런건 아닌듯

ㄴㄴ

public interface IBossAbility {
    // ... 기존 코드
    enum AbilityTrigger {
        ON_HURT,
        ON_TICK,
        ON_ATTACK,
        ON_KILL, // <-- 죽였을 때 트리거 추가
        PASSIVE
    }
}

 

 

 

 

 

 

 

적용 방법: 마인크래프트의 Attributes.MAX_HEALTH 속성의 **baseValue**를 직접 수정해야 합니다.

뭔소리지[   ]

아무튼 된다는거 같은데 

 

 

 

아 엔티티 죽을떄, 체력 흡수하는걸 보스로직에 그거 전체를 다쓰는게 아니라

클래스로 분리하고, 그 클래스의 함수 호출하는거네 

 

왜 NBT 저장이 따로 필요 없나요?

마인크래프트의 속성(Attributes) 시스템은 아주 똑똑합니다. setBaseValue를 통해 수정된 값은 마인크래프트 엔진이 엔티티를 저장할 때 자동으로 NBT에 포함시킵니다.

즉, 우리가 따로 compound.putDouble("MaxHealth", ...) 같은 코드를 짤 필요가 없습니다. 서버를 껐다 켜도 보스의 늘어난 체력은 그대로 유지됩니다.

 

 

 

 

동기화 타이밍

ON_KILL 능력(포식)으로 인해 최대 체력이 변했다면, 이를 클라이언트에게도 알려줘야 할 수 있습니다.

  • 마인크래프트의 속성(Attribute) 수정은 자동으로 동기화되지만, 보스 데이터와 함께 확실히 갱신하려면 ON_KILL 루프가 끝난 뒤에도 syncToNearbyPlayers를 한 번 더 호출해주는 것이 UI 갱신에 유리합니다.

 

 

 

1. "데이터의 진실"은 서버에만 있습니다 (Source of Truth)

마인크래프트는 똑같은 코드가 **내 컴퓨터(클라이언트)**와 게임 서버(서버) 양쪽에서 동시에 돌아갑니다.

  • 서버: 몹의 체력, 레벨, 킬 수, 공격력 등 모든 **"진짜 데이터"**를 관리합니다.
  • 클라이언트: 서버가 보내준 데이터를 받아서 화면에 **"보여주기"**만 합니다.

만약 이 체크가 없다면, 클라이언트(내 컴퓨터)가 혼자서 "오! 방금 몹이 죽었네? 레벨 올려야지!"라고 판단하고 데이터를 수정합니다. 하지만 진짜 서버의 데이터는 변하지 않았기 때문에, 잠시 후 서버가 "아니야, 레벨은 그대로야"라고 데이터를 보내면 클라이언트의 화면이 번쩍이며 데이터가 되돌아가는 **싱크 오류(Desync)**가 발생합니다.

2. 패킷 중복 및 낭비 방지

작성하신 코드에는 syncToNearbyPlayers(killer, data);가 포함되어 있습니다.

  • 서버에서 실행될 때: 서버가 근처 모든 플레이어에게 "보스 레벨업했다!"라고 소식을 알립니다. (정상)
  • 클라이언트에서 실행될 때: 내 컴퓨터가 서버에게 혹은 다른 사람에게 소식을 알리려고 시도합니다. 하지만 클라이언트는 다른 유저에게 직접 패킷을 보낼 권한이 없습니다. 이 과정에서 에러가 나거나 불필요한 연산이 일어납니다.

3. 영구적인 저장 (NBT)

우리가 수정한 최대 체력(baseValue)이나 보스 레벨은 서버가 세이브 파일(NBT)에 저장합니다.

  • 클라이언트에서 아무리 최대 체력을 1,000으로 바꿔봤자, 서버의 세이브 파일에는 반영되지 않습니다.
  • 결국 서버에서 코드를 실행해야만 게임을 껐다 켜도 보스의 능력치가 유지됩니다.

 

 

 

 

 

/ (기존 addAdditionalSaveData, readAdditionalSaveData에도 // "ShieldAmount", "ShieldCooldown"이라는 이름으로 저장/불러오기 로직을 추가해야 서버를 껐다 켜도 유지됩니다.)

 

ClientboundBossDataSyncPacket.java 수정 (데이터 전송)

보호막 수치가 몹 머리 위에 보이려면, 서버에서 클라이언트로 쏴주는 패킷에 이 수치를 실어 보내야 합니다.

  1. 패킷 생성자에 shieldAmount 추가.
  2. encode, decode 로직에 float 값 추가.
  3. 클라이언트에서 패킷을 받았을 때 data.setShieldAmount(packetShield) 실행.

 

 

 

 

 중요한 경고: 패킷 동기화(Sync)

onBossUpdate (매 틱 실행되는 곳) 안에서 syncToNearbyPlayers()를 호출하지 마세요!

  • 데이터가 변할 때마다 패킷을 보내면 네트워크 과부하로 서버가 터질 수 있습니다.
  • 체력이나 킬 수 처럼 중요한 값이 변할 때만(DeathEvent, HurtEvent 등) 패킷을 보내는 것이 안전합니다.

 

 

[   ]

지금 매틱마다 확인하나, 

20틱에 한번(1초에 한번 확인해도되나, ) 

<아니면 이건 꽤나 주요한 변경사항인간, 

 

쿨타임 계산해야하는 등의 공격은, 

<이런건 최대한 안만들고 기본 공격할떄 같이 나가도록 하는게 나으려나 

 

 

 

공격할 때 같이 나가는 게(ON_ATTACK) 나은가요?

네, 훨씬 낫습니다! 이유는 다음과 같습니다.

  1. 성능: 공격할 때만 로직이 돌아가므로 서버 부하가 가장 적습니다.
  2. 전투 리듬: 뜬금없이 발동되는 것보다 보스가 플레이어를 때릴 때 "번개가 치거나", "공중에 띄우는" 것이 플레이어 입장에서 대처하기 쉽고 공정하게 느껴집니다.
  3. 구현의 편의성: ON_ATTACK 트리거를 쓰면 공격 대상(target)이 확실하기 때문에 따로 주변에 누가 있는지 찾을 필요가 없습니다.

 

 

 

 

매 틱(20Hz) vs 1초(1Hz)에 한 번?

결론부터 말씀드리면: "쿨타임 감소"는 매 틱 해야 하지만, "능력 실행(검사)"은 1초에 한 번만 해도 충분합니다.

  • 쿨타임 계산: 매 틱마다 -1씩 해줘야 시간이 정확하게 흐릅니다. 1초에 한 번씩 -20을 하면 수치가 뚝뚝 끊겨 보일 수 있습니다.
  • 능력 실행: 예를 들어 "주변 플레이어에게 독 걸기" 같은 오라 능력은 0.05초(1틱)마다 검사할 필요가 전혀 없습니다. 1초에 한 번만 검사해도 플레이어는 차이를 못 느낍니다.

 

 

 

방어도 쿨타임[   ]

랜더링 추가 

 

 

 

제공해주신 로그와 에러 코드 -1073741819를 종합해 볼 때, 이 문제는 자바 코드의 일반적인 에러(NullPointerException 등)가 아니라 **JVM(자바 가상 머신) 자체가 메모리 접근 오류로 인해 즉사(Crash)**했을 때 발생합니다.

특히 마인크래프트 1.18.2 환경에서 이 코드가 발생한다면, 최근에 수정한 능력(Ability) 로직이나 패킷(Packet) 부분이 트리거가 되었을 가능성이 높습니다. 다음 단계별로 확인해 보세요.

1. 무한 루프(Infinite Loop) 확인 (가장 유력)

능력 실행 로직 중에 자기 자신을 계속 호출하거나, 서로를 호출하는 코드가 있는지 확인해야 합니다.

  • 상황: onBossUpdate가 매 틱마다 실행되는데, 여기서 호출하는 ability.execute() 내부에서 다시 엔티티의 상태를 변경하여 다른 이벤트를 발생시키고, 그 이벤트가 다시 능력을 호출하는 구조인가요?
  • 확인: 만약 execute 안에서 데미지를 입히거나(hurt), 엔티티를 업데이트하는 로직이 있다면 무한 재귀 호출이 발생해 스택이 터지면서 JVM이 즉사할 수 있습니다.
  • 해결: execute 내부에서 호출하는 메서드들이 또 다른 이벤트를 발생시키지 않는지 검토하세요.

2. 패킷(Packet) 읽기/쓰기 순서 불일치

최근에 ClientboundBossDataSyncPacketshieldAmount를 추가하셨습니다. 만약 **보낼 때(toBytes)**와 **받을 때(생성자)**의 데이터 순서나 종류가 하나라도 다르면 버퍼(Buffer)가 깨지면서 게임이 즉시 튕깁니다.

현재 코드 다시 체크:

  • 쓰기 (toBytes): writeInt(id) -> writeInt(level) -> writeInt(kills) -> writeInt(tier) -> writeFloat(shield) -> writeMap(abilities)
  • 읽기 (생성자): readInt(id) -> readInt(level) -> readInt(kills) -> readInt(tier) -> readFloat(shield) -> readMap(abilities)
  • 체크: 이 순서가 BossNetwork에 등록된 순서와 정확히 일치하는지, 혹은 다른 클래스에서 패킷을 생성할 때 인자를 빼먹지 않았는지 확인하세요.

3. Capability 초기화 시점 문제

onBossUpdate에서 getCapability를 호출할 때, 아직 Capability가 완전히 준비되지 않은 상태에서 접근하면 네이티브 레벨에서 충돌이 날 수 있습니다.

  • if (!entity.isAlive()) return; 코드를 onBossUpdate 최상단에 추가하여 죽은 엔티티나 초기화 중인 엔티티에 대한 접근을 막아보세요.

4. hs_err_pid 파일 찾기 (핵심 단서)

프로젝트 폴더(보통 build.gradle이 있는 위치)에 hs_err_pidXXXX.log라는 이름의 파일이 생성되었을 것입니다.

  • 이 파일을 열어 "Problematic frame:" 섹션을 보세요.
  • 거기에 ig75icd64.dll이나 nvoglv64.dll 같은 이름이 있다면 그래픽 드라이버 충돌입니다.
  • 반면 java.exe 내부의 특정 주소가 적혀 있다면 코드 로직(무한 루프 등)의 문제입니다.

5. 해결을 위한 추천 조치 (순서대로)

  1. Gradle 정지: 터미널에 ./gradlew --stop 입력.
  2. 캐시 삭제: 프로젝트 폴더의 run 폴더 안에 있는 options.txtconfig 폴더를 지우고 실행.
  3. 코드 주석 처리: onBossUpdate 내부의 for문(능력 실행 부분)을 통째로 주석 처리하고 실행해 보세요. 만약 안 튕긴다면 능력 구현 코드 중 하나에 문제가 있는 것입니다.

특히 1.18.2 Forge 40.3.0 버전은 안정적이지만, 렌더링이나 네트워크 패킷 순서가 틀리면 아무런 에러 메시지 없이 바로 창이 닫히는 특성이 있습니다. 최근에 추가한 패킷의 순서와 onBossUpdate 내의 로직을 가장 먼저 의심해 보시기 바랍니다.

 

 

 

 

 

 

  • 처음 (안 됐던 코드): font.drawInBatch를 직접 호출하면서 복잡한 X좌표 계산을 넣었습니다.
  • Java
     
    font.drawInBatch(shieldText, font.width(hpText) / 2f + 5, 18, 0x55FFFF, ...);
  • 지금 (수정된 코드): 이미 잘 작동하던 스킬 출력용 헬퍼 메서드인 **drawCenteredText**를 그대로 재사용했습니다.
  • Java
     
    drawCenteredText(font, poseStack, buffer, shieldText, 28, 0x55FFFF, event.getPackedLight());

 

 

 

 

먼소리야[   ]

fill메소드가 안좋다는건가 

 

왜 처음 코드는 튕겼을까요?

가장 큰 이유는 "렌더링 상태의 오염" 때문입니다.

  1. 범인은 GuiComponent.fill (체력바):
    처음 코드에서는 체력바를 그릴 때 GuiComponent.fill을 썼습니다. 이 메서드는 "즉시 그리기" 방식입니다. 호출하자마자 그래픽 카드에 명령을 내립니다.
  2. drawInBatch (텍스트):
    반면 텍스트를 그리는 drawInBatch "모아서 그리기" 방식입니다. 장바구니에 담아뒀다가 나중에 한꺼번에 그립니다.
  3. 한글 폰트의 로딩:
    처음 코드에는 **"(+보호막: )"**이라는 한글이 들어있었습니다. 마인크래프트에서 한글을 그리려면 실시간으로 폰트 이미지를 생성해서 메모리에 올려야 합니다.
  4. 충돌 발생:
    fill 메서드가 그래픽 카드의 설정을 "즉시 그리기 모드"로 바꿔버린 상태에서, 한글 폰트를 생성하기 위해 메모리에 접근하려고 하니 그래픽 드라이버가 **"순서가 틀렸다"**고 판단해서 강제로 프로그램을 종료(Access Violation)시킨 것입니다.

3. 지금 코드는 뭐가 다른가요?

  1. 검증된 메서드 사용: drawCenteredText는 이미 상단 텍스트나 스킬 목록을 그릴 때 문제없이 작동하던 메서드입니다. 이미 장바구니(buffer)를 안정적으로 사용하도록 설정되어 있습니다.
  2. 영어 텍스트 사용: Shield:는 한글과 달리 추가적인 폰트 로딩 과정이 거의 필요 없습니다. 그래서 그래픽 카드에 주는 부담이 훨씬 적습니다.
  3. 좌표의 분리: 체력 수치 옆에 억지로 붙이려다 보면 좌표 계산 과정에서 미세한 오차가 생길 수 있는데, 아예 한 줄 아래(Y=28)로 내려서 다른 텍스트들과 간섭이 없게 만들었습니다.

요약하자면

**"즉시 그리기(fill)가 망쳐놓은 운동장 위에, 복잡한 한글 폰트(drawInBatch)를 직접 그리려다가 넘어진 것"**이 이전 상황입니다.

지금 코드는 **"안전하게 작동하던 스킬 출력용 통로(drawCenteredText)를 그대로 빌려 쓰고, 텍스트를 가볍게 바꿔서 통과시킨 것"**입니다. 그래서 튕기지 않는 것입니다.

 

 

 

 

 

1. fill 메서드의 정체 (2D용)

GuiComponent.fill은 원래 인벤토리, 메뉴 화면, 타이틀 화면 같은 2D UI를 그리라고 만든 도구입니다.

  • 작동 방식: 호출되는 즉시 그래픽 카드에 "지금 당장 그려!"라고 명령합니다. (Immediate Mode)

2. RenderLivingEvent 환경 (3D용)

지금 코드를 작성 중인 몹 머리 위 렌더링은 3D 월드 안에서 벌어지는 일입니다.

  • 작동 방식: 마인크래프트는 성능을 위해 "그릴 것들을 장바구니에 다 모아뒀다가 한꺼번에 그려!"라고 명령합니다. (Batch Mode)

3. 왜 튕겼을까요? (충돌의 원인)

  1. 체력바를 그릴 때 fill이 그래픽 카드 설정을 **"2D 모드"**로 강제로 바꿔버립니다.
  2. 그다음에 텍스트를 그리려고 drawInBatch가 장바구니를 열었는데, 그래픽 카드는 이미 fill 때문에 **"2D 모드"**에 고정되어 있습니다.
  3. 3D 렌더링 도중에 이런 설정 꼬임이 발생하면 그래픽 드라이버가 **"데이터가 오염됐다"**고 판단하고 안전을 위해 게임을 강제로 꺼버리는 것입니다. (-1073741819 에러의 본질)

4. 결론

  • 체력바만 있을 때: 운 좋게 그래픽 드라이버가 버텨준 것입니다.
  • 방어도 텍스트를 추가했을 때: 렌더링 연산이 늘어나면서 드라이버가 더 이상 버티지 못하고 터진 것입니다.

해결책은 간단합니다:
앞으로는 몹 머리 위처럼 3D 세계에서 뭔가를 그릴 때는 fill 대신, 제가 이전에 예시로 드린 **drawRect (VertexConsumer 방식)**를 사용하는 것이 정석입니다. 이 방식은 텍스트와 똑같이 "장바구니에 담았다가 나중에 같이 그리는" 방식이라 절대 튕기지 않습니다.

 

 

 

 

체력바, 이방식으로 바꾸기

미래의 튕김방지 

 

 

 

 숫자를 더 키울수록(더 큰 양수 값) 글자가 오른쪽으로 이동합니다.

마인크래프트의 렌더링 좌표계에서 X축은 0이 중앙이고, + (양수)는 오른쪽, - (음수)는 왼쪽을 의미합니다.

 

 

현재 보호막이 작동을 안하는데 

[   ]

쿨타임 기다려해서 그런걸지도 

 

 

 

왜 1.18.2는 다른가요?

1.18.2는 마인크래프트가 현대적인 렌더링 엔진으로 넘어가는 과도기적인 버전입니다.

  • Matrix4f: 1.19.3 이전까지는 마인크래프트 내부에 포함된 com.mojang.math 패키지의 행렬 연산자를 썼습니다.
  • RenderType.lightning(): 1.18.2에서는 UI 전용 렌더타입이 3D 월드 이벤트(RenderLivingEvent)와 완벽하게 통합되어 있지 않아서, 비슷한 속성(텍스처 없음, 색상 위주)을 가진 lightning 타입을 빌려 쓰는 것이 가장 안전하고 안 튕기는 방법입니다.