
📘 목차
1. 수중 전투 메커니즘 해결
- 투사체 수중 저항 분석: AbstractArrow의 감속 로직 및 0.2F 속도의 한계
- 물리 상수 오버라이드: getWaterInertia()를 1.0F로 수정하여 속도 유지
- AI 경로 탐색 보정: BlockPathTypes.WATER 비용 수정을 통한 수중 이동성 확보
2. 엔티티 아키텍처 및 버전 관리
- 패키지 기반 독립 개발: 상속(Inheritance) 대신 복사 및 패키지 분리 전략의 이유
- 레지스트리 식별자 관리: V3와 V4의 충돌을 방지하는 Registry ID 설정법
- 클래스 참조 무결성: 패키지 복사 시 발생하기 쉬운 Import 오류 해결법
3. 지능형 전투 사이클 (Skirmish AI)
- 5초 순환 로직: '도망(회피)'과 '시즈모드(포격)'의 상태 전환 메커니즘
- 효율적 타이머 관리: 비전투 시 자원 낭비를 방지하는 타겟팅 기반 시계 작동 원리
- 선제 타격 시스템 복구: FOLLOW_RANGE 속성 강제 부여 및 탐색 거리 확장
4. 사격 통제 및 탄도학 (Ballistics)
- 선형 예측 사격(Leading Shot): 대상의 현재 속도와 탄속을 이용한 미래 위치 추적
- 예측 사격의 변수: 플레이어의 방향 전환(지그재그)에 따른 심리전 요소
- 대(對) 닌자 대응 전략: 탄환을 그물처럼 뿌리는 '분산 사격' 및 '혼합 사격' 이론
5. 닌자 좀비: 지능형 회피 로직
- 위험 감지 센서: 접근하는 투사체 궤적 실시간 모니터링
- 랜덤 회피 패턴: 주사위 기반의 옆면 대시(Side-step) 및 점프 회피
- 동적 무빙: 고정된 예측 사격을 무력화하는 가속도 변화 로직
6. 타겟팅 정교화 및 그룹 AI
- 집단 반격 시스템: setAlertOthers()를 이용한 아군 마법사들의 동시 반격 원리
- 타겟 판정 로직 통합: V3의 ZombieEnemyHandler와 V4 인터페이스 방식 비교 및 수정
- 시야 확보(canSee): 지형지물 및 눈 높이에 따른 타겟팅 성공률 분석
7. AI 성능 최적화 (Optimization)
- 탐색 연산 절감: 교전 중 중복 스캔을 차단하는 canUse() 오버라이드 기법
- 익명 클래스 활용: 기존 NearestAttackableTargetGoal을 즉석에서 커스터마이징하기
- 메서드 참조(::) 적용: Java 8 문법을 통한 코드 가독성 향상 및 람다 최적화
- GoalSelector 작동 원리: 부모 클래스의 super.canUse() 호출 시점과 연산 흐름
[업데이트 사항]
- 성능 부분: 단순히 '성능 최적화'로 묶지 않고, canUse와 super의 관계를 상세히 나누었습니다.
- 집단 반격: 한 대 맞으면 다 같이 싸우는 setAlertOthers 내용을 추가했습니다.
- 타겟팅 오류: V3는 되고 V4는 안 됐던 isEnemyZombie 핸들러 문제를 명시했습니다.
- 개발 전략: 클래스 이름은 같지만 패키지가 다른 구조에서 오는 주의점들을 포함했습니다.
[ ]
체력표시, 다이아좀비
물속 투사체
:공격 못하고잇는데 [ ]확인해볼것
[ pass ]
일단 수중 공격할일은 없으니깐 넘어갈까나
[ ]
현재 isally부분 정리 좀 해야겟는데,
필요없는 클래스 삭제하고
isally부분 비슷한게 많아서
[ ]
뭔소리지 이게
든 클래스 이름과 레지스트리 ID를 V4로 변경하여 V3와 충돌 없이 독립적으로 작동하도록 구성했습니다.
[ ]
닌자 좀비
<랜덤이용해서 발사체 잘피하도록 할수있나(적군)
예측샷
계속 한방향으로만 움직인다고 가정을 한건가
[ ]벌관련, 근데 벌은 플레이어말고는 공격안하나,
[ ]혹시 사용자 정의벌도 가능한가
[ ]순간이동하는 법사
(일단 ver3은 잘작동한다는거 알앗으니깐 거기에 기반하여 향후 법사 코드를)
[ ]체력도 표시해두는게
[ ]
시즈모드처럼, 계속 도망다니다가,
5초 쿨타임지나면, 그자리에서 대상을 향해서, 공격발사
계속하는
[ ok ]
법사 선제타격
:일단 선제 공격은 하는것같고
WizardEntityV3가 물속에서 공격을 제대로 하지 못하거나, 투사체가 발사되자마자 멈추는 이유는 크게 두 가지입니다. 하나는 AbstractArrow의 기본 수중 물리 법칙 때문이고, 다른 하나는 AI의 경로 찾기(Navigation) 문제입니다.
질문하신 "어느 부분이 투사체 관련 코드이며 왜 그런지"에 대해 분석하고 해결책을 드립니다.
1. 원인 분석: 왜 물속에서 공격이 안 되는가?
원인 A: 투사체의 속도와 수중 저항 (가장 유력)
- 관련 코드: WizardSkirmishGoalV3.java 의 performShot 메서드
projectile.shoot(dx, dy, dz, 0.2F, 1.0F); // 속도가 0.2F로 매우 낮음 - 이유: MagicProjectileV3가 상속받고 있는 AbstractArrow는 마인크래프트의 기본 화살 로직을 따릅니다. 화살은 **물에 들어가는 순간 속도가 급격히 줄어드는 저항(Drag)**을 받습니다.
- 현재 설정된 속도 0.2F는 화살(기본 3.0F)에 비해 이미 매우 느린 상태입니다. 물속에서는 이 속도가 0.6배(기본값)로 계속 줄어들기 때문에, 발사되자마자 제자리에 멈추거나 아주 짧은 거리만 이동하고 소멸하게 됩니다.
원인 B: 엔티티의 AI 탐색 (Navigation) 문제
- 관련 코드: WizardEntityV3.java 및 WizardSkirmishGoalV3.java
- 이유: WizardEntityV3는 Villager의 속성을 가지고 있어 기본적으로 지상 네비게이터를 사용합니다. 물속에 있으면 "길이 막혔다"고 판단하거나, 허우적거리느라 canSee(target) 조건(시야 확보)을 만족시키지 못해 사격 로직 자체가 실행되지 않을 수 있습니다.
2. 해결 방법
해결 1: 투사체의 수중 저항 없애기 (MagicProjectileV3 수정)
AbstractArrow에는 물속에서의 감속 비율을 결정하는 메서드가 있습니다. 이를 오버라이드하여 물속에서도 느려지지 않게 설정해야 합니다.
[MagicProjectileV3.java 수정]
@Override
protected float getWaterInertia() {
// 기본값은 0.6f (40% 감속). 1.0f로 설정하면 물속에서도 공기 중과 똑같은 속도를 유지합니다.
return 1.0F;
}
해결 2: 발사 속도 상향 (WizardSkirmishGoalV3 수정)
0.2F는 투사체치고 너무 느립니다. 최소 1.0F 이상으로 높이는 것이 좋습니다.
[WizardSkirmishGoalV3.java 수정]
private void performShot(LivingEntity target) {
MagicProjectileV3 projectile = new MagicProjectileV3(this.wizard.level, this.wizard);
projectile.setNoGravity(true);
// Y축 보정: 대상의 발밑이 아니라 몸통 중앙을 향하게 함
double dx = target.getX() - this.wizard.getX();
double dy = target.getY(0.5D) - projectile.getY(); // 0.33D보다 약간 높임
double dz = target.getZ() - this.wizard.getZ();
// 속도를 0.2F에서 1.5F 정도로 상향 (기본 화살은 3.0F)
projectile.shoot(dx, dy, dz, 1.5F, 1.0F);
this.wizard.level.addFreshEntity(projectile);
this.wizard.playSound(SoundEvents.ILLUSIONER_CAST_SPELL, 1.0F, 1.2F);
}
해결 3: 물속에서의 시야 확보 및 타겟팅 확인
만약 법사가 물에 잠겨 있을 때 공격을 아예 안 한다면, canSee 로직이 물 블록에 가려지는지 확인해야 합니다. 하지만 위 1, 2번만 수정해도 대부분 해결됩니다.
추가로, 법사가 물속에서 더 잘 움직이게 하려면 WizardEntityV3 생성자에서 수영 경로 찾기를 도와주는 설정을 추가할 수 있습니다.
[WizardEntityV3.java 수정]
public WizardEntityV3(EntityType<? extends AbstractVillager> type, Level level) {
super(type, level);
// 물속에서도 경로를 찾을 수 있도록 설정
this.setPathfindingMalus(BlockPathTypes.WATER, 0.0F);
}
개발 전략: 상속인가, 복사인가?
결론부터 말씀드리면: "똑같이 복사해서 새로운 클래스(혹은 패키지)로 만드는 것"을 추천합니다.
이유는 다음과 같습니다.
1) 왜 상속(Inheritance)을 비추천하나?
- 지금은 V3 코드가 깔끔하지만, 상속을 시작하면 부모 클래스(V3)의 코드를 수정할 때 자식 클래스들이 원치 않게 같이 변하거나 에러가 날 확률이 높습니다.
- 특히 마인크래프트 엔티티 코드는 Goal이나 Register 부분이 얽혀 있어 상속 구조가 복잡해지면 디버깅이 매우 힘듭니다.
2) 왜 "프로토타입 복사"가 더 좋나?
- 완전한 독립성: WizardEntityV4 혹은 FireWizard 같은 이름으로 새로 만들면, V3가 망가질 걱정 없이 마음껏 실험할 수 있습니다.
- 비교 분석 가능: V3와 V4를 나란히 소환해 놓고 어떤 AI가 더 잘 싸우는지 실시간으로 비교 테스트하기 좋습니다.
- 패키지 분리: wizard_v4_fire, wizard_v5_ice 식으로 패키지를 나누면 나중에 리소스(텍스처, 모델) 관리도 훨씬 편합니다.
네, 자바(Java) 언어의 규칙상 패키지가 다르면 클래스 이름이 같아도 아무런 문제가 없습니다.
예를 들어:
- changmin.myMod.wizard_v3.WizardEntity
- changmin.myMod.wizard_v4.WizardEntity
이렇게 두 클래스가 공존하는 것은 문법적으로 완전히 가능합니다. 하지만 마인크래프트 모딩(Forge/Fabric) 환경에서는 몇 가지 매우 중요한 주의점이 있습니다. 단순히 복사만 했다가는 빨간 줄(에러) 폭탄을 맞거나 게임이 켜지지 않을 수 있습니다.
1. 레지스트리 ID (Registry Name)는 무조건 달라야 합니다.
자바 클래스 이름은 같아도 되지만, 게임 엔진에 등록하는 **ID(문자열)**는 중복되면 안 됩니다.
- V3 코드: .register("wizard_v3", ...)
- V4 코드: .register("wizard_v4", ...) ← 이 부분은 반드시 수정해야 합니다.
만약 클래스 이름만 똑같이 쓰고 이 ID를 수정하지 않으면, 게임이 켜질 때 "ID 중복 등록" 에러를 내며 튕깁니다.
2. 임포트(Import) 지옥 주의
패키지만 다르고 이름이 같은 클래스를 복사했을 때 가장 많이 발생하는 실수입니다.
예를 들어 v4 패키지에 WizardEntity를 복사해 넣었는데, 정작 그 안의 registerGoals()나 Renderer 클래스가 v3 패키지의 클래스를 import하고 있는 경우가 많습니다.
- V4 Renderer는 반드시 V4 Entity를 렌더링해야 하는데, 실수로 import ...v3.WizardEntity;가 되어 있으면 V4 법사가 투명하게 보이거나 엉뚱한 모델이 나오는 버그가 생깁니다.
- IDE(인텔리제이 등)에서 클래스 이름을 검색할 때 똑같은 이름이 여러 개 뜨기 때문에, 엉뚱한 파일을 수정하는 실수를 하기 매우 쉽습니다.
3. 작업 생산성 (추천하는 방법)
클래스 이름을 똑같이 유지하고 싶다면, 다음 순서를 지키는 것이 가장 안전합니다.
- 새 패키지 생성: wizard_v4 패키지를 만듭니다.
- 통째로 복사: v3에 있는 모든 파일을 선택해서 v4로 붙여넣습니다.
- 패키지 선언 확인: 파일 최상단의 package changmin.myMod.wizard_v4;가 잘 바뀌었는지 확인합니다.
- 전체 임포트 갱신: 파일 내부에서 v3를 참조하고 있는 모든 import 문을 v4로 바꿉니다. (IDE의 'Replace in Path' 기능을 쓰면 편합니다.)
- 레지스트리 ID 수정: EntityTypeInit 등에서 "wizard_v3" 문자열을 "wizard_v4"로 바꿉니다.
결론
"이름을 똑같이 써도 되나요?" → 네, 됩니다. 패키지로 구분하면 자바는 알아듣습니다.
하지만 **"이름을 똑같이 쓰는 게 좋은가요?"**라고 물으신다면, 저는 **"비추천"**합니다. 나중에 코드가 많아지면 WizardEntity.java 탭이 5개 열려 있는데, 그중 어떤 게 내가 지금 수정하려는 불 마법사인지, 얼음 마법사인지 헷갈려서 실수를 유발할 가능성이 99%이기 때문입니다.
차라리 WizardFireEntity, WizardIceEntity 처럼 뒤에 특징을 붙여주는 것이 나중에 관리하기 훨씬 편하실 겁니다! 혹은 최소한 WizardEntityV4 처럼 버전을 붙이는 것이 디버깅 로그를 볼 때도 훨씬 명확합니다. (로그에 클래스 이름이 찍히니까요!)

주변에 적이 없을 때의 동작 원리는 마인크래프트의 Goal(AI) 시스템의 작동 방식에 답이 있습니다. 코드 없이 로직의 흐름만 설명해 드릴게요.
1. "적"이 있어야만 시계가 돌아갑니다
이 AI는 canUse()라는 체크 기능을 가지고 있습니다. 여기에는 "대상이 있고, 그 대상이 살아있는가?"라는 조건이 걸려 있습니다.
- 적이 없을 때: 이 AI(Goal) 자체가 작동을 중지합니다. 즉, 5초를 세는 타이머(stateTimer)도 그 자리에 그대로 멈춥니다.
- 적이 다시 나타나면: 멈췄던 타이머가 다시 흐르기 시작합니다.
2. 도망/공격 시간의 측정 방식
현재 로직은 "전투 중인 시간"을 기준으로 합니다.
- 법사가 적을 발견하고 전투 모드에 들어가면 그제야 "자, 이제부터 5초 센다!" 하고 시계를 누르는 방식입니다.
- 따라서 적이 없어서 평화롭게 걸어 다닐 때는 "도망 모드"나 "시즈 모드" 시간을 미리 계산해두지 않습니다. 오직 전투가 시작된 순간부터 5초 도망 -> 5초 공격의 사이클이 시작됩니다.
3. 만약 전투 중에 적이 사라진다면?
만약 5초 동안 도망가야 하는데 3초 만에 적이 죽거나 사라지면 어떻게 될까요?
- 현재 코드상으로는 타이머가 3초에서 일시정지된 것과 같습니다.
- 나중에 새로운 적이 나타나면 남은 2초를 마저 채우고 나서 시즈 모드로 들어갑니다. (즉, 전투 흐름이 끊겨도 이전에 진행하던 사이클을 기억하고 이어서 진행합니다.)
4. 요약하자면
- 도망가는 시간을 따로 측정했나?
-> 아니요. "적을 타겟팅하고 있는 상태"에서의 시간만 측정합니다. - 평화로울 때의 처리:
-> AI가 완전히 비활성화되어 타이머가 작동하지 않으므로, 아무런 계산도 하지 않고 리소스를 아낍니다.
결론: 이 마법사는 평소에는 일반 주민처럼 돌아다니다가, "누군가와 싸우기 시작한 시점부터" 정해진 5초/5초 리듬을 타기 시작하는 구조입니다. 만약 평소(전투 외)에도 시간이 흐르게 하고 싶다면 별도의 처리가 필요하겠지만, 지금처럼 전투 중에만 리듬을 타는 것이 훨씬 효율적이고 직관적입니다.
현재 코드는 **"발사하는 그 순간"**에 적이 서 있는 위치를 향해서만 쏩니다.
하지만 투사체 속도가 0.2로 매우 느리기 때문에, 적이 옆으로 조금만 걸어가도 투사체가 도착할 때쯤 적은 이미 그 자리에 없게 됩니다. 즉, **백발백중 빗나가는 "과거의 환영"**만 쫓는 공격이 됩니다.
이를 해결하기 위해 "예측 사격(리드샷, Leading the Shot)" 로직을 적용해야 합니다. 적의 현재 이동 속도를 계산해서, 투사체가 도착할 지점을 미리 예측해서 쏘는 방식입니다.
어떻게 수정해야 할까? (로직 설명)
- 거리 계산: 법사와 적 사이의 거리를 구합니다.
- 도착 시간 예측: 거리 / 투사체 속도(0.2)를 하면 투사체가 적에게 닿을 때까지 몇 초(틱)가 걸릴지 알 수 있습니다.
- 미래 위치 계산: 적의 현재 속도 * 예측 시간만큼 적의 좌표에 더해줍니다.
- 발사: 그 "미래의 좌표"를 향해 쏩니다.
예측샷은 좀 복잡할지도
계속 한방향으로만 움직인다고 가정을 한건가
예측 샷(리드샷)의 원리는 생각보다 간단합니다. 코드를 빼고 **'축구 선수가 패스하는 상황'**을 상상하면 바로 이해하실 수 있습니다.
1. 거리와 시간 계산
먼저 마법사가 적을 봅니다.
- "음, 적이 여기서 10미터 떨어져 있군."
- "내 마법 탄환은 느려서 1초에 2미터밖에 못 가."
- "그럼 내가 지금 쏘면 적한테 도착할 때까지 총 5초가 걸리겠네?"
2. 적의 움직임 관찰
마법사는 적이 지금 어느 방향으로 뛰고 있는지 봅니다.
- "오호, 적이 왼쪽으로 1초에 1미터씩 뛰고 있네?"
3. 미래 위치 상상 (핵심)
마법사는 머릿속으로 계산기를 두드립니다.
- "탄환이 도착하는 데 5초가 걸리는데, 적은 그동안 계속 왼쪽으로 뛸 거야."
- "1초에 1미터씩 5초 동안 뛰면, 5초 뒤에 적은 지금보다 왼쪽으로 5미터 더 가 있겠군!"
4. '허공'에 발사
이제 마법사는 적이 지금 서 있는 곳을 조준하지 않습니다.
- 방금 계산한 **'5초 뒤에 적이 가 있을 예상 지점(허공)'**을 조준해서 쏩니다.
요약하자면 이렇습니다:
- 도착 시간 계산: "내 마법이 적한테 닿으려면 얼마나 걸릴까?" (거리 ÷ 탄환 속도)
- 미래 위치 예측: "그 시간 동안 적은 어디까지 이동할까?" (적의 이동 속도 × 도착 시간)
- 조준: "적이 지금 있는 곳이 아니라, 도착할 그곳에 대고 쏘자!"
결과:
마법 탄환은 허공을 향해 천천히 날아가고, 적은 자기 갈 길을 가다가 마치 자석에 이끌리듯 그 탄환에 스스로 부딪히게 됩니다.
이게 바로 예측 샷의 기본 원리입니다! 코드는 단지 이 계산을 컴퓨터에게 시키는 것뿐입니다.
플레이어는 로직이 고정된 몹과 달리 방향을 휙휙 바꾸는 존재라는 점이 예측 샷의 가장 큰 변수입니다. 마법사가 "적이 왼쪽으로 간다"고 판단하는 과정과, 플레이어가 이를 어떻게 역이용할 수 있는지 자세히 설명해 드릴게요.
1. "적이 왼쪽으로 간다"는 걸 어떻게 알까요? (찰나의 스냅샷)
마법사는 플레이어의 미래를 읽는 초능력이 있는 게 아니라, **'지금 이 순간'**의 물리적인 상태를 봅니다.
- 델타 무브먼트(Delta Movement): 마인크래프트 엔진은 매 0.05초(1틱)마다 플레이어가 X축으로 얼마나, Z축으로 얼마나 이동했는지 계산합니다.
- 스냅샷: 마법사가 마법을 발사하는 딱 그 0.05초의 순간에 플레이어의 발을 봅니다. "방금 0.05초 동안 플레이어의 몸이 왼쪽으로 0.2블록 움직였네?"라고 체크하는 거죠.
- 직선 가정: 마법사는 "이 인간은 방금 움직인 그 속도와 그 방향 그대로 5초 동안 쭉 가겠지?"라고 아주 단순하게(직선적으로) 가정하고 쏩니다.
2. 플레이어가 방향을 휙 바꾼다면? (예측 샷의 약점)
여기서 질문하신 것처럼 플레이어가 방향을 바꾸면 재미있는 현상이 생깁니다.
- 발사 순간: 플레이어가 왼쪽으로 뛰고 있습니다. 법사는 플레이어의 미래 왼쪽 지점을 조준해 탄환을 쏩니다.
- 발사 직후: 탄환이 날아가는 도중, 플레이어가 갑자기 멈추거나 오른쪽으로 휙 꺾습니다.
- 결과: 탄환은 이미 발사되었고, 법사의 손을 떠난 탄환은 유도탄이 아니기 때문에 **원래 조준했던 '왼쪽 허공'**을 향해 계속 날아갑니다. 결국 플레이어는 탄환을 피하게 됩니다.
3. "그럼 예측 샷이 쓸모없는 거 아닌가요?"
아니요, 오히려 게임이 훨씬 심리전 같아지고 재미있어집니다!
- 그냥 쐈을 때: 플레이어가 한 방향으로 계속 걷기만 해도 다 빗나갑니다. (바보 같은 AI)
- 예측해서 쐈을 때: 플레이어가 한 방향으로 계속 걸으면 무조건 맞습니다. 플레이어는 탄환을 피하기 위해 '지그재그로 움직이거나' '갑자기 멈추는' 등의 능동적인 컨트롤을 강요받게 됩니다.
4. 시즈 모드와 결합하면 생기는 효과
특히 지금 만드시는 **'시즈 모드(5초간 20발 연사)'**와 이 예측 샷이 만나면 아주 무서워집니다.
- 법사가 0.25초마다 1발씩 쏩니다.
- 플레이어가 왼쪽으로 뛰면, 법사는 플레이어의 이동 경로를 따라가며 탄환을 도로 위에 깔아버립니다. (마치 기관총으로 이동 경로를 긁는 느낌)
- 플레이어는 탄환이 내 앞길을 막고 있으니, 계속 가던 길을 갈 수가 없고 반대쪽으로 꺾거나 멈춰야만 합니다.
요약하자면:
마법사는 **"지금 너의 속도와 방향이 영원할 것"**이라고 믿고 그곳을 쏘는 정직한 예측을 합니다. 플레이어는 그 마법사의 '정직함'을 이용해서 방향을 휙 바꿔 피할 수 있게 되고, 이것이 마인크래프트 전투에서 **"컨트롤로 피하는 재미"**를 만들어냅니다.
만약 플레이어가 방향을 바꿔도 끝까지 쫓아가게 하고 싶다면 그건 '예측 샷'이 아니라 '유도탄(Homing)' 로직을 써야 합니다! (하지만 지금의 예측 샷 방식이 훨씬 더 게임답고 공정한 난이도를 제공합니다.)
네, 가능합니다! **예측샷(Wizard)**과 **피하기(Ninja Zombie)**의 대결은 프로그래밍적으로 매우 흥미로운 주제입니다. 닌자 좀비가 예측샷을 역이용하거나 랜덤성을 이용해 투사체를 피하게 만드는 원리를 설명해 드릴게요.
1. 닌자 좀비: 투사체를 어떻게 피할까? (랜덤 회피)
닌자 좀비에게는 **"투사체 감지 센서"**와 **"주사위 굴리기(랜덤)"**를 넣어주면 됩니다.
- 감지 센서: 좀비 주변 일정 범위(예: 8칸) 안에 날아오는 투사체가 있는지 매 순간 체크합니다.
- 회피 기동 (Side-step): 투사체가 가까이 오면 좀비는 가던 길을 멈추고 현재 방향의 수직(옆) 방향으로 순간적으로 움직입니다.
- 랜덤성 추가: 이때 "무조건 왼쪽"이 아니라 주사위를 굴립니다.
- 1이 나오면: 왼쪽으로 대시(Dash)
- 2가 나오면: 오른쪽으로 대시
- 3이 나오면: 제자리에서 점프 (화살이 발 밑으로 지나가게)
- 4가 나오면: 그냥 무시하고 돌진 (플레이어를 속이기 위함)
이런 랜덤성이 들어가면 플레이어나 법사가 아무리 예측해서 쏴도 "쏠 때는 거기 있었는데, 날아가는 도중에 주사위를 굴려서 옆으로 가버리는" 닌자 좀비를 맞추기 매우 힘들어집니다.
2. 예측샷은 "한 방향으로만 간다"고 가정하는가?
네, 맞습니다. 기본적인 예측샷 로직은 **'선형 예측(Linear Prediction)'**이라고 부르며, 다음과 같은 가정을 전제로 합니다.
- 속도가 일정하다: "너는 지금 초속 1미터로 움직이고 있구나."
- 방향이 일정하다: "너는 지금 정확히 서쪽으로 가고 있구나."
- 가속도가 없다: "너는 갑자기 멈추거나 급발진하지 않겠구나."
즉, **"너는 지금 하던 짓을 5초 뒤에도 똑같이 하고 있을 거야"**라고 믿고 쏘는 것입니다.
3. 닌자 좀비가 예측샷을 바보로 만드는 방법
예측샷의 이 "정직한 가정"이 바로 닌자 좀비에게는 최고의 기회가 됩니다.
- 지그재그 무빙 (Zig-zag): 닌자 좀비가 일부러 왼쪽으로 뛰다가 법사가 쏘는 순간 오른쪽으로 꺾으면, 법사의 예측탄은 좀비가 원래 가려던 왼쪽 허공에 박힙니다.
- 속도 조절: 천천히 걷다가 투사체가 날아오면 순간적으로 속도 x 3으로 대시를 하면, 예측 지점보다 훨씬 더 멀리 가버려서 피할 수 있습니다.
4. 닌자 좀비 AI 로직 (요약)
만약 닌자 좀비를 만드신다면 다음과 같은 로직이 들어갈 것입니다.
- 평상시: 플레이어를 향해 지그재그로 뛰어옵니다. (이것만으로도 예측샷은 빗나가기 시작합니다.)
- 위기 감지: MagicProjectile이 5칸 이내로 접근하면 Random.nextInt(3)을 실행합니다.
- 기술 발동:
- Case 0 (회피): 투사체 궤적의 수직 방향으로 강한 힘(Velocity)을 주어 확 피합니다.
- Case 1 (은신/순보): 아주 짧은 거리(2~3칸)를 텔레포트하여 투사체를 통과해버립니다.
- Case 2 (반격): 아주 낮은 확률로 투사체를 쳐내거나(Deflect) 공중에서 회전하며 피합니다.
결론
- 법사 V4: "너의 현재 속도와 방향을 보니 3초 뒤엔 여기 있겠군! (직선 예측)"
- 닌자 좀비: "난 네가 쏘는 순간 주사위를 굴려서 다른 곳으로 갈 건데? (랜덤 회피)"
이렇게 두 몹이 붙으면 법사의 포격망을 뚫고 요리조리 피하며 접근하는 닌자 좀비라는 아주 화려한 전투 장면이 연출됩니다. 이 대결 구도는 게임의 긴장감을 엄청나게 높여줄 것입니다!
닌자 좀비처럼 예측을 역이용해 '휙휙' 피하는 적을 상대하려면, 쏘는 사람(법사)도 단순한 계산을 넘어선 새로운 전략이 필요합니다.
단순히 한 점을 노리는 것이 아니라, **'확률의 그물'**을 짜는 방식으로 대응할 수 있습니다. 닌자 좀비에 대항할 수 있는 3가지 진화된 사격 방식을 제안해 드립니다.
1. 분산 예측 사격 (The Scatter/Net Shot) - "그물을 짜라"
가장 효과적인 방법입니다. 시즈 모드에서 20발을 쏠 때, 모든 탄환을 한 점으로 쏘지 말고 예측 지점 주변에 살포하는 것입니다.
- 원리: 법사가 예측 지점을 계산한 뒤, 그 지점을 중심으로 좌우 2~3칸 범위에 랜덤하게 쏩니다.
- 효과: 닌자 좀비가 투사체를 보고 왼쪽으로 피했는데, 마침 법사가 '왼쪽으로 피할 것까지 예상해서' 그쪽에 뿌려둔 탄환에 걸리게 됩니다.
- 비유: 과녁 하나만 노리는 소총이 아니라, 탄환이 퍼지는 **산탄총(Shotgun)**의 원리를 연사력으로 구현하는 것입니다.
2. 다중 모드 혼합 사격 (Mixed Mode Aiming) - "심리전"
닌자 좀비는 "투사체가 날아오면 피한다"는 로직을 가집니다. 이를 역이용해 법사도 사격 방식을 섞습니다.
- 원리: 20발을 쏠 때 주사위를 굴려 섞어서 발사합니다.
- 30% 확률: 현재 적의 위치로 발사 (적이 멈출 것을 대비)
- 50% 확률: 적의 미래 예측 지점으로 발사 (적이 계속 뛸 것을 대비)
- 20% 확률: 아예 엉뚱한(좌우 랜덤) 곳으로 발사 (적이 피할 곳을 미리 점령)
- 효과: 닌자 좀비의 회피 AI가 어느 장단에 맞춰 피해야 할지 모르게 만듭니다. (피했는데 거기에 탄환이 이미 와 있는 상황 연출)
3. 지뢰형 잔류 사격 (Area Denial/Minefield) - "길막기"
시즈 모드의 투사체가 **속도가 매우 느리고(0.2) 지속시간이 길다(10초)**는 점을 극대화하는 전략입니다.
- 원리: 법사가 적을 직접 맞추려 하지 않고, 적과 자신 사이의 통로 전체에 탄환을 깔아버립니다.
- 효과: 닌자 좀비는 빠르기 때문에 움직임이 큽니다. 법사가 사방에 뿌려놓은 '느릿느릿한 탄환'들 사이를 지나오다가, 회피 기동을 하는 순간 옆에 떠 있던 다른 탄환에 부딪히게 됩니다.
- 비유: 적에게 직접 던지는 돌멩이가 아니라, 적 주변에 깔아두는 가시밭을 만드는 것입니다.
결론: 법사 V4에게 추천하는 수정 방향
현재 V4의 시즈 모드(performShot)에 **'약간의 오차(Spread)'**만 추가해도 닌자 좀비를 훨씬 잘 잡게 됩니다.
[수정 아이디어 - 코드 없이 설명]
- 예측 지점을 계산합니다.
- 거기에 -2.0에서 +2.0 사이의 랜덤한 숫자를 X와 Z 좌표에 더합니다.
- 발사합니다.
이렇게 하면 법사가 20발을 쏘는 동안 탄환들이 예측 지점 근처에 부채꼴이나 구름 모양으로 퍼지면서 날아갑니다. 닌자 좀비가 아무리 랜덤하게 피해도, 그 '랜덤한 범위' 전체에 탄환이 깔려 있다면 결국 한두 발은 맞을 수밖에 없습니다.
**"예측은 계산으로 하고, 명중은 확률(랜덤)로 만든다"**는 것이 닌자 좀비 같은 고기동 적을 잡는 마법사의 새로운 비법이 될 것입니다!
뭔가 아이디어가 다양하긴하네
체력바/체력 표시, 렙업할때마다, 체력+5

다시 선제타격안하는 문제
떨어져잇으면 인식못하나
근데 현재 공격받고나면, 도망5초, 공격5초하는데,
공격받기전에는 좀비(isAllyzombie타격을 안함
혹시 공격 조건이 도망 5초, 공격5초이기떄문인건가,
즉, 공격받기전에는 도망치기 조릭이 활성화되지않기때문인건가
즉, 선제타격은 공격이지 도망이 아닌거니깐,
그니깐, 좀비감지하면 공격이 아니라, 대충 도망 로직 실행하도록 해야하나,
[ ]
이게 어떻게 바뀐거지
이전에는 왜 안되엇던거고, 이전에 현재 어떤 상태인지 출력로그를 찍게 해볼껄 그랫나
수중 이동 보정: getWaterInertia() { return 1.0F; }를 유지하여 수중 전투 능력을 확보했습니다.
[ ok ]
왜 아군이 맞으면 같이 공격해주지
아맞다 법사3에서도 탐색거리 늘리는거 해줫던것같기도
1. 왜 한 대 맞으면 다 같이 반격하나요?
코드의 이 부분 때문입니다:
this.targetSelector.addGoal(1, new AllyAwareHurtByTargetGoal(this).setAlertOthers());
- HurtByTargetGoal: 나를 때린 놈을 타겟으로 잡는 AI입니다.
- .setAlertOthers(): 이게 핵심입니다. "주변에 있는 같은 종류의 엔티티(법사 V4)들에게도 범인이 누구인지 알리고 같이 싸우자!"라고 신호를 보내는 기능입니다.
- 그래서 한 마리만 맞아도 주변 법사들이 클래스 단위로 단체 반격을 시작하는 것입니다. (마치 좀비 피그맨이나 늑대 같은 군집 행동입니다.)
2. 왜 선제공격(탐색)을 전혀 못 하나요? (V3와 차이점)
V3와 코드는 거의 같지만, 마을 주민(Villager) 베이스 엔티티는 기본적으로 '탐색 거리(FOLLOW_RANGE)' 속성이 매우 짧게 설정되어 있을 수 있습니다.
탐색 AI는 돌아가고 있는데, 법사의 눈(속성 값)이 너무 낮아서 바로 앞의 적도 못 알아보는 상태일 확률이 높습니다. 이를 해결하려면 속성(Attribute)에서 탐색 거리를 강제로 늘려줘야 합니다.
//event.put(WizardEntityTypeInitV4.WIZARD_V4.get(), Villager.createAttributes().build());
event.put(WizardEntityTypeInitV4.WIZARD_V4.get(), WizardEntityV4.createAttributes().build());
이 두개가 차이를 만드나
확인범위를 로그로 찍어볼수잇게 할수잇나

[ ]
벽너머에잇으면 스캔이 안되나
딱히 그런건 아닌것 같은데,
스캔 범위가 어떻게 되지
탐색 성능 및 쿨타임 최적화 (질문 답변)
"도망이거나 공격할 때는 굳이 탐색할 필요 없는데, 탐색 안 하게 했나?"
현재 마인크래프트 기본 AI 구조상, NearestAttackableTargetGoal은 타겟이 없을 때만 주로 작동하지만, **조건문(Predicate)**은 매 주기마다 돌아갑니다.
- 해결책: 타겟이 이미 있다면 주변 스캔 로직을 **즉시 종료(return false)**하도록 코드를 수정하면 CPU 성능을 아낄 수 있습니다.
- 탐색 쿨타임: 현재 checkInterval이 10으로 되어있어 0.5초마다 한 번씩 훑습니다. 성능상 적절하지만, 전투 중에는 이마저도 아낄 수 있습니다.
법사 V3는 감지를 잘하는데 V4가 감지를 못하는 이유는 코드상으로 거의 동일해 보이지만, 엔티티의 속성(Attribute) 설정이나 AI의 상태 전이 로직에서 발생하는 미세한 차이 때문일 가능성이 매우 높습니다.
제공해주신 코드를 분석한 결과, V4의 감지력을 떨어뜨리는 주요 원인 3가지와 해결 방안을 정리해 드립니다.
1. 가장 유력한 원인: FOLLOW_RANGE 속성 누락
Minecraft의 모든 엔티티는 '추적 거리(Follow Range)' 속성을 가집니다. NearestAttackableTargetGoal은 이 속성 값 안의 범위에서만 대상을 탐색합니다.
현재 WizardEventsV4에서 사용하는 속성 설정은 다음과 같습니다:
event.put(WizardEntityTypeInitV4.WIZARD_V4.get(), Villager.createAttributes().build());
문제점: Villager는 기본적으로 공격적인 속성이 없으며, FOLLOW_RANGE가 매우 낮게 설정되어 있거나 정의되지 않았을 수 있습니다. 반면 V3가 잘 작동한다면 V3의 기본 클래스나 어딘가에서 이 값이 더 높게 잡혀있을 가능성이 큽니다.
[해결책] V4의 속성 설정에 명시적으로 추적 거리를 추가하세요.
// WizardEventsV4 클래스 내부
@SubscribeEvent
public static void onAttributeCreation(EntityAttributeCreationEvent event) {
event.put(WizardEntityTypeInitV4.WIZARD_V4.get(),
Villager.createAttributes()
.add(net.minecraft.world.entity.ai.attributes.Attributes.FOLLOW_RANGE, 32.0D) // 32칸까지 감지
.build());
}
2. AI 목표(Goal) 충돌 및 시즈모드 고정 문제
V4에 추가된 WizardSkirmishGoalV4에는 V3와 다른 isSiegeMode 로직이 있습니다.
// WizardSkirmishGoalV4.java
@Override
public void start() {
this.isSiegeMode = true; // 시작하자마자 시즈모드
this.stateTimer = 100;
}
@Override
public void tick() {
if (isSiegeMode) {
this.wizard.getNavigation().stop(); // 움직임 멈춤
// ... 사격 로직
}
}
문제점:
- NearestAttackableTargetGoal이 대상을 한 번이라도 놓치면 AI가 초기화됩니다. [ ]
- 만약 주변 좀비가 법사의 사거리(15칸)보다 살짝 먼 16~20칸 사이에 있다면, V4는 시즈모드 때문에 움직이지 않고 가만히 서 있게 됩니다.
- 사용자 입장에서는 **"움직이지도 않고 좀비를 쳐다만 보거나 무시한다"**고 느껴져 감지를 못 하는 것처럼 보일 수 있습니다.
[해결책] canUse() 조건에서 좀비가 너무 멀면 시즈모드보다 추격 모드가 먼저 발동되도록 우선순위를 점검하거나, FOLLOW_RANGE를 공격 사거리보다 확실히 크게 잡아야 합니다.
3. NearestAttackableTargetGoal의 mustSee 옵션
두 버전 모두 아래와 같이 설정되어 있습니다:
new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, true, false, ...)
네 번째 인자인 **true**는 **"시야에 직접 보여야만 타겟팅함"**을 의미합니다.
- V4의 모델 크기나 눈 높이(Eye Height): V4의 모델이 V3와 미세하게 다르거나, 렌더링 위치 때문에 '눈(Eye)'의 위치가 블록에 파묻혀 있다면 mustSee가 true일 때 대상을 전혀 감지하지 못합니다.
- 테스트 환경: V4를 소환한 주변에 풀이나 반블록 등 시야를 가리는 요소가 있다면 감지를 못 할 수 있습니다.
[해결책] 테스트를 위해 mustSee를 false로 바꿔서 감지가 되는지 확인해 보세요.
this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, LivingEntity.class, 10, false, false, ...));
가장 큰 차이점: 타겟 판정 로직 (범인)
- 법사3 (V3):
- 설명: ZombieEnemyHandler라는 별도의 클래스를 통해 이 좀비가 공격해야 할 "적"인지 아닌지를 광범위하게 체크합니다. 일반 좀비나 특정 조건의 좀비들을 모두 포함할 가능성이 높습니다.ava
(target) -> ZombieEnemyHandler.isEnemyZombie(target)
- 설명: ZombieEnemyHandler라는 별도의 클래스를 통해 이 좀비가 공격해야 할 "적"인지 아닌지를 광범위하게 체크합니다. 일반 좀비나 특정 조건의 좀비들을 모두 포함할 가능성이 높습니다.ava
- 법사4 (V4):
- 설명: 오직 IZombieAlly 인터페이스를 직접 상속받은 개체만 타겟팅합니다.
- 문제점: 만약 테스트 중인 좀비가 그냥 일반 마인크래프트 좀비(Zombie)이거나, IZombieAlly를 구현하지 않은 다른 모드의 좀비라면 법사4는 걔네들을 아예 "투명 인간" 취급합니다.
(target) -> target instanceof IZombieAlly
[해결책] 법사4의 타겟 로직도 법사3과 동일하게 ZombieEnemyHandler를 사용하도록 수정하세요.
맞다 젤처음에 법사3도 이 문제 가지고잇엇엇지
네, 결론부터 말씀드리면 똑같이 동작합니다. 그리고 결과가 완벽히 같음에도 IDE(IntelliJ 등)에서 이를 추천하는 이유는 가독성과 코드의 깔끔함 때문입니다.
1. 왜 똑같이 동작하나요?
람다 식 (target) -> ZombieEnemyHandler.isEnemyZombie(target)를 분석해보면:
- target이라는 인자를 받아서
- ZombieEnemyHandler.isEnemyZombie라는 메서드에 그대로 전달하고
- 그 결과를 반환합니다.
이 과정이 매우 단순하기 때문에, 자바에서는 **"굳이 인자 이름을 정하고 화살표(->)를 쓸 필요 없이, 그냥 이 메서드를 가져다 써라"**라는 의미로 ::(메서드 참조) 문법을 제공합니다.
- 람다: (target) -> ZombieEnemyHandler.isEnemyZombie(target)
- 메서드 참조: ZombieEnemyHandler::isEnemyZombie
컴파일러 입장에서는 두 코드가 사실상 동일한 코드로 변환됩니다.
2. 왜 메서드 참조(::)를 추천하나요?
- 가독성 (Readability): 코드가 훨씬 짧아집니다. "타겟을 받아서 어쩌구 저쩌구..." 하는 과정보다 "이 기능을 사용한다"는 의도가 더 명확해집니다.
- 불필요한 변수명 제거: target이라는 이름을 지어줄 필요가 없습니다. (때로는 t, obj, entity 등 사람마다 다르게 이름을 짓는데, 메서드 참조는 이런 고민을 없애줍니다.)
- 성능상의 미세한 이점: 아주 미세한 차이지만, 람다는 내부적으로 익명 클래스나 추가적인 연결 고리를 만들 수 있는데, 메서드 참조는 이미 존재하는 메서드에 직접 연결되므로 실행 시 조금 더 효율적일 수 있습니다. (체감될 정도는 아닙니다.)
3. 적용 방법
아래와 같이 바꾸셔도 법사4는 법사3과 똑같이 좀비를 잘 감지할 것입니다.
this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(
this,
LivingEntity.class,
10,
true,
false,
ZombieEnemyHandler::isEnemyZombie // 메서드 참조 방식
));
요약
- 동작: 100% 동일함.
- 추천 이유: 코드가 더 깔끔해 보이고 현대적인 자바 코딩 스타일이기 때문.
- 결정: ZombieEnemyHandler::isEnemyZombie라고 써도 법사4의 감지 기능에는 아무런 문제가 없으니 안심하고 바꾸셔도 됩니다!
tick() 메소드 하는일이 잇나
- 부모 클래스가 대신 일함: tick()을 삭제하면, 이 엔티티는 부모 클래스인 AbstractAllyEntity나 마인크래프트 기본 엔티티의 tick() 로직을 그대로 사용하게 됩니다. (현재 코드에서도 super.tick()을 호출하고 있으므로 결과적으로 똑같습니다.)
- 전투 AI는 별개임: 좀비를 감지하고 공격하는 기능은 registerGoals()에 등록된 AI(WizardSkirmishGoalV4)가 담당하므로, tick() 메소드와는 상관없이 잘 작동합니다.
도망/공격중일땐 탐색할 필요없으니깐
return 반환하거나 그렇게 하도록
성능이 얼마나 더 좋아질수잇으려나
만약 보스몹으로 잡고싶다면,
특정 장소 주위를 맴돌게 하거나 해야할것같은데
이제 선제 타격함
도망가거나 공격 중일 때(즉, 이미 타겟이 있을 때)는 새로운 타겟을 찾지 않도록 하는 것은 매우 좋은 성능 최적화 아이디어입니다.
마인크래프트의 NearestAttackableTargetGoal은 기본적으로 범위 내의 모든 엔티티를 매번 리스트로 불러와서(Scanning) 조건을 대조하기 때문에, 이미 싸우고 있을 때 이 과정을 생략하면 CPU 사용량을 줄일 수 있습니다.
1. 왜 registerGoals에 if문을 쓰면 안 되나요?
registerGoals()는 엔티티가 처음 태어날 때 딱 한 번만 실행됩니다. 따라서 여기에 if문을 넣으면 태어날 때의 상태로 고정되어 버립니다.
2. 더 좋은 방법: canUse() 오버라이드
가장 효율적인 방법은 NearestAttackableTargetGoal을 생성할 때, **"이미 타겟이 있으면 이 AI 자체를 실행하지 마라"**라고 내부 로직을 살짝 비트는 것입니다. 이렇게 하면 리스트를 불러오는 무거운 작업 자체를 건너뛰게 됩니다.
성능이 얼마나 좋아지나요?
- 탐색 연산 절감: 마인크래프트에서 NearestAttackableTargetGoal은 실행될 때마다 level.getEntitiesOfClass라는 메소드를 호출합니다. 이건 주변 수십 칸 내의 모든 개체를 리스트로 뽑아내는 무거운 작업입니다.
- 교전 중 최적화: 위 코드를 쓰면 법사가 적 한 명을 포착해서 싸우는 동안(도망치거나 쏠 때)에는 이 무거운 작업을 아예 건너뜁니다.
- 대규모 전투 시 효과: 법사가 10명, 좀비가 30명 있을 때, 원래라면 법사 10명이 매번 30마리를 스캔해야 하지만, 이 코드를 적용하면 이미 타겟을 잡은 법사들은 연산을 멈춥니다. 서버 렉(TPS 저하) 방지에 큰 도움이 됩니다.
동작 로직 (Boolean 변수 없이 처리)
- 따로 boolean 변수를 만들 필요가 없는 이유는 this.getTarget()이 이미 그 역할을 수행하기 때문입니다.
- getTarget() != null 이라는 것은 법사가 공격 모드이거나 도망 모드(SkirmishGoal 작동 중)라는 뜻입니다.
- 타겟이 죽으면 자동으로 getTarget()이 null이 되고, 그때 다시 canUse()가 true가 되면서 주변 스캔을 시작합니다.
이 방식이 가장 마인크래프트답고(Vanilla-friendly), 성능상으로도 가장 깔끔한 해결책입니다.
registerGoals() vs canUse() 차이점
registerGoals() (태어날 때 딱 한 번)
- 실행 시점: 엔티티(법사)가 세상에 스폰될 때 딱 한 번 실행됩니다.
- 역할: "너는 앞으로 이런 인공지능(Goal)들을 가질 거야"라고 목록을 등록하는 곳입니다.
- 만약 여기에 if문을 쓰면:
- 태어날 때 타겟이 없으면(당연히 없겠죠?) AI를 아예 등록조차 안 해버립니다.
- 그럼 그 법사는 평생 동안 좀비를 찾는 기능이 없는 채로 살아가게 됩니다.
canUse() (매 틱 혹은 자주)
- 실행 시점: 등록된 AI들이 내부적으로 **"나 지금 일 시작해도 돼?"**라고 매번 체크할 때 실행됩니다.
- 역할: AI를 실행할지 말지 실시간으로 결정합니다.
- 여기에 if문을 쓰면:
- "타겟이 있네? 그럼 새로운 타겟 찾는 일은 하지 말자(return false)"라고 매 순간 유연하게 판단합니다.
성능향상부분
코드해석및 잘되는지
구두(말로 하는 설명)로 아주 쉽게 설명해 드릴게요.
질문하신 대로 canUse()는 사실상 거대한 if 문이라고 생각하시면 정확합니다. 다만, 내가 직접 if를 쓰는 게 아니라, 게임 엔진이 나에게 "지금 이 행동(AI)을 시작해도 될까?"라고 물어보는 질문에 답하는 역할입니다.
1. canUse()의 역할: "출전 대기소의 게이트키퍼"
마인크래프트의 몹은 수많은 행동(Goal)을 가지고 있습니다. (걷기, 적 찾기, 공격하기, 도망가기 등)
마인크래프트 엔진은 매 순간(1초에 20번) 몹에게 이렇게 물어봅니다.
엔진: "야, '주변 적 탐색' AI! 너 지금 작동할 조건 되니?"
canUse(): (자기 안의 조건문을 확인한 뒤) "네(true)!" 또는 **"아니오(false)!"**라고 대답합니다.
- true를 리턴하면: 엔진이 "오케이, 그럼 이제 적을 찾아라!" 하고 해당 기능을 실행합니다.
- false를 리턴하면: 엔진이 "아직은 때가 아니군." 하고 그냥 지나칩니다.
2. 왜 조건문을 직접 안 쓰고 canUse 안에 넣나요?
사용자님 말씀대로 "만약 ~라면 실행해라"라는 코드를 짜고 싶은 건데, 마인크래프트 AI 시스템은 **"네가 직접 실행하지 말고, 조건(if)만 나(canUse)한테 알려줘. 실행은 내가 할게!"**라는 규칙을 정해둔 것입니다.
이 코드의 상황을 대화로 풀면 이렇습니다:
- 엔진: "적 탐색 AI님, 주변에 적 있는지 찾아볼까요?"
- canUse() 안의 코드: (계산해봄) "잠깐, 우리 주인님(마법사)이 이미 싸우고 있는 적이 있네? 게다가 그놈이 아직 살아있어."
- canUse(): "그럼 굳이 새로운 적 찾느라 고생할 필요 없지. 엔진님, **false**입니다! 탐색 작업 하지 마세요!"
- 엔진: "오케이, 탐색(스캔) 안 하고 CPU 성능 아낄게!"
1. canUse()는 누가 호출하나?
이걸 호출하는 주체는 마인크래프트 엔진의 **GoalSelector (골 셀렉터)**라는 관리자입니다.
- 몹(Entity)은 targetSelector라는 명단을 가지고 있습니다.
- 게임 엔진은 매 틱(1초에 20번)마다 이 명단을 쭉 훑으면서 명단에 적힌 Goal(행동)들에게 질문을 던집니다.
- 엔진: "명단 2순위에 있는 NearestAttackableTargetGoal님, 지금 실행해도 될까요? (canUse() 호출)"
2. "부모 대신 자식 거를 호출하나?" → YES!
이게 바로 자바의 오버라이딩(Overriding) 규칙입니다.
- 엔진은 이 객체가 "익명 클래스"인지 "원래 클래스"인지 상관하지 않습니다. 그냥 Goal이라는 타입으로 보고 canUse()를 부를 뿐입니다.
- 하지만 자바의 규칙상, 자식이 메서드를 재정의(Override)했다면, 무조건 자식의 메서드가 우선권을 가집니다.
- 따라서 엔진이 canUse()를 부르면, 부모(NearestAttackableTargetGoal)의 코드가 아니라 사용자가 새로 작성한 (익명 클래스의) canUse()가 호출됩니다.
3. super.canUse()의 의미 (매우 중요)
사용자님이 보신 코드 마지막에 return super.canUse();가 있죠? 이게 아주 중요한 역할을 합니다.
@Override
public boolean canUse() {
// 1. 내가 추가한 "최적화 조건" (커스텀 logic)
if (this.getTarget() != null && this.getTarget().isAlive()) {
return false; // 내 조건에 걸리면 여기서 바로 "실행 안 함" 종료
}
// 2. 내 조건에 안 걸렸다면?
// "원래 부모 클래스가 하던 복잡한 탐색 로직"을 마저 수행해라!
return super.canUse();
}
- 만약 super.canUse()를 안 쓴다면?
부모 클래스가 가진 "주변 10블록 안에 적이 있는지 스캔하는 기능" 자체를 아예 안 쓰겠다는 뜻이 됩니다. 그러면 이 AI는 아무것도 찾지 못하게 됩니다. - super.canUse()를 쓴다면?
"내가 앞에 적은 **특수 조건(최적화)**을 통과했을 때만, **원래 기능(주변 스캔)**을 수행해라"라는 뜻입니다.
GoalSelector는 canUse()사용중
1. 부모의 super.canUse()가 하는 일 (진짜 힘든 일)
NearestAttackableTargetGoal이라는 부모 클래스의 canUse() 안에는 사실 굉장히 복잡하고 무거운 코드가 들어있습니다.
- 주변 스캔: 주변 10블록(설정한 범위) 안에 있는 모든 엔티티를 다 뒤집니다.
- 조건 필터링: "얘가 좀비인가?", "얘가 살아있나?", "얘가 투명 상태는 아닌가?" 등을 일일이 대조합니다.
- 가장 가까운 놈 찾기: 조건에 맞는 애들이 여러 명이면 그중 누가 제일 가까운지 거리 계산을 합니다.
- 결과: 적을 찾았다면 true, 못 찾았다면 false를 리턴합니다.
2. 사용자님의 커스텀 canUse()가 하는 일 (가벼운 필터링)
사용자님이 추가한 윗부분의 코드는 아주 간단한 일만 합니다.
if (this.getTarget() != null && this.getTarget().isAlive()) {
return false; // "이미 싸우는 놈 있으니 부모님한테 찾으라고 시키지도 마!"
}
- 현재 상태 확인: "지금 내 타겟 변수에 누가 들어있나?" (단순 변수 체크)
- 결과: 이미 싸우는 중이면 바로 false를 뱉고 끝냅니다. 이때는 부모의 무거운 스캔 로직(super.canUse())이 아예 실행조차 안 됩니다. 이게 바로 성능 최적화의 핵심입니다.
3. 둘의 결합: 왜 super.canUse()를 마지막에 부르나?
자바 코드는 위에서 아래로 흐릅니다. 흐름을 따라가 보죠.
@Override
public boolean canUse() {
// [1단계: 내 필터]
// 싸우는 중이면 여기서 즉시 종료 (부모 호출 안 함)
if (this.getTarget() != null && this.getTarget().isAlive()) {
return false;
}
// [2단계: 부모의 로직 호출]
// 여기까지 내려왔다는 건 "현재 타겟이 없다"는 뜻!
// 그러니 이제 부모님께 "주변에 좀비 좀 찾아봐주세요"라고 부탁함.
return super.canUse();
}
만약 super.canUse()를 안 쓴다면?
- 코드 끝에 return true;를 적으면: 주변에 적이 있든 없든 엔진에게 "나 적 찾았어!"라고 거짓말을 하는 꼴이 됩니다. (에러 발생 가능)
- 코드 끝에 return false;를 적으면: 현재 타겟이 없을 때조차 적을 찾으려고 시도하지 않습니다. 즉, 이 몹은 평생 아무도 공격하지 않는 바보가 됩니다.
[ ]
물속공격 문제와
다이아좀비,
isally되도록 클래스 재구성하던지 해야겟음
사제
정신지배같은 효과내는 좀비도 만들수도
'모딩 > 마인크래프트 모드 개발 일지' 카테고리의 다른 글
| 맠모딩 ) 다른사람의 엔티티 모델 가져오기 (1) | 2025.12.27 |
|---|---|
| 마크 모딩) "천사강림" (1) | 2025.12.26 |
| 마크모딩) 선제 타격 ai/ 투사체 물리 판정/ 섹션 탐색 최적화 / 인터페이스 상속 구조 정리 (0) | 2025.12.25 |
| 마크모딩) 사용자정의 법사 만들기(3d 엔티티로) (0) | 2025.12.24 |
| 마크모딩) 스스로 발전하는 마을주민 만들기 #3 (0) | 2025.12.24 |