this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, false));// ^priority ^Goal 인스턴스
targetSelector — 엔티티가 누구를 공격할지 결정합니다.
this.targetSelector.addGoal(0, new HurtByTargetGoal(this));// 공격받으면 공격자를 타겟으로 설정this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Zombie.class, true));// 가장 가까운 Zombie를 자동 타겟
MeleeAttackGoal은 targetSelector가 타겟을 설정해야 실제로 동작합니다. 두 셀렉터가 협력해야 공격 AI가 완성됩니다.
public class FleeFromRainGoal extends Goal { private final Mob mob; public FleeFromRainGoal(Mob mob) { this.mob = mob; this.setFlags(EnumSet.of(Goal.Flag.MOVE)); } /** * 이 Goal을 시작할 수 있는지 매 틱 평가. * true를 반환하면 Goal이 활성화됩니다. */ @Override public boolean canUse() { return this.mob.level().isRaining() && this.mob.level().canSeeSky(this.mob.blockPosition()); } /** * Goal이 활성화된 동안 매 틱 호출. * 실제 행동 로직을 여기에 작성합니다. */ @Override public void tick() { // 비를 피해 가장 가까운 지붕 아래로 이동하는 로직 BlockPos shelter = findNearestShelter(); if (shelter != null) { this.mob.getNavigation().moveTo(shelter.getX(), shelter.getY(), shelter.getZ(), 1.2D); } } /** * Goal이 중단될 때 호출 (canUse() == false 또는 canContinueToUse() == false). * 상태 초기화를 여기서 처리합니다. */ @Override public void stop() { this.mob.getNavigation().stop(); } private BlockPos findNearestShelter() { // 구현 생략 — 실제로는 BlockPos 탐색 로직 return null; }}
Goal.Flag는 동시에 실행될 수 없는 Goal 그룹을 정의합니다. MOVE 플래그를 가진 Goal은 동시에 하나만 활성화됩니다.
// ❌ 두 Goal 모두 priority 1 → 무작위 한 쪽만 동작goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, false));goalSelector.addGoal(1, new RandomStrollGoal(this, 1.0D));
같은 priority를 가진 Goal은 동시에 동작을 시도해 충돌합니다. 어느 쪽이 실행될지 예측할 수 없습니다.
항상 고유한 priority 값을 사용하세요.
⚠️ 매 틱 새 Goal 등록
// ❌ tick() 안에서 Goal 등록 — 절대 금지@Overridepublic void tick() { this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, false));}
registerGoals()는 생성자에서 한 번만 호출됩니다. tick() 안에서 Goal을 추가하면 매 틱 중복 등록이 발생해 메모리 누수와 예측 불가능한 AI 동작을 유발합니다.