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

마크모딩) 도망만 다니던 주민을 전투 요원으로

kimchangmin02 2025. 12. 21. 17:43

feat)궁수작업대 궁수 vs 새로운 클래스 궁수 비교를 기반으로 

[  ]자료구조,알고리즘 부분 좀 어려운데 패스할까 

일단 도망자체의 효율을 지금 수정해야하는건 아니니깐 

 

[  ]

protected void updateTrades() { /* 거래 목록 없음 */ }

부분을 수정해서, 좀 재미있는 거래목록, 

대신에 랜덤으로 설정되도록 할수도잇겟는데

원본은 어떤 모습이지 

뭔가 유저가 특별한 할인아이템 가지고있다던가, 어떤 유물 가지고 있으면, 

특별 아이템 팔수있도록 하는것도

아, 주민 스폰알을 이런 방식으로 팔게 할수도 

 

또한 이러한 경비병 느낌으로 할거면, 모험가 주변에 있도록, goal변경도 해야겟는데 

 

 

 

 

[  ]카이팅 안하는 문제

(아니면 레벨업 함에따라, 카이팅 하는 아처도 있을수도

 

주의사항 (카이팅 부재): 코드 주석에는 "카이팅(도망)을 유도한다"고 적혀 있고 생성자에 fleeDist 인자가 있지만, 실제 tick() 소스 코드 내에는 뒤로 도망가는 로직이 구현되어 있지 않습니다. 현재 상태로는 거리가 좁혀져도 도망가지 않고 제자리에서 활만 쏩니다.

 

 

 

[   ]적 찾을떄 Monster.으로 찾고잇나 

 

 

 

 

[  ]

Villager 클래스에는 GossipContainer가 있어 주민들끼리 소문을 퍼뜨립니다.

재밋는거 할수있을지도

일단 이거 관련 연구는 궁수 관련 끝나고 

 

 

 

 

[  ]

도망치다가 모서리에 가게되면 멍청해지는 기분

 

[  ]탱커,힐러에 대해서도, 작업대 말고 스폰알로

 

[  ]힐러, 포션 던지게, 

레벨업 함에따라 광역힐도되게,

메이플처럼, 언데드에게 큰 피해주는 공격할수도, 

혹은 주변 주민들(이건 인터페이스 구현해야겟지만, )레벨 올리게 할수도 

 

 

 

 


🚩 목차 (Table of Contents)

1. 서론: 왜 주민은 도망만 다닐까?

  • 주민 AI의 기본 메커니즘 분석
  • 오늘의 목표: 도망과 공격을 동시에 하는 '지능형 궁수(Archer)' 만들기

2. 이론 편: 마인크래프트의 길 찾기(Pathfinding) 시스템

  • A (A-star) 알고리즘과 이진 힙(Binary Heap)*
    • 왜 BFS(너비 우선 탐색)가 아닌 A*를 사용하는가?
    • 목적지를 알고 움직이는 '휴리스틱'의 효율성
  • GroundPathNavigation 분석
    • 3D 좌표 보정 (위아래 스캔 로직)
    • 문(Door)을 인식하고 통과하는 원리

3. 문제 발생: Villager 클래스 상속의 늪

  • Brain 시스템 vs GoalSelector의 충돌
    • 주민 특유의 'Panic(공포)' 로직이 커스텀 AI를 방해하는 현상
    • 엔티티가 버벅이고 떨리는 현상의 원인 분석
  • 해결책: 상속 구조 변경
    • Villager  AbstractVillager로 갈아타기 (불필요한 Brain 제거)

4. 구현 편: 카이팅(Kiting)하는 궁수 AI 설계

  • SkirmishGoal: 거리에 따른 행동 변화
    • 너무 가까우면? DefaultRandomPos를 활용한 지능적 후퇴
    • 적정 거리라면? 제자리 공격 및 조준 로직
  • A 알고리즘을 활용한 도망 좌표 계산*
    • 단순히 반대로 뛰는 게 아닌, '장애물을 피해 도망갈 수 있는 안전한 좌표' 찾기

5. 렌더링 편: 투명인간이 된 주민 구출하기

  • 텍스처 레이어의 함정
    • 왜 얼굴 없이 옷만 보일까? (Body vs Overlay 텍스처)
  • ArcherRenderer 수정
    • 기본 피부(villager.png)와 직업 의상(fletcher.png)을 겹쳐 그리는 방법

6. 디테일 강화: 아군 식별 및 Friendly Fire 방지

  • IVillagerAlly 인터페이스 활용
    • "이름표" 확인을 통한 타겟팅 제외 로직
    • 실수로 아군을 맞춰도 데미지가 들어가지 않게 처리하는 법

7. 마무리: 빌드 경고 해결 및 최적화

  • 사용되지 않는 Import 및 중복 변수 정리 (IDE 팁)
  • 향후 개선 사항 (레벨업에 따른 거래 목록 랜덤화 등)

 

 

 

 


 

마인크래프트 주민의 경로 탐색(Pathfinding)은 A (A-star) 알고리즘*을 사용하며, 핵심 자료구조는 **이진 힙(Binary Heap / Priority Queue)**입니다.

제공해주신 Villager 클래스 코드에서 **GroundPathNavigation**이라는 부분을 보셨을 겁니다. 이 객체가 실제 길 찾기를 수행하는 엔진인데, 내부 구조를 요약해 드릴게요.

1. 사용되는 알고리즘: A* (A-star)

마인크래프트는 단순히 넓게 퍼지는 BFS(너비 우선 탐색)를 쓰지 않습니다. 목적지까지의 직선거리를 계산하는 휴리스틱(Heuristic) 값을 사용하는 A* 알고리즘을 사용합니다.

  • BFS와의 차이: BFS는 모든 방향을 다 뒤지지만, A*는 "목적지 방향" 위주로 먼저 탐색해서 훨씬 빠릅니다.

2. 핵심 자료구조

A* 알고리즘을 구현하기 위해 다음 자료구조들을 사용합니다.

  1. Binary Heap (이진 힙 / Min-Heap):
    • 역할: 탐색 중인 좌표들 중 목적지까지의 예상 비용()이 가장 적은 노드를 즉시 꺼내기 위해 사용합니다. (Java의 PriorityQueue와 유사한 자체 구현체인 BinaryHeap 클래스를 사용함)
    • f(n)=g(n)+h(n)f(n) = g(n) + h(n)
  2. Long2ObjectMap (해시 맵):
    • 역할: 이미 방문한 좌표(Closed List)를 저장합니다. 3차원 좌표 를 하나의 long 값으로 압축해서 키로 사용해 중복 방문을 방지합니다.
    • (X,Y,Z)(X, Y, Z)
  3. PathPoint (노드 객체):
    • 역할: 각 좌표의 위치 정보와 부모 노드 정보(어디서 왔는지)를 담고 있는 개별 노드입니다.

3. 현재 클래스에서 알 수 있는 부분

제공하신 Villager 클래스 생성자에서 다음 줄을 확인하세요:

    ((GroundPathNavigation)this.getNavigation()).setCanOpenDoors(true);
  
  • GroundPathNavigation: 이 클래스가 바로 A* 알고리즘을 실행하는 주체입니다.
  • 3D 좌표 계산: findSpawnPositionForGolemInColumn 같은 메소드에서 보듯, 상하(below(), offset()) 좌표를 모두 계산하여 3D 경로를 찾습니다.

4. 지하/지상 및 문 인식 로직

  • 지형 판단: 탐색 과정에서 각 블록의 BlockState를 확인하여 "지나갈 수 있는가(Air/Liquid)"와 "발을 딛을 수 있는가(Solid)"를 체크합니다.
  • 문(Door): setCanOpenDoors(true) 설정이 되어 있으므로, A* 알고리즘이 경로를 계산할 때 문 블록을 "통과 불가능한 벽"이 아니라 "비용을 지불하고 열 수 있는 통로"로 계산합니다.

결론:
주민은 이진 힙 자료구조를 활용한 A 알고리즘*으로 최단 경로를 찾습니다. BFS보다 훨씬 효율적이며 3D 공간의 높낮이와 문 같은 장애물을 모두 수치화하여 계산합니다. 지금 바로 찾으셔야 한다면 *"A Algorithm with Binary Heap"**이 정답입니다.

 

 

 

 

 

네, A 알고리즘*이 마인크래프트 같은 게임 환경에서 **BFS(너비 우선 탐색)**보다 훨씬 효율적이고 뛰어난 이유는 명확합니다.

가장 큰 이유는 **"목적지가 어디인지 알고 가느냐, 모르고 가느냐"**의 차이입니다. 네트워크 수업에서 배운 개념과 연결하여 쉽게 설명해 드릴게요.


1. 효율성: 탐색 범위의 차이 (가장 큰 이유)

  • BFS (눈먼 탐색): 목적지가 어디인지 고려하지 않습니다. 출발점에서 사방팔방으로 똑같이 퍼져 나갑니다. (마치 연못에 돌을 던졌을 때 퍼지는 동그란 파도와 같습니다.)
    • 문제점: 목적지가 동쪽에 있는데 서쪽, 남쪽, 북쪽을 다 뒤져야 하므로 메모리와 CPU를 엄청나게 낭비합니다.
  • A (지능적 탐색):* 목적지의 좌표를 알고 있습니다. 그래서 **"목적지 방향"**에 있는 길을 최우선적으로 탐색합니다. (마치 나침반을 들고 목적지를 향해 걸어가는 것과 같습니다.)
    • 결과: BFS보다 방문하는 블록(노드)의 수가 압도적으로 적습니다.

2. 비용 계산: 가중치(Weight) 처리

  • BFS: 모든 블록의 이동 비용이 1이라고 가정합니다.
  • A:* 각 경로의 **'비용'**을 계산할 수 있습니다. 마인크래프트는 3D 공간이라 단순히 옆으로 가는 것 외에도 여러 상황이 있죠.
    • 예시: "평지로 걷기(비용 1)" vs "물속에서 헤엄치기(비용 5)" vs "문 열고 지나가기(비용 2)".
    • 주민은 단순히 가까운 길이 아니라, 시간이 적게 걸리는 최적의 길을 찾아야 하므로 가중치를 계산하는 A*가 필수적입니다.


4. 한눈에 비교 요약

특징 BFS (너비 우선 탐색) A* (A-star) 알고리즘
인식 목적지를 모름 (Blind) 목적지 위치를 활용 (Heuristic)
탐색 모양 동그란 원형 (모든 방향 탐색) 목적지를 향한 타원형/직선형
가중치 불가능 (모든 길의 가중치가 같음) 가능 (험한 길, 편한 길 구분)
속도 느림 (방문 노드가 너무 많음) 매우 빠름 (필요한 곳만 탐색)
자료구조 일반 Queue () Binary Heap ()

결론

마인크래프트는 수많은 엔티티(주민, 몬스터 등)가 동시에 길을 찾아야 하는 실시간 환경입니다. BFS를 쓰면 수십 마리의 주민이 길을 찾을 때마다 서버가 멈출 것입니다.

따라서 목적지라는 힌트를 활용해 탐색 범위를 대폭 줄이는 A* 알고리즘과, 최적의 후보를 빠르게 뽑아내는 이진 힙 자료구조의 조합을 사용하는 것입니다.

빨리 찾아야 하신다면 이 키워드를 기억하세요:

"A*는 **방향성(Heuristic)**이 있어 BFS보다 빠르고, 이진 힙으로 최솟값을 빨리 찾기 때문에 실시간 게임에 최적이다."

 

 

 

 

 

도망칠때

계속 검사하는게 아니라 일정 틱마다 타이머를 두고 검사<충돌이나, 렉 막으려고 

투명,

ㅂㅕㄱ뒤 적 감지등도 코드의 멤버변수로 되어있네

 

 

 

 

 

 

 

제공해주신 GroundPathNavigation 클래스는 마인크래프트의 주민, 좀비 같은 **지상 이동 엔티티의 '내비게이션 시스템(GPS)'**입니다.

사용자님이 궁금해하셨던 3차원 좌표 계산, 지하/지상 판단, 문 통과 로직이 이 코드에 어떻게 녹아있는지 기술적으로 분석해 드립니다.


1. 핵심 동작: "어디를 딛고 갈 것인가?" (3D 좌표 처리)

이 클래스의 가장 중요한 역할은 공중에 떠 있는 좌표나 땅속 좌표를 '걸어갈 수 있는 위치'로 보정하는 것입니다.

  • createPath(BlockPos, int) 메소드:
    • 대상 좌표가 공기(Air)일 때: 목적지가 공중이라면, below() 메소드를 사용하여 바닥이 나올 때까지 아래로 스캔합니다. (while 루프) 바닥을 찾으면 그 바로 윗칸을 최종 목적지로 잡습니다.
    • 대상 좌표가 고체(Solid)일 때: 목적지가 벽 속이라면, above() 메소드를 사용하여 천장이 뚫린 곳이 나올 때까지 위로 스캔합니다.
    • 결론: 이 로직 덕분에 주민은 "지하 10층에 있는 좀비"나 "2층 테라스"를 정확히 3차원적으로 구분하여 찾아갈 수 있습니다.

2. 문(Door)에 대한 인식

    public void setCanOpenDoors(boolean p_26478_) {
   this.nodeEvaluator.setCanOpenDoors(p_26478_);
}
  
  • 주민은 생성자에서 이 값을 true로 설정합니다.
  • 작동 원리: 길 찾기 알고리즘(A*)이 경로를 계산할 때, 문 블록을 만난다면 이를 "막힌 벽"이 아니라 **"잠시 멈춰서 열고 지나갈 수 있는 통로"**로 점수를 매깁니다. 그래서 문 뒤에 좀비가 있어도 "문만 열면 갈 수 있는 길"이라고 판단하여 경로를 생성합니다.

3. 햇빛 회피 로직 (avoidSun)

    protected void trimPath() {
   if (this.avoidSun) {
      // 하늘이 보이는 곳(햇빛)이 경로에 있다면 그 이후의 경로는 잘라버림(truncate)
   }
}
  
  • 주로 낮에 타죽는 좀비나 스켈레톤에게 활성화되는 기능입니다.
  • 주민의 경우 보통 false이지만, 이 로직이 들어있음으로써 엔티티가 **단순히 최단 거리가 아니라 '안전한 길'**을 선호하게 만들 수 있습니다.

4. 수직 좌표 보정 (getSurfaceY)

    private int getSurfaceY() {
   if (this.mob.isInWater() && this.canFloat()) {
      // 물속에 있다면 수면 높이를 계산해서 반환
   }
   return Mth.floor(this.mob.getY() + 0.5D);
}
  
  • 엔티티가 물에 떠 있을 때나 경사진 곳을 오를 때, 현재 서 있는 정확한 지면의 Y축 높이를 계산합니다. 길 찾기 노드(Node)를 생성할 때 Y축 오차를 줄여서 "계단"이나 "반 블록"을 자연스럽게 오르내리게 합니다.

5. 사용자 질문에 대한 답변

  1. 지하나 지상 인식:
    • createPath 메소드 내의 below()/above() 루프가 핵심입니다. 엔티티는 목적지의 X, Z만 보는 게 아니라 Y축을 따라 위아래로 훑으며 실제로 발을 딛고 설 수 있는 블록(Solid)을 찾습니다. 따라서 지하 동굴이나 건물 옥상을 명확히 구분합니다.
  2. 문 뒤의 좀비 인식:
    • 이 클래스는 "어떻게 갈 것인가(GPS)"만 담당합니다. "문 뒤에 좀비가 있다"는 것을 인식하는 것은 이전에 보신 Sensor 클래스의 역할입니다.
    • 센서가 좀비를 발견하면, 이 GroundPathNavigation에게 좀비의 반대 방향 좌표를 주면서 "이리로 가는 길을 만들어!"라고 명령합니다.
  3. 알고리즘 및 자료구조:
    • 이 클래스는 createPathFinder에서 **PathFinder**를 호출합니다.
    • 내부적으로 A 알고리즘*을 사용하며, 탐색 후보지들을 관리하기 위해 이진 힙(Binary Heap) 자료구조를 사용해 가장 비용이 적은(빠른) 경로 노드를 우선적으로 추출합니다.

요약

이 코드는 **"3D 공간에서 발을 딛을 수 있는 실제 좌표를 찾고(Vertical Scan), 문을 통과할 수 있는지 확인하며, 물이나 햇빛 같은 위험 요소를 피해 경로를 다듬는 역할"**을 수행합니다. 즉, AI의 **'실행 팔다리'**에 해당하는 핵심 로직입니다.

 

 

 

 

 

 

날카로운 질문입니다! **"도망치는 거면 그냥 멀어지는 건데, 딱 정해진 목적지가 있나?"**라는 의문이 드는 게 당연합니다.

결론부터 말씀드리면, 주민은 도망칠 때도 '가짜 목적지' 또는 '안전 지대'라는 명확한 좌표를 찍고 움직입니다. 목적지 없이 그냥 뛰는 게 아니에요.

도망칠 때 주민이 목적지를 정하는 3가지 방식은 다음과 같습니다.


1. 반대 방향의 '가상 지점' 설정 (Escape Point)

좀비가 내 서쪽에 있다면, 주민의 AI는 즉석에서 **"내 위치에서 동쪽으로 10블록 떨어진 지점"**의 좌표를 계산합니다.

  • 작동: "동쪽 10블록 지점"을 임시 목적지로 설정합니다.
  • A*의 역할: 그냥 직선으로 뛰면 벽에 막힐 수 있으니까, 그 임시 목적지까지 A 알고리즘*을 돌려서 장애물을 피해 도망가는 길을 찾습니다.

2. 안전 지대(POI) 찾아가기 (Point of Interest)

주민 클래스 코드에서 보셨던 MemoryModuleType.HOME이나 POTENTIAL_JOB_SITE 기억나시나요?

  • 작동: 좀비를 만나 패닉(PANIC) 상태가 되면, 주민은 자신의 기억 속에 있는 **'내 침대(집)'**나 **'마을 종(Meeting Point)'**의 좌표를 목적지로 잡습니다.
  • A*의 역할: "가장 가까운 집 안"을 목적지로 찍고, 거기로 가는 최단 경로를 A*로 계산해서 뛰어갑니다.

3. 무작위 안전 좌표 탐색 (Random Stroll in Panic)

갈 곳(집)이 없는 주민이라도 AI는 주변 10~15블록 이내의 좌표 중 "좀비와 가장 멀어지는 좌표" 하나를 랜덤하게 골라 목적지로 삼습니다.


왜 그냥 "좀비 반대로 걷기"를 안 하고 A*를 쓰나요?

만약 A*를 안 쓰고 그냥 "반대로 걷기"만 한다면 이런 바보 같은 상황이 생깁니다.

  1. 주민 뒤에 **담장(울타리)**이 있습니다.
  2. 좀비가 앞에서 옵니다.
  3. 주민은 반대로 가고 싶지만 담장에 막혀서 제자리에서 비비기만 합니다. (사망)

하지만 A*를 쓰면:

  1. 좀비 반대편의 담장 너머 좌표를 목적지로 찍습니다.
  2. A 알고리즘*이 "아, 뒤는 막혔으니 옆에 있는 문으로 나가서 돌아가야겠네!"라고 돌아가는 길을 찾아냅니다.
  3. 주민은 지능적으로 담장을 피해 도망갑니다.

요약: 주민의 도망 시나리오

  1. 센서: "10시 방향에 좀비 발견!"
  2. AI 결정: "4시 방향에 있는 우리 집(또는 4시 방향 10블록 뒤)을 목적지로 정하자!"
  3. A 알고리즘:* "그 목적지까지 가려면... 앞에 상자가 있으니까 오른쪽으로 꺾어서 가야겠군." (이때 이진 힙 사용)
  4. 내비게이션: 결정된 길을 따라 실제로 다리를 움직여 이동.

즉, **도망칠 때의 목적지는 '내가 도망가서 도착할 안전한 좌표'*입니다. 이 좌표가 있어야만 A 알고리즘이 작동할 수 있습니다!

 

 

 

 

 

 

 

 

 

 

 

 "가상의 목적지"는 언제 갱신되나? (Dynamic Refresh)

이 부분이 가장 궁금하셨을 텐데, 목적지는 고정되어 있지 않고 계속 변합니다.

(1) 갱신 타이밍 (Re-pathing)

주민이 도망칠 때 목적지는 다음과 같은 상황에서 재계산(Refresh) 됩니다:

  • 매 20틱 (1초): 마인크래프트 AI는 보통 1초에 한 번씩 "내가 지금 잘 가고 있나?"를 체크합니다.
  • 좀비가 움직였을 때: 좀비가 내 왼쪽에서 오른쪽으로 이동하면, 아까 찍어둔 "오른쪽으로 도망가기" 좌표는 더 이상 안전하지 않습니다. 이때 즉시 목적지를 반대편으로 다시 찍습니다.
  • 길이 막혔을 때: 누군가 도망가는 길에 블록을 놓으면 GroundPathNavigation이 "어? 길이 끊겼네?"를 감지하고 다시 A*를 돌립니다.

(2) 도착 전까지 그대로인가?

아니요. 목적지에 도착하기 전이라도 상황이 변하면 목적지를 버리고 새 목적지를 잡습니다.

  • 이유: 좀비가 계속 쫓아오기 때문입니다. 목적지에 도착했는데 좀비가 아직 옆에 있으면 다시 **"여기서 또 10블록 먼 곳"**을 찍고 계속 튑니다.

 

 

 

 

 

 

사용자님의 질문은 전산학적으로 매우 중요한 포인트입니다. **"지형 정보를 미리 알고 있는가?"**와 **"왜 굳이 탐색을 하는가?"**에 대한 답을 드리겠습니다.


1. 지형 정보는 이미 '메모리'에 있습니다 (O(1) 조회)

마인크래프트 서버는 실행되는 동안 주변의 블록 데이터를 **'청크(Chunk)'**라는 단위로 **RAM(메모리)**에 이미 다 올려두고 있습니다.

  • 자료구조: 보통 3차원 배열(또는 팔진트리/Octree 구조)로 저장되어 있습니다.
  • 접근 방법: level.getBlockState(x, y, z)라는 함수를 호출하면, 복잡한 계산 없이 배열의 인덱스 조회처럼 아주 빠르게 해당 좌표에 무엇(돌, 흙, 공기 등)이 있는지 알 수 있습니다. 이는 ****의 속도입니다.
  • O(1)

즉, 지형을 알아내기 위해 BFS를 쓸 필요는 전혀 없습니다. 지형은 이미 '지도'로써 주어져 있습니다.


2. 그럼 왜 A*나 탐색 알고리즘이 필요한가요?

지형 정보(지도)가 있다고 해서 **"어디로 가야 할지(경로)"**가 자동으로 나오는 것은 아니기 때문입니다.

  • 지형 정보: "(10, 64, 10) 좌표에는 돌 블록이 있다." (데이터)
  • 경로 탐색: "좀비를 피해서 집까지 가려면 어떤 칸들을 순서대로 밟아야 벽에 안 막히고 도착할까?" (계산)

비유를 들자면:
우리가 구글 지도를 켰을 때, 지도는 이미 화면에 다 있죠? (지형 정보 확보). 하지만 우리가 "강남역에서 홍대역까지 가는 길 찾기" 버튼을 누르면, 구글 서버는 수많은 도로 중 최적의 길을 계산해야 합니다. 그 계산 과정이 바로 A*입니다.


3. BFS vs A*: "계산 범위"의 차이

여기서 말씀하신 효율성의 차이가 나옵니다.

  • BFS (무작위 확장):
    지도를 알고 있더라도, 목적지가 어디인지 무시하고 출발점 근처의 모든 빈 공간을 다 뒤져봅니다. (목적지가 동쪽인데 서쪽 끝 골목길까지 다 계산함)
  • A (목적지 방향 위주 계산):*
    이미 알고 있는 지도 데이터 중에서, **"목적지와 가까워 보이는 방향의 칸"**들만 골라서 우선적으로 길을 만들어봅니다.

자료구조때 배운게 나온데

신기

일단, 더 자세한거까지 들어가려니깐 머리도 아프고

지금 당장은 수정할필요가있는건 아니니깐 

 

 

 

 

 

 

 

농부(Farmer)와 같은 특정 직업의 거래 목록이나 **특수 행동(작물 수확 등)**은 Villager.java 클래스 안에 직접 구현되어 있지 않습니다. 마인크래프트는 코드를 분리하여 관리하기 때문에, 다음 두 곳을 찾아가야 합니다.

1. 직업별 거래 목록 (아이템 목록)

주민이 레벨업할 때 어떤 아이템을 파는지 정의된 코드는 net.minecraft.world.entity.npc.VillagerTrades 클래스에 있습니다.

  • 찾아가는 법: IDE(IntelliJ 등)에서 Shift를 두 번 누르고 VillagerTrades를 검색하세요.
  • 코드 내용: 이 클래스 안에는 TRADES라는 거대한 Map이 있습니다.
    • 예를 들어, 농부의 거래는 VillagerProfession.FARMER라는 키값에 연결된 ItemListing 배열들로 정의되어 있습니다.
    • increaseMerchantCareer()가 실행되면 이 VillagerTrades에서 다음 레벨에 해당하는 리스트를 가져와서 offers에 추가하는 방식입니다.

 

 

 

 

 

p_35633_.put(VillagerProfession.FARMER, toIntMap(ImmutableMap.of(

음, 맵 구조로 되어있네, 

그래야 빨리찾으니깐 

 

 

 

 

 

무작위 선택의 핵심: addOffersFromItemListings

이 메소드는 부모 클래스인 AbstractVillager에 구현되어 있으며, 실제 랜덤 알고리즘은 다음과 같이 작동합니다 (의사 코드).

  1. 후보지 준비: VillagerTrades에서 가져온 배열(예: 농부 1레벨 아이템 5개)을 리스트로 바꿉니다.
  2. 무작위 섞기 (Shuffle): Collections.shuffle() 같은 기능을 사용하여 리스트의 순서를 마구 섞습니다.
  3. 개수만큼 뽑기: 섞인 리스트에서 앞에서부터 2개만 꺼내서 주민의 거래 목록(offers)에 저장합니다.

 

 

 

 

 

 

 

 

첫 번째 패키지(archer)의 AI는 잘 작동하는데, 두 번째 패키지(archer_entity)의 ArcherEntity 클래스를 사용하는 버전에서 유독 버벅이는 이유는 크게 두 가지 결정적인 차이 때문입니다.

결론부터 말씀드리면, Villager 클래스를 직접 상속받은 엔티티에 GoalSelector를 섞어 쓰면서 발생하는 "AI 충돌" 때문입니다.


1. 버벅임의 핵심 원인

① 주민의 Brain 시스템 vs GoalSelector의 충돌

마인크래프트 1.14 이후의 Villager는 일반적인 몹(좀비, 스켈레톤 등)과 달리 **Brain (Behavior 패키지)**이라는 아주 복잡한 시스템을 사용합니다.

  • 첫 번째 버전(archer 패키지)은 아마도 기존 주민에게 기능을 덧씌운 형태거나 특수한 방식으로 작동할 수 있지만,
  • 두 번째 버전(archer_entity)은 Villager를 상속받았습니다. 주민은 근처에 몬스터가 있으면 "Panic(공포)" 상태가 되어 도망가려는 내부 Brain 로직이 강제로 작동합니다.
  • 당신이 추가한 VillagerSkirmishGoal은 "조준하고 쏴라"라고 명령하는데, 주민의 Brain은 "무서우니 도망가라"고 명령합니다. 이 두 명령이 매 틱마다 싸우면서 **엔티티가 덜덜 떨리거나 이동이 끊기는 '버벅임'**이 발생하는 것입니다.

 findNewTarget()의 좀비 고정 문제

VillagerSkirmishGoal 클래스의 코드를 보면 다음과 같은 부분이 있습니다:

private Zombie findNewTarget() {
    return this.villager.level.getNearestEntity(Zombie.class, ...);
}
  • ArcherEntity NearestAttackableTargetGoal을 통해 모든 Monster(크리퍼, 스켈레톤 등)를 타겟으로 잡습니다.
  • 그런데 실제 공격 로직인 SkirmishGoal은 내부적으로 **오직 좀비(Zombie.class)**만 찾도록 설계되어 있습니다.
  • 만약 크리퍼가 타겟이 되면, 엔티티는 타겟을 잡았지만(TargetSelector), 정작 AI 로직(GoalSelector)은 "좀비가 없네?" 하고 작동을 멈췄다 켰다를 반복(Start-Stop 루프)하며 버벅이게 됩니다.

 

 

 

 

 

 

 

 

 

왜 첫 번째는 좀비를 잘 쏘나요?

첫 번째 버전은 기존 주민 시스템(Fletcher 직업 로직 등)에 당신의 AI를 주입하는 방식이라 충돌이 덜하거나, 혹은 기존 주민 AI가 이미 안정된 상태에서 추가 로직이 돌아가기 때문입니다. 반면 두 번째는 아예 **새로운 종(Species)**을 정의하면서 주민의 무거운 시스템을 통째로 들고 왔는데, 그 시스템을 제어(Clear)하지 못해서 발생하는 문제입니다.


해결하려면? (두 번째 버전을 고치는 방법)

두 번째 버전(ArcherEntity)을 포기하지 않고 버벅임을 없애려면, 주민의 무거운 AI(Brain)를 사용하지 않는 클래스를 상속받아야 합니다.

방법 A: AbstractVillager 상속 (추천)
주민의 외형과 소리, 거래 기능의 기본은 갖추고 있지만 주민 특유의 복잡한 "생각(Brain)" 시스템은 빠져 있는 클래스입니다.

public class ArcherEntity extends AbstractVillager { // Villager 대신 AbstractVillager
    public ArcherEntity(EntityType<? extends AbstractVillager> type, Level level) {
        super(type, level);
    }
    // ... 나머지 구현
}

방법 B: PathfinderMob 상속 (가장 깔끔함)
주민의 특징이 전혀 필요 없고 오직 "활 쏘는 전사"를 만들고 싶다면 주민 시스템을 아예 버리고 PathfinderMob을 상속받는 것이 가장 부드럽습니다. (렌더러만 주민 모델을 쓰면 됩니다.)

요약

  • 첫 번째 버전: 기존 주민을 "재활용"함 -> 비교적 부드러움.
  • 두 번째 버전: 주민을 상속해서 "새로 만듦" -> 주민의 기본 AI(Brain)와 직접 만든 AI가 충돌함 -> 버벅임 발생.

두 번째 버전을 제대로 쓰시려면 반드시 Villager 상속을 AbstractVillager PathfinderMob으로 바꾸셔야 합니다.

 

 

 

 

기존 궁수 작업대 주민을 바꾸지 않기 위해서 스폰알로 생성할수있는 궁수 만들엇는데 충돌이 일어나네 

 

 

 

 

 

 

이렇게 해두면, 

굳이 기존 파일들을 옮겨둘 필요가 없으니깐, 

편하긴하네 

 

 

 

 

수정필요파일들

③ VillagerSkirmishGoal.java (AI 개선)

② ArcherEvents.java & ArcherRenderer.java (렌더러 수정)

① ArcherEntity.java (가장 많이 바뀜)

 

 

 

 첫 번째 버전에서는 "이 주민이 화살 제조인인가?"를 확인해야 했지만, 이제는 ArcherEntity 클래스 자체가 궁수이므로 복잡한 직업 확인 절차 없이 바로 정보를 띄우면 됩니다.

 

 

 

 원인 분석: 왜 투명하게 보이나요?

스크린샷을 보면 이름표와 체력바는 정확한 위치에 떠 있고, 바닥에 아주 작은 갈색 점(화살 제조인의 앞치마 부분)만 보입니다.

  • 이유: 마인크래프트의 fletcher.png 텍스처는 몸 전체가 들어있는 텍스처가 아니라, 옷만 들어있는 투명한 오버레이 텍스처입니다.
  • 바닐라 주민은 몸(바이옴 텍스처) + 옷(직업 텍스처)을 겹쳐서 그리는데, 현재 코드에서는 투명한 '옷'만 입히고 '몸'을 그리지 않아서 투명인간처럼 보이는 것입니다.

이를 해결하기 위해 기본 몸체(Plains 타입) 위에 화살 제조인 옷을 입히는 커스텀 렌더러로 수정해야 합니다.

 

 

 

 

 

옷은 있는데 얼굴이 안나옴 

 

 

 

  1. 얼굴 문제 해결: 기존에는 옷만 있는 투명한 레이어를 몸체로 써서 얼굴이 안 나왔습니다. 이제 얼굴이 포함된 기본 피부(villager.png)를 먼저 그리고 그 위에 옷을 입히도록 수정했습니다.

 

 

 

 

근데 .png는 겹쳐서는 안나오는건가 주민 얼굴은 있지만 

기본 속옷만 입고잇는것같은데 

또한 손에 화살을 안들고있음

 

 

 

 

 

화살 거꾸로 들고있는 문제 

사소하니깐 일단 넘어가고 

 

 

[  ]

좀비봐도 도망치지않는 문제 

<아마 villager상속 끊어서 

 

 

 왜 도망치지 않는가? (AI 분석)

주민의 AI는 GoalSelector가 아니라 Brain 시스템을 사용합니다.

// Villager.java의 핵심 로직
p_35425_.addActivity(Activity.PANIC, VillagerGoalPackages.getPanicPackage(villagerprofession, 0.5F));
  • 원인: 우리가 AbstractVillager로 갈아탄 이유는 이 Brain 시스템이 너무 무거워서 커스텀 Goal과 충돌(버벅임)했기 때문입니다. 하지만 상속을 바꾸면서 주민의 기본 탑재 기능인 **"위협 시 도망가는 행동(Panic Behavior)"**도 함께 사라졌습니다.
  • A 알고리즘:* 마인크래프트의 Navigation 객체(this.villager.getNavigation())는 이미 내부적으로 A* 알고리즘을 사용하여 경로를 계산합니다. 따라서 우리가 A*를 직접 짤 필요는 없지만, **"어느 방향으로 도망가야 안전한지"**를 계산하는 로직은 Goal에 추가해야 합니다.

 

 

 

 

 

DefaultRandomPos Brain 시스템과는 전혀 별개의 독립적인 유틸리티 클래스입니다. 따라서 Brain을 상속받지 않아도, 심지어 AbstractVillager가 아니더라도 PathfinderMob을 상속받은 모든 엔티티에서 언제든지 호출해서 사용할 수 있습니다.

1. 왜 가져올 수 있나요? (이론적 이유)

  • 상속 구조: ArcherEntity -> AbstractVillager -> AgeableMob -> PathfinderMob.
  • DefaultRandomPos.getPosAway 메서드는 첫 번째 인자로 PathfinderMob 타입을 받습니다. 우리 궁수는 PathfinderMob의 자식이기 때문에 아무 문제 없이 들어갈 수 있습니다.
  • 작동 방식: 이 메서드는 Brain 메모리를 뒤지는 것이 아니라, 현재 엔티티의 **Navigation (길찾기 장치)**을 사용하여 주변 블록 중 "지정된 좌표(적)로부터 가장 멀면서도 발을 디딜 수 있는 곳"을 수학적으로 계산해주는 유틸리티일 뿐입니다.

 

 

 

 

[  ]

도망거리는 나중에 한번 고치던지 하고, 대각선에 몰리면 좀 멍청해지는기분, 

 

 

 

 

 

 

[  ]

아군 타격할때 데미지는 안들어오게 햇지만, 

그게 ally implement하고 잇으면, 공격도 안하게 해야하는데 

if문 추가해야하나

아니면 boolean변수 추가 ?

 

 

implements IVillagerAlly만 쓴다고 해서 자동으로 공격을 멈추지는 않습니다.

IVillagerAlly는 일종의 **"이름표"**입니다. 궁수 AI가 공격하기 전에 상대방의 이름표를 확인하고 "아, 우리 편이네?"라고 판단하는 if문 로직이 반드시 필요합니다.

 

 

 

이젠 아군이 타격해도 공격안하고

데미지도 안들어옴 

 

 

 

 

 

 

C:\dev5\src\main\java\changmin\myMod\feature\archer_entity\VillagerSkirmishGoal.java
경고:(12, 1) 사용되지 않는 import 문
경고:(13, 1) 사용되지 않는 import 문
경고:(135, 29) 지역 변수 'coolDown'이(가) 중복됩니다
C:\dev5\src\main\java\changmin\myMod\feature\archer_entity\ArcherEntity.java
경고:(21, 33) private 필드 'LOGGER'이(가) 사용되지 않습니다
경고:(47, 48) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(51, 107) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(51, 156) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
C:\dev5\src\main\java\changmin\myMod\feature\archer_entity\ArcherEntityRenderer.java
경고:(38, 29) 어노테이션이 없는 메서드가 @MethodsReturnNonnullByDefault(으)로 어노테이션이 추가된 메서드를 재정의합니다
경고:(38, 61) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(54, 38) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(54, 67) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(54, 105) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(69, 38) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
경고:(69, 67) 어노테이션이 없는 매개변수가 @ParametersAreNonnullByDefault 매개변수를 재정의합니다
C:\dev5\src\main\java\changmin\myMod\feature\archer_entity\ArcherEvents.java
경고:(81, 99) 람다 구문은 람다 표현식으로 바꿀수 있습니다

 

 

경고 내용 요약 (왜 떴을까?)

  1. 사용되지 않는 import / LOGGER:
    • 코드를 짜다가 나중에 안 쓰게 된 줄들입니다. 지워도 되고 안 지워도 실행엔 지장 없습니다. (Ctrl + Alt + O를 누르면 한 번에 정리됩니다.)
  2. Duplicate local variable 'coolDown' (VillagerSkirmishGoal):
    • coolDown이라는 변수 이름이 같은 메서드 안에서 두 번 선언되었다는 뜻입니다. (예: int coolDown = ...을 위아래로 두 번 썼을 때) 큰 문제는 아니지만 변수 이름을 하나 바꾸면 깔끔해집니다.
  3. @ParametersAreNonnullByDefault 관련 (ArcherEntity, Renderer):
    • 이건 마인크래프트 포지(Forge) 개발 환경 특유의 경고입니다. 부모 클래스는 "매개변수에 null이 들어오면 안 된다"고 되어 있는데, 상속받은 코드에는 @NotNull 같은 표시가 없어서 뜨는 것입니다. 모딩할 때 아주 흔하게 발생하는 경고이며 무시해도 100% 안전합니다.
  4. 람다 구문 (ArcherEvents):
    • player -> { ... } 코드를 더 짧게 쓸 수 있다는 제안입니다. 역시 무시해도 됩니다.

 

 

 

 

 

 

 

 

1. 구현 방식의 근본적 차이 (개조 vs 신규 생성)

  • 제작대 방식 (첫 번째): 기존에 존재하는 바닐라 마인크래프트의 **'주민(Villager)'**에게 새로운 기능을 '덧붙이는' 방식입니다. 주민이 '화살 제조인' 직업을 가지면 추가적인 AI와 데이터를 주입합니다.
  • 스폰알 방식 (두 번째): 바닐라 주민과는 완전히 분리된 **'궁수 엔티티(ArcherEntity)'**라는 새로운 몹을 만드는 방식입니다. 외형은 주민과 닮았지만, 게임 코드상으로는 좀비나 스켈레톤처럼 독립적인 고유 ID를 가진 엔티티입니다.

2. 직업 및 시스템 의존성

  • 제작대 방식: 마인크래프트의 **직업 시스템(Profession)**에 의존합니다. 주민이 제작대를 통해 화살 제조인이 되어야만 궁수 역할을 수행하며, 직업을 잃으면 궁수 능력도 사라집니다. 마을 시스템의 일부로 작동합니다.
  • 스폰알 방식: 직업 시스템과 무관합니다. 태어날 때부터 궁수로 설계되었으며, 별도의 제작대가 필요하지 않습니다. 마을의 경제 시스템(거래 등)보다는 **'전투 유닛'**으로서의 정체성이 더 강합니다.

3. 렌더링 및 외형 처리

  • 제작대 방식: 바닐라 주민 모델을 그대로 사용하면서, 그 위에 **오버레이(HUD)**를 띄워 레벨과 체력을 표시하는 데 집중합니다. 주민의 기본 모델(팔을 모으고 있는 모습)을 수정하기보다는 정보 전달에 치중합니다.
  • 스폰알 방식: 모델링과 렌더링을 훨씬 상세하게 제어합니다. 주민 모델을 기본으로 하되, 의상을 레이어별로(피부-마을옷-직업옷) 직접 입히고, 특히 주민이 팔을 모으고 있음에도 불구하고 가슴 쪽에 활이 보이도록 별도의 렌더링 레이어를 추가하여 시각적인 완성도를 높였습니다.

4. 대상 인식 및 전투 AI (아군 판정)

  • 제작대 방식: 비교적 단순한 타겟팅을 사용합니다. 주로 몬스터를 공격하며, 화살을 쏠 때 같은 주민을 맞추지 않도록 하는 '오사 방지' 로직이 포함되어 있습니다.
  • 스폰알 방식: 훨씬 정교한 아군 관리 시스템이 통합되어 있습니다. IVillagerAlly라는 인터페이스를 사용하여 모드 내의 다른 아군 시스템과 연동되며, 단순히 몬스터를 쏘는 것을 넘어 '아군을 공격하지 않음'과 '아군이 맞았을 때 함께 반격함' 같은 복잡한 상호작용이 가능합니다.

5. 게임 플레이에서의 활용도

  • 제작대 방식: 서바이벌 모드 지향적입니다. 플레이어가 마을을 찾고, 주민에게 직업을 주어 마을을 방어하게 만드는 '성장과 운영'의 재미를 줍니다.
  • 스폰알 방식: 샌드박스 또는 커스텀 맵 지향적입니다. 플레이어가 원하는 곳에 즉시 군대를 배치하거나, 특정 상황에서 아군 NPC를 소환하는 용도에 최적화되어 있습니다. 또한 독립적인 엔티티이므로 바닐라 주민의 행동 패턴(밤에 잠자기, 회당 모이기 등)에 얽매이지 않고 전투에만 집중시킬 수 있습니다.

요약 비교

구분 제작대 방식 (Fletcher Extension) 스폰알 방식 (Custom Entity)
정체성 직업을 가진 일반 주민 궁수라는 이름의 신규 몹
활성화 조건 화살 제조인 직업 획득 시 스폰 시 즉시
외형 바닐라 주민 + 정보 UI 전용 의상 레이어 + 활 렌더링
데이터 저장 주민의 추가 능력(Capability)으로 저장 엔티티 자체 데이터로 저장
전투 로직 일반적인 몬스터 사냥 아군 시스템과 연동된 조직적 전투
적합한 용도 마을 방어 및 주민 성장 콘텐츠 용병, 아군 유닛 소환 및 전투 콘텐츠

결론적으로, 기존 마을 시스템을 풍성하게 만들고 싶다면 제작대 방식이 적합하고, 플레이어를 돕는 강력한 전투 용병을 추가하고 싶다면 스폰알 방식이 더 효과적입니다.