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

마크모딩) 부모 클래스 추상화 / NBT 데이터 영구 저장 / 주민 빵

kimchangmin02 2025. 12. 22. 12:06

 

 

 

 

[  ]좀비, 아군등도 하나의 폴더밑에 넣어둬도되나, 

그냥 자동으로 리펙터링되나 

 

 

 

 

[  ]소문, 거래,자연적으로 나타남, 유적지 만들기 ,전리품

 

[  ]아기에게 주면, 즉시 어른으로 되어버리는 아이템 

 

[  ]단순히 마주보는거 말고, 뭔가 추가적인 행동가능하도록 할수잇나 

주민번식빵

재미잇는거 잇나, 

주민이 할수잇는 기본 행동양식이 뭐가 잇지 

 

 

[  ]사랑의 묘약, 

어쩌면 좀비에게 매혹 걸수도

 

[  ]

구미호같은 캐릭터 만들수도, 

무조건 따라오도록 해서 

 

 


 

[ok  ]이거 수정, 좀 이름 같은걸로 구분할수있게 하는 방법잇다면서 

  1. 변수명이 아니라 "역할"에 집중: 궁수의 kills와 저거너트의 damageTaken을 **Experience**라는 공통 용어로 통합했습니다. 하지만 getMaxExperienceForLevel 메소드 덕분에 궁수는 '1'을 반환하고 저거너트는 '50'을 반환함으로써 각자의 개성을 유지합니다.

 

 

 

[ ok ]일단, 자주 쓰이는 클래스는 인터페이스로 구현해둘수잇을것같은데 

 

 

[  ok ]ImyModMob구현하고잇으면

칼모양이지만, 맞으면 한번에 죽는 아이템 만들수도 

 

 

[ ok ]부모 클래스 REAMME파일 만들기 

[ ok ]뭔가 비슷한 파일이나 클래스 정리<하거나

README파일 만들어놔야겟는데 

 


[ ok ]예전에 실패햇던 주민 번식빵에 대해 


 

 

📂 목차

제 1장: 프로젝트 구조화 및 추상화 (Project Refactoring)

  • 1.1 역할 중심의 변수 통합: kills(궁수)와 damageTaken(저거너트)을 Experience라는 공통 용어로 통합하여 개성 유지 및 코드 간결화.
  • 1.2 인터페이스 규격화: 자주 쓰이는 기능을 IMyModMob 인터페이스로 구현하여 다중 상속 효과 및 기능 확장성 확보.
  • 1.3 프로젝트 문서화: 부모 클래스와 핵심 로직의 역할을 명시하는 README.md 작성 및 클래스 정리.

제 2장: 데이터 관리 및 네트워크 최적화 (Data & Network)

  • 2.1 패킷 충돌 근본 해결: entityId를 활용하여 단 하나의 패킷(AllyDataSyncPacket)으로 수만 종류의 아군 데이터를 구별하여 전송하는 시스템.
  • 2.2 Capability 시스템의 이해:
    • 엔티티별 독립적 데이터 주머니(AllyDataProvider) 귀속 원리.
    • Map 구조를 쓰지 않고 엔티티 객체 자체에서 데이터를 꺼내는 최적화 방식.
  • 2.3 NBT 영구 저장 메커니즘: serializeNBT deserializeNBT를 통한 자바 객체와 파일 데이터 간의 변환 로직.

제 3장: 엔티티 및 전투 매커니즘 구현 (Core Mechanics)

  • 3.1 마커 인터페이스와 처단검: IMyModMob 구현체만 감지하여 즉사시키는 이벤트 기반 특수 아이템 로직.
  • 3.2 엔티티 등록 프로세스: Registry Object 미발생 오류 해결 및 능력치(Attribute) 설정 시점 최적화.
  • 3.3 렌더링 및 레이어:
    • 엔티티 머리 위 레벨/경험치 바 시각화 (fill 로직을 이용한 막대 구현).
    • 주민 모델 기반의 커스텀 레이어(옷, 방패 등) 렌더링.
  • 3.4 AI 타겟팅 분리: 타겟 포착(Target Goal)과 실제 행동(Action Goal)의 분리를 통한 비공격형 아군(탱커 등) 설계.

제 4장: 주민 번식 시스템 고도화 (Advanced AI)

  • 4.1 리플렉션 한계 극복: 바닐라 주민의 WANTED_ITEMS 수정 불가(final) 문제를 해결하기 위한 메모리 주입 방식 채택.
  • 4.2 메모리 조작을 통한 행동 제어:
    • WALK_TARGET LOOK_TARGET을 강제 주입하여 10초간의 최면 및 연애 연출 구현.
    • 장애물 감지: 경로 탐색(createPath) 확인을 통해 벽 너머의 아이템 무시 로직 추가.
  • 4.3 번식 매커니즘 및 쿨타임 제어:
    • 침대 체크 및 기본 번식 조건을 무시하는 강제 아기 탄생 로직.
    • age 변수 직접 조작(setAge(0))을 통한 부모 주민의 번식 쿨타임 즉시 초기화.
    • 아기 주민의 간섭 방지: isBaby() 체크를 통한 아이템 가로채기 차단.

 

 

 


 

패킷 충돌 해결: entityId가 열쇠입니다.

"궁수 패킷"과 "탱커 패킷"을 따로 만들 필요가 없습니다. ClientboundAllyDataSyncPacket 하나만 만드세요.

  • 해결 방법: 패킷 안에 entityId를 넣어서 보냅니다.
  • 원리: 클라이언트가 패킷을 받으면 world.getEntity(msg.entityId)로 엔티티를 찾습니다. 이때 찾은 엔티티를 부모 클래스인 AbstractAllyEntity로 캐스팅해서 데이터를 넣어주면 됩니다.
  • 충돌 없음: 마인크래프트의 모든 엔티티는 고유한 ID를 가지므로, 패킷 하나로 수만 종류의 아군 데이터를 구별 없이 보낼 수 있습니다.

 

 

 

 

 

NBT 중복이나 꼬임 문제는 **"데이터를 저장하는 주머니(NBT)를 하나로 합칠 것인가, 아니면 따로 둘 것인가"**의 문제입니다.

 

 

 

 

 

1. "Map 형태로 저장해야 하나요?" (아니오, 그럴 필요 없습니다)

사용자님은 아마 **"모든 아군 데이터가 한 주머니(ALLY_DATA)에 들어가면, 누가 누군지 구별하려고 Key-Value(Map)가 필요한 거 아닌가?"**라고 생각하신 것 같아요.

하지만 마인크래프트의 Capability 시스템은 **"엔티티 객체 각각"**에게 주머니를 달아주는 방식입니다.

  • 비유: 전교생에게 "명찰"을 달아준다고 해봅시다.
    • ALLY_DATA라는 명찰 양식(Capability)은 하나입니다.
    • 하지만 학생(엔티티)마다 자기 가슴에 명찰을 하나씩 달고 있습니다.
    • 학생 A의 명찰을 본다고 해서 학생 B의 이름이 적혀있지 않죠?
  • 실제 동작:
    • 궁수 A가 생성되면 -> ALLY_DATA 주머니가 하나 생겨서 궁수 A에게 붙습니다.
    • 탱커 B가 생성되면 -> 또 다른 ALLY_DATA 주머니가 하나 생겨서 탱커 B에게 붙습니다.
    • 각 주머니는 서로 완전히 독립된 메모리 공간입니다. 따라서 Map을 써서 구별할 필요 없이, 그냥 entity.getCapability(...)를 하면 그 엔티티의 전용 데이터만 나옵니다.

2. "애초에 DataProvider가 왜 필요한 거죠?" (존재 이유)

이게 가장 핵심적인 질문입니다. 그냥 ArcherData 클래스만 있으면 되지, 왜 복잡하게 DataProvider라는 클래스를 하나 더 만들어서 감싸야 할까요?

그 이유는 **"마인크래프트(마이크로소프트/모장)가 만든 엔티티 코드"**와 "내가 만든 데이터" 사이를 연결해주는 어댑터(연결 잭) 역할을 하기 때문입니다.

DataProvider가 하는 3가지 핵심 일:

  1. 데이터 노출 (getCapability):
    마인크래프트의 엔티티는 수많은 데이터(체력, 위치, 이름 등)를 가지고 있습니다. 누군가 "이 엔티티의 아군 데이터를 줘!"라고 요청했을 때, "어, 여기 있어"라고 데이터를 꺼내주는 입구 역할을 합니다.
  2. 파일 저장 및 불러오기 (serializeNBT, deserializeNBT):
    이게 가장 중요합니다. 사용자님이 만든 ArcherData는 그냥 자바 객체입니다. 게임을 끄면 사라지죠.
    • 게임을 저장할 때: 마인크래프트가 DataProvider에게 **"야, 네가 가진 데이터 NBT(파일)로 변환해서 내놔"**라고 시킵니다.
    • 게임을 켤 때: 마인크래프트가 NBT 파일을 읽어서 DataProvider에게 **"여기 예전에 저장된 데이터 파일 있으니까 다시 데이터 객체에 집어넣어"**라고 시킵니다.
    • 즉, DataProvider가 없으면 게임을 껐다 켰을 때 레벨이 1로 초기화됩니다.
  3. 지연 로딩 (LazyOptional):
    엔티티가 수백 마리인데 사용하지도 않는 데이터를 미리 다 만들어두면 메모리가 아깝겠죠? DataProvider는 실제 그 데이터가 필요해지는 순간에 딱 맞춰서 데이터를 생성하는 센스 있는 역할도 합니다.

 

 

 

 

그 저장 공간은 '마인크래프트의 엔티티 객체' 그 자체가 내부적으로 가지고 있습니다. 개발자가 직접 Map<Entity, Data> 같은 걸 만들 필요가 없도록 포지(Forge) 프레임워크가 설계를 해둔 것입니다.

 

 

 

 

 

 

 

 

1. 엔티티는 '기본적으로' 빈 주머니 보관함을 가지고 있습니다.

마인크래프트의 모든 엔티티(Entity 클래스)는 태어날 때부터 내부에 **CapabilityDispatcher**라는 일종의 **"커스텀 데이터 보관함"**을 하나씩 품고 태어납니다.

  • 처음에는 이 보관함이 비어 있습니다.
  • 우리가 쓴 AttachCapabilitiesEvent는 이 보관함에 **"내 주머니(DataProvider)도 하나 넣어줘!"**라고 포스트잇을 붙이는 작업과 같습니다.

2. "벨크로(찍찍이)"처럼 붙어버립니다. (Event의 역할)

엔티티가 생성되는 순간, 포지가 전국의 모드들에게 소문을 냅니다. "자, 지금 엔티티 하나가 태어났는데 여기 데이터 주머니 달아줄 사람?"

그때 사용자님이 작성하신 아래 코드가 실행됩니다.

if (event.getObject() instanceof AbstractAllyEntity) {
    // 해당 엔티티 객체 내부의 '보관함'에 내 AllyDataProvider를 쏙 집어넣음
    event.addCapability(new ResourceLocation(MODID, "ally_data"), new AllyDataProvider());
}

이 로직이 실행되면, 그 엔티티 객체 내부에 사용자님의 AllyDataProvider가 물리적으로(메모리상으로) 귀속됩니다. 이제 이 주머니는 그 엔티티가 죽어서 사라질 때까지 한 몸입니다.

3. 꺼낼 때는 "그 엔티티에게" 직접 물어봅니다.

우리가 entity.getCapability(...)를 호출할 때, 내부에서는 이런 일이 벌어집니다.

  1. 사용자가 archerA.getCapability(...)를 호출함.
  2. archerA라는 객체 내부의 전용 보관함을 뒤짐.
  3. 거기 들어있는 여러 주머니 중 ALLY_DATA라는 이름표가 붙은 주머니를 찾아서 반환함.

그래서 Map이 필요 없는 것입니다. 데이터가 Entity 객체 안에 들어있으니까요! 아르바이트생 100명에게 각각 개인 사물함을 하나씩 준 것과 같습니다. 사장님은 "전체 사물함 리스트"를 관리할 필요 없이, 그냥 그 아르바이트생에게 가서 "네 사물함 열어봐"라고 하면 되는 원리입니다.


4. 그럼 저장(Save)은 누가 알아서 해주나요?

네, 마인크래프트가 알아서 해줍니다. 마인크래프트가 월드를 저장할 때 엔티티들을 하나하나 돌면서 이렇게 물어봅니다.

  • 마인크래프트: "자, archerA야. 너 저장할 시간이다. 네 몸뚱아리에 붙어있는 주머니들 다 나와봐."
  • 포지(Forge): "여기 AllyDataProvider라는 주머니가 붙어있네요."
  • 마인크래프트: "오케이, AllyDataProvider야. 네가 가진 데이터를 NBT 형식으로 나한테 줘. 내가 파일(level.dat이나 엔티티 파일)에 써넣을게."

이 과정이 DataProvider 안에 있는 serializeNBT 메소드를 통해 자동으로 일어납니다.

요약하자면:

  1. 저장 공간: 엔티티 객체 내부의 비공개 리스트.
  2. 연결 방식: 엔티티가 생성될 때 '이벤트'를 통해 엔티티 몸에 직접 붙여버림.
  3. 데이터 구별: getCapability를 호출한 **'주체(엔티티 객체)'**가 누구냐에 따라 자동으로 자기 데이터를 꺼냄.

이 구조 덕분에 우리는 **"이 데이터가 누구 거지?"**를 고민할 필요 없이, 그냥 entity.getCapability(...) 한 줄만 믿고 쓰면 되는 것입니다. 아주 편한 시스템이죠!

 

 

 

 

 

 

 

궁수(Archer)**와 **저거너트(Juggernaut)**의 모든 클래스와 메소드를 나열한 뒤, 이를 바탕으로 공통 요소를 추출

1단계: 현재 코드의 모든 클래스 및 메소드 나열

[Archer Entity 관련]

  • ArcherData: getLevel, setLevel, getKills, setKills, addKill, saveNBTData, loadNBTData
  • ArcherDataProvider: createArcherData, getCapability, serializeNBT, deserializeNBT
  • ArcherEntity: Constructor, registerGoals, updateTrades, rewardTradeXp, getBreedOffspring, getAmbientSound
  • ArcherEntityRenderer: Constructor, getTextureLocation
    • (내부) ArcherCustomLayer: render
    • (내부) ArcherHeldBowLayer: render
  • ArcherEntityTypeInit: ENTITY_TYPES (변수), ARCHER (변수)
  • ArcherEvents: onEntityAttributeCreation, updateArcherMaxHealth
    • (내부) ClientEvents: registerRenderers
    • (내부) ForgeEvents: onAttachCapabilities, onFriendlyFireCheck, onLivingDeath, onStartTracking
  • ArcherItemInit: ITEMS (변수), ARCHER_SPAWN_EGG (변수)
  • ArcherNetwork: register, sendToPlayer
  • ArcherRenderer (HUD): onRenderLiving
  • ClientboundArcherDataSyncPacket: Constructor, toBytes, handle
  • VillagerSkirmishGoal: canUse, canContinueToUse, stop, tick, performSingleShot

[Juggernaut Entity 관련]

  • JuggernautData: getLevel, setLevel, getDamageTaken, setDamageTaken, addDamageTaken, saveNBTData, loadNBTData
  • JuggernautDataProvider: createJuggernautData, getCapability, serializeNBT, deserializeNBT
  • JuggernautEntity: Constructor, createAttributes, registerGoals, updateTrades, rewardTradeXp, getBreedOffspring
  • JuggernautEntityRenderer: Constructor, getTextureLocation
    • (내부) JuggernautClothingLayer: render
    • (내부) JuggernautShieldLayer: render
  • JuggernautEntityTypeInit: ENTITY_TYPES (변수), JUGGERNAUT (변수)
  • JuggernautEvents: onAttributeCreate, registerRenderers, syncToAll
    • (내부) ForgeEvents: onAttachCaps, onDamageTransfer, onJuggernautHurt, onStartTracking
  • JuggernautInfoRenderer (HUD): updateClientData, onClientTick, onRenderInfo
  • JuggernautItemInit: ITEMS (변수), JUGGERNAUT_SPAWN_EGG (변수)
  • JuggernautNetwork: register, sendToPlayer
  • ClientboundJuggernautDataSyncPacket: Constructor, toBytes, handle
  • JuggernautRegenGoal: canUse, tick
  • JuggernautTauntGoal: canUse, tick, performTaunt, applyAggro, removeBuff
  • JuggernautSkirmishGoal: isTaunting, canUse, canContinueToUse, tick, findNearestAlly, stop

2단계: 공통 요소 묶기 (추상화 대상)

위 목록을 비교해 보면, 이름만 다를 뿐 구조가 똑같은 **"중복 영역"**이 뚜렷하게 보입니다.

1. 데이터 관리 (Data & Capability)

  • 공통점: level 변수, save/loadNBT 로직, Capability를 붙이고 관리하는 공급자(Provider) 로직이 동일함.
  • 차이점: 경험치 계산 기준(kills vs damageTaken).
  • 추상화: BaseAllyData, BaseAllyDataProvider 하나로 통합 가능.

2. 엔티티 기본 설정 (Entity & Goals)

  • 공통점:
    • AbstractVillager 상속 및 IVillagerAlly 구현.
    • 거래 금지(updateTrades, rewardTradeXp), 번식 금지(getBreedOffspring).
    • 기본 AI(FloatGoal, LookAtPlayerGoal, RandomLookAroundGoal).
    • 레벨에 따른 최대 체력 증가 로직.
  • 추상화: AbstractAllyEntity라는 부모 클래스에서 위 기능을 모두 처리.

3. 네트워크 통신 (Network & Packet)

  • 공통점: entityId, level, experience를 서버에서 클라이언트로 동기화하는 로직.
  • 차이점: 패킷 클래스 이름과 보낼 변수 개수.
  • 추상화: AllyDataSyncPacket 하나로 통합하여 모든 아군 엔티티가 공유.

4. 렌더링 및 HUD (Renderer)

  • 공통점:
    • 엔티티 머리 위에 레벨, 경험치 바, 체력 정보를 띄우는 좌표 계산 및 렌더링 로직.
    • 주민 모델 위에 옷 레이어를 입히는 방식.
  • 추상화: AllyHUDOverlay (공통 HUD), BaseAllyRenderer (기본 렌더러 구조).

5. AI 알고리즘 (Skirmish/Kiting Goal)

  • 공통점: 타겟과의 거리를 재고, 너무 가까우면 도망가고(Flee), 너무 멀면 추적하며, 시야가 있는지 확인하는 카이팅(Kiting) 로직.
  • 차이점: 도망가는 거리 수치, 공격 방식(화살 vs 도발).
  • 추상화: AbstractSkirmishGoal을 만들고 구체적인 행동만 자식이 오버라이드.

 

 

 

 

 

 

  • saveNBTData / loadNBTData: 메모리 카드의 내용을 실제 하드디스크 파일(NBT)로 저장하고 불러옵니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Java의 상속(부모 클래스)은 "코드"의 중복을 줄여줄 뿐,

마인크래프트 게임 엔진에 이 존재들을 등록하는 과정(Registry)과 리소스(JSON)는 엔티티마다 각각 직접 작성해주어야 합니다.

마인크래프트 입장에서는 부모가 누구든 상관없이 **"새로운 ID(experiment_archer)를 가진 존재가 추가되었다"**는 사실을 알아야 하기 때문입니다.

 

 

 

 

Registry Object not present: mymod:experiment_archer**입니다.

이것은 마인크래프트가 궁수 엔티티의 능력치(체력, 속도 등)를 설정하려고 시도했지만, 정작 "궁수"라는 엔티티 자체가 아직 게임 시스템(Registry)에 등록되지 않았기 때문에 발생한 NullPointerException입니다.

 

 

 

 

 

 

 

[ ok ]

기존 아처 스폰알은 좀비를 먼저 때렷던가 

ㅇㅇ

 

 

 

 

 

 

 

  1. 렌더러 문제: 이전 코드에는 font.drawInBatch만 있었고, 네모난 칸을 채우는 GuiComponent.fill 로직이 빠져 있었습니다. 그래서 글자만 보이고 막대기는 안 보였던 것입니다.
  2. AI 문제: 부모 클래스인 AbstractAllyEntity는 **"맞으면 화낸다(반격)"**는 공통 로직만 가지고 설계되었습니다. **"몬스터를 보면 쏜다"**는 궁수만의 특징이므로 궁수 클래스의 addCustomGoals NearestAttackableTargetGoal을 적어줘야 작동합니다.

 

 

 

타겟을 잡는 것"**과 **"공격 행동을 하는 것"**은 마인크래프트 AI 시스템에서 완전히 별개의 단계입니다.

  • Target Goal (타겟Selector): "누구를 내 적으로 삼을 것인가?"를 결정합니다. (눈으로 적을 포착하는 단계)
  • Action Goal (골Selector): "타겟이 정해졌을 때 무엇을 할 것인가?"를 결정합니다. (화살을 쏠지, 도망갈지, 주먹을 휘두를지 결정하는 단계)

따라서 탱커(저거너트)도 몬스터를 타겟으로 잡아야 **"저 좀비가 나를 보게 유도(도발)하고, 거리를 벌려야겠다"**는 판단을 할 수 있습니다. 공격을 안 하게 하는 것은 Action Goal에서 결정하면 됩니다.

 

 

 

 

 

모든 몹의 코드를 일일이 수정하지 않고도 **"내 모드의 모든 몹을 한 번에 죽이는 다이아 검"**을 만드는 가장 효율적인 방법은 **마커 인터페이스(Marker Interface)**와 **포지 이벤트(Forge Event)**를 조합하는 것입니다.

이 방법을 쓰면 몹이 100마리여도 **수정해야 하는 코드는 단 한 곳(이벤트 핸들러)**뿐입니다.

 

 

 

 클래스는 부모를 하나만 가질 수 있지만(상속), 인터페이스는 여러 개를 동시에 가질 수 있고(구현), 심지어 인터페이스끼리 서로 상속도 가능

 

 

 

 

 

 

 

 

[마인크래프트 기본 클래스]          [내 모드 인터페이스]
      Entity                         IMyModEntity (최상위: 처단검용)
        │                                 ▲
        │                                 │
  LivingEntity                   ┌────────┴────────┐
        │                        │                 │
 AbstractVillager <─── AbstractAllyEntity    IZombieAlly (좀비아군 표식)
        │             (레벨/NBT/HUD 공통)          ▲
        │                 ▲      ▲                 │
        │                 │      │                 │
        └──────────── Archer   Juggernaut     CustomZombies

 

 

 

 

 

 

 

대충 태그를 이용하네 

 

 

 

 

 

 

 

 

그림처럼 검은색과 보라색 체크무늬가 나오는 이유는 마인크래프트가 해당 아이템의 텍스처 파일(.png)을 찾지 못했기 때문입니다.

기본 빵과 똑같은 모양으로 보이게 하려면, JSON 파일에서 텍스처 경로를 mymod가 아니라 마인크래프트 기본 경로인 minecraft로 수정해줘야 합니다.

 

 

 

 

왜 이렇게 하나요?

  • 기존 코드: "layer0": "mymod:item/new_villager_breed_bread"
    • 이것은 마인크래프트에게 "내 모드 폴더(mymod) 안에 있는 new_villager_breed_bread.png 파일을 써라"라고 명령하는 것입니다. 하지만 해당 파일이 없기 때문에 체크무늬가 뜬 것입니다.
  • 수정 코드: "layer0": "minecraft:item/bread"
    • 이것은 "마인크래프트 기본 팩에 들어있는 bread.png 파일을 그대로 가져다 써라"라는 뜻입니다.

만약 빵 모양인데 색깔만 바꾸고 싶다면?

나중에 직접 그린 텍스처를 쓰고 싶다면, src/main/resources/assets/mymod/textures/item/ 폴더 안에 new_villager_breed_bread.png 파일을 직접 넣고 다시 원래 경로(mymod:item/...)로 되돌리면 됩니다.

 

 

 

 

 

 

 

 

 

주민이 아이템을 줍지 않는 이유는 마인크래프트 주민의 **'관심 아이템 목록(WANTED_ITEMS)'**에 우리가 만든 커스텀 빵이 등록되어 있지 않기 때문입니다. 주민은 자신이 모르는 아이템은 바닥에 있어도 쳐다보지 않습니다.

 

 

 

 

 

 

 

주민이 아이템을 "줍는 AI"가 작동하길 기다리지 않고, 코드가 매 순간 주민 발밑에 이 빵이 있는지 감시합니다<성능저하문제없나,

 

 

 

 

 

 

 

 

1. 매번 검사하는 방식 (LivingUpdateEvent)

이 방식은 주민의 의사와 상관없이 **모드(외부 프로그램)**가 강제로 주민의 주머니를 뒤지는 것과 같습니다.

  • 동작: 주민이 100명이면, 매 초 20번씩(2000번/초) "너 발밑에 빵 있니?"라고 묻는 연산을 실행합니다.
  • 비효율성: 주민은 가만히 있어도 CPU는 계속 주민 주변의 모든 엔티티를 검색해서 아이템인지 확인해야 합니다. 주민이 빵 근처에 있든 말든 무조건 검색합니다.

2. AI 주도권 방식 (WANTED_ITEMS 등록)

이 방식은 주민의 **'시각 시스템'**에 "이 빵은 맛있는 거야"라는 정보를 입력하는 방식입니다.

  • 동작: 주민은 원래부터 바닥에 떨어진 빵, 감자, 당근을 찾기 위해 **NearestItemSensor**라는 시각 센서를 작동시키고 있습니다.
  • 구현 원리: 주민의 뇌 안에는 WANTED_ITEMS라는 '내가 좋아하는 아이템 목록'이 있습니다. 우리는 여기에 우리 빵을 추가만 합니다.
  • 차이점:
    1. 추가 연산 없음: 마인크래프트 주민은 어차피 바닥에 떨어진 아이템을 스캔합니다. 우리는 그 스캔 목록에 아이템 이름 하나만 더 넣었을 뿐입니다. (기존에 돌아가는 엔진을 그대로 쓰는 것)
    2. AI 최적화: 마인크래프트는 모든 주민의 AI를 동시에 계산하지 않습니다. 한 틱에는 1번 주민, 다음 틱에는 2번 주민 식으로 나눠서(Batching) 계산합니다. 우리가 만든 코드는 매 틱 모든 주민을 다 검사하지만, 바닐라 AI는 자체적인 스케줄에 따라 연산을 분산 처리하여 렉을 방지합니다.

3. 주민이 빵을 찾는 과정 (이론적 흐름)

주민이 땅에 떨어진 빵을 찾는 것은 "지도를 다 알고 있어서"가 아니라 아래와 같은 '센서 거리' 시스템을 사용합니다.

  1. 센서 작동 (NearestItemSensor): 주민은 반경 약 8~16블록 내의 아이템 엔티티를 주기적으로 스캔합니다.
  2. 목록 대조: 스캔된 아이템 중 WANTED_ITEMS 목록에 있는 것이 있는지 확인합니다.
  3. 기억(Memory) 저장: 목록에 있는 아이템을 발견하면 주민의 브레인 내 NEAREST_VISIBLE_WANTED_ITEM 메모리에 해당 아이템 위치를 저장합니다.
  4. 행동 실행 (GoToWantedItem): 메모리에 아이템 위치가 들어오면 주민의 AI는 즉시 "저 아이템으로 걸어가라"는 명령을 내리고 줍는 동작을 수행합니다.

 

 

그래서 이거 차이가 뭔데 

 

 

 

 

 

 

차이점이 명확하게 이해되지 않는 이유는 **"마인크래프트가 이미 하고 있는 일"**과 **"우리가 새로 시키는 일"**의 경계가 모호하기 때문일 것입니다. 아주 쉽게 비유와 함께 설명해 드릴게요.


1. 두 방식의 진짜 차이 (비유로 이해하기)

상황: **전교생(주민 100명)**이 운동장에 있고, 누군가 **특정한 사탕(빵)**을 주우면 상을 줘야 합니다.

방식 1: 매번 검사 (LivingUpdateEvent)

  • **교사인 당신(모드)**이 1초에 20번씩 전교생 100명에게 일일이 달려가서 **"너 지금 발밑에 사탕 있니?"**라고 묻습니다.
  • 학생들이 사탕 근처에 있든 없든, 당신은 계속 뛰어다니며 100명에게 물어야 합니다.
  • 문제점: 학생이 많아질수록 교사인 당신은 숨이 차서 쓰러집니다. (이것이 서버 렉입니다.)

방식 2: AI 주도권 (WANTED_ITEMS 등록)

  • 학생들은 이미 자기 발밑에 맛있는 게 있는지 찾아보는 **본능(바닐라 AI)**이 있습니다.
  • 당신은 학생들의 머릿속에 있는 **'좋아하는 간식 목록'**에 이 사탕 이름을 몰래 적어넣습니다.
  • 이제 당신은 교무실에 앉아서 쉽니다. 학생 중 누군가가 사탕을 발견하고 **"선생님, 저 사탕 주웠어요!"**라고 외칠 때(이벤트 발생)만 나가서 상을 주면 됩니다.
  • 장점: 당신(모드)은 평소에 아무 일도 안 합니다. 원래 하던 본능적인 행동은 학생들이 알아서 하기 때문에 추가적인 힘이 들지 않습니다.

결론: 방식 1은 내가 직접 노가다를 하는 것이고, 방식 2는 주민이 원래 하던 일에 항목 하나만 추가하는 것입니다.


2. 리플렉션(Reflection)이 도대체 뭐야?

마인크래프트의 소스코드는 우리가 마음대로 바꿀 수 없는 **'잠긴 상자'**와 같습니다.

  • 주민 클래스 안에는 WANTED_ITEMS라는 목록이 들어있는데, 제작자(Mojang)가 이걸 **private (사적인)**으로 설정해놨습니다.
  • private의 의미: "이 목록은 주민 클래스 내부에서만 쓸 거야. 외부인(모더)은 절대 손대지 마!"라는 뜻입니다. 일반적인 코딩 방식으로는 이 목록에 우리 빵을 넣을 수 없습니다.

여기서 리플렉션은 '마스터 키'입니다.

리플렉션은 자바(Java) 언어가 제공하는 특수한 기능으로, **"보안이고 뭐고 다 무시하고 잠긴 상자를 강제로 열어서 내용물을 수정하겠다"**는 기법입니다.

  1. 접근: 원래는 볼 수 없는 주민의 비밀 목록(WANTED_ITEMS)을 강제로 찾아냅니다.
  2. 수정 권한: "수정 금지"라고 붙어있는 딱지를 강제로 떼어냅니다.
  3. 삽입: 우리 모드의 '번식 빵'을 그 목록에 쏙 집어넣습니다.
  4. 복구: 다시 상자를 닫습니다.

즉, 리플렉션은 마인크래프트가 꽁꽁 숨겨둔 내부 설정을 억지로 열어서 고치는 '해킹'과 비슷한 기술이라고 이해하시면 됩니다.


3. 왜 리플렉션을 써야만 하나요?

주민이 우리 빵을 **"빵"**이라고 인식하게 하려면 주민의 뇌 구조(WANTED_ITEMS)를 바꿔야 하는데, 마크 제작자가 그걸 수정 못 하게 막아놨기 때문에 리플렉션이라는 마스터 키를 써서 강제로 우리 빵을 등록하는 것입니다.

그렇게 한 번 등록만 해두면, 주민은 그때부터 우리 빵을 보고 **"오! 저건 내가 주워야 할 맛있는 거야!"**라고 생각하게 됩니다.

요약하자면:

  • 차이점: 내가 계속 감시하느냐(렉 유발), 아니면 주민이 스스로 발견하게 만드느냐(렉 없음)의 차이입니다.
  • 리플렉션: 주민의 뇌 속 '비밀 목록'을 강제로 열어서 우리 아이템을 추가하는 비밀 도구입니다.

이해가 좀 되셨나요? 리플렉션은 복잡해 보이지만 **"잠긴 데이터를 강제로 고치는 마법 도구"**라고 생각하시면 제일 정확합니다!

 

 

 

 

 

 

아, 클릭해서 줄수잇게하면, 여기서 어떤 상호작용이 발생할수잇겟네 

 

 

 

로그를 보니 java.lang.NoSuchFieldException: f_35380_ 오류가 발생했습니다. 이는 개발 환경(IntelliJ/Eclipse)에서 마인크래프트 코드가 "탈외계어화(De-obfuscated)"되어 있어서, 컴퓨터가 f_35380_이라는 이름 대신 실제 이름인 **WANTED_ITEMS**를 찾고 있기 때문입니다.

또한, 주민이 아이템을 무시하는 이유는 이 리플렉션이 실패해서 주민의 "관심 목록"에 등록되지 않았기 때문입니다.

질문하신 **"이게 뇌를 건드리는 게 아닌가?"**에 대한 답은 **"아니오"**입니다.

  • 뇌를 건드린다: 주민의 '생각하는 순서(AI)'를 바꾸는 것 (위험함)
  • 리플렉션: 주민이 가지고 있는 '메모장(아이템 목록)'에 이름 하나를 몰래 적어두는 것 (안전함)

 

 

 

 

 

 

 

  1. 리플렉션 오류: WANTED_ITEMS static final(정적 상수)이라서 자바 보안상 강제로 수정하려고 하면 IllegalAccessException이 발생하며 거부됩니다.
  2. 동작 피드백: 빵을 주웠을 때 즉시 아기가 나오는 것이 아니라, 10초간 서로 마주 보며 하트가 나오는 과정이 필요합니다.

 

 

 

 

결국 원하는 아이템 목록은 수정못하는거네, 

그러면 이를 어떻게 해결할수있는거지 

 

 

 

 

리플렉션이나<final이라서 원하는 음식 못바꿈

AT

외부 주입식 기억<성능저하, 버벅임, idle상태일때 다른 할거잇으면 충돌

 

 

 

 

 

 

 

 

  1. 아이템 발견: LivingUpdateEvent에서 1초에 한 번씩 주민 주변을 봅니다. (렉 없음)
  2. 유혹: 빵이 보이면 주민의 뇌 속 WALK_TARGET 메모리에 빵 위치를 넣습니다. (주민은 자기가 가고 싶어서 가는 줄 압니다.)
  3. 가로채기: 주민이 빵 근처에 오면 아이템을 지우고, 우리만의 10초 타이머를 켭니다.
  4. 강제 고정: 10초 동안은 LOOK_TARGET 메모리에 파트너를 넣어 강제로 서로만 보게 만듭니다.
  5. 탄생: 10초 뒤, 모든 조건을 무시하고 breed() 함수를 호출해 아기를 소환합니다.

 

 

  1. 도착 및 시식 (Eat): 주민이 빵의 위치(WALK_TARGET)에 도달하여 거리가 1.5블록 이내가 되면, 아이템을 제거(shrink)하고 해당 주민에게 연애 중이라는 NBT 태그(타이머)를 붙입니다.
  2. 파트너 강제 지정 (Match): 빵을 먹은 주민 주위에서 즉시 다른 주민을 찾습니다. 찾으면 두 주민 모두에게 동일한 타이머와 상대방의 UUID를 저장합니다.
  3. 10초간의 최면 (Hypnosis): 주민의 본래 AI가 "이제 일하러 가야지"라고 생각할 틈을 주지 않도록, LivingUpdateEvent에서 매 틱마다 상대방을 LOOK_TARGET WALK_TARGET으로 뇌에 주입합니다.
  4. 강제 탄생 (Birth): 타이머가 0이 되는 순간, 침대 체크 함수를 거치지 않고 getBreedOffspring을 직접 호출하여 아기를 소환합니다.

 

 

 

 

주민 번식 쿨타임의 원리 (age 변수)

마인크래프트의 모든 동물과 주민은 AgeableMob이라는 클래스를 상속받습니다. 이 클래스에는 **age**라는 핵심 변수가 모든 것을 결정합니다.

  • age == 0: 번식 가능 상태 (성인).
  • age > 0: 번식 쿨타임 중. (기본값은 6000틱 = 5분). 이 숫자가 매 틱마다 1씩 줄어들어 0이 되어야 다시 번식할 수 있습니다.
  • age < 0: 아기 상태. (기본값은 -24000틱 = 20분). 이 숫자가 매 틱마다 1씩 늘어나 0이 되면 성인이 됩니다.

2. 이 변수는 final인가요?

아니오, 절대 아닙니다.
매 틱마다 숫자가 변해야 하므로 final일 수 없으며, 마이크래프트 소스코드에서 protected int age로 선언되어 있습니다. 또한, 이 값을 마음대로 바꿀 수 있는 **setAge(int)**라는 public 메소드가 이미 존재합니다.

3. 상속으로 바꿀 수 있나요?

이론적으로는 Villager를 상속받은 MyVillager를 만들어 setAge 로직을 바꿀 수 있지만, 마인크래프트 월드에 이미 스폰되어 있는 바닐라 주민들에게는 적용되지 않습니다.

따라서 상속보다는 우리가 만든 이벤트 핸들러에서 setAge(0)을 호출하는 것이 가장 확실하고 쉬운 방법입니다.

 

 

 

 

부모 주민의 setAge(0)을 설정했다면, 빵을 2개 주면 정확히 2번 번식하게 됩니다.

다만, 주민이 동시에 빵 2개를 한꺼번에 먹고 아기를 한 명만 낳는 "아이템 낭비"가 생기지 않도록 내부적으로 어떻게 돌아가는지 그 순서를 설명해 드릴게요.


1. 빵 2개로 2번 번식하는 과정 (시나리오)

  1. 첫 번째 빵 인식: 주민 A가 바닥에 떨어진 빵 1번을 발견하고 걸어가서 먹습니다.
  2. 연애 시작: 주민 A와 파트너 주민 B의 머릿속에 BREEDING_TIMER = 200(10초)이 생깁니다.
  3. 두 번째 빵 대기: 연애 중(timer > 0)일 때는 제 코드의 if (timer > 0) return; 부분 때문에 주민들이 추가로 빵을 찾지 않고 연애에만 집중합니다. (이게 중요합니다! 빵을 한꺼번에 다 먹어버리는 걸 방지합니다.)
  4. 첫 번째 아기 탄생: 10초 뒤 아기가 태어납니다. 이때 부모의 age 0으로 초기화되고, BREEDING_TIMER 0이 됩니다.
  5. 두 번째 빵 인식: 이제 주민들이 다시 자유로운 상태가 되었습니다. 바닥에 남은 빵 2번을 발견합니다.
  6. 두 번째 연애 및 탄생: 다시 10초간 하트를 띄우고 두 번째 아기를 낳습니다.

결론: 빵 개수만큼 아기가 태어납니다. (단, 한 번 낳을 때마다 10초의 연애 시간은 필요합니다.)

 

 

 

 

 

 

 

 

설계 변경 내용 (이론)

  1. 빵 탐색 시: 단순히 거리만 보지 않고, 주민의 AI에게 "저 빵까지 가는 길이 있니?"라고 물어봅니다 (createPath). 길이 없으면 무시합니다.
  2. 파트너 탐색 시: 주변 주민들 중 "걸어서 도달할 수 있는(Reachable)" 주민만 필터링합니다. 벽 뒤에 있거나 갇혀 있는 주민은 후보에서 제외됩니다.
  3. 연애 중 체크: 연애 중에도 장애물 때문에 서로 닿지 못하면 아기를 낳지 못하게 거리를 엄격히 제한합니다.

 

 

 

 

 

 

 

 

마인크래프트에서 **아기 주민은 age 값이 음수(보통 -24000)**입니다. 따라서 빵을 찾거나 주울 때 **age >= 0**인지 확인하는 조건만 추가하면 아기가 빵을 뺏어 먹는 문제를 완벽하게 해결할 수 있습니다.

 

 

 

 

아기 주민의 age

마인크래프트에서 아기가 태어날 때 setAge(-24000)을 설정합니다. 이 값은 시간이 지날수록 커져서(예: -23999, -23998...) 0이 되는 순간 성인이 됩니다. 따라서 !isBaby() 또는 age >= 0은 이론적으로 같지만, 메서드를 쓰는 것이 가장 안전합니다.