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

마크 모딩) 디펜스 타워 만들기

kimchangmin02 2026. 1. 12. 14:18

[   ]수정된 코드에서 왜 좀비 공격이 안됫는지 해결해야지

나중에 아군을 향해서, 물약던지는 타워도 만들수가 잇을듯 

 

 

 

 

[  ] 타워는 스폰알로만 생성이 가능한가, 그러면, 사용자 정의 상인만들어서, 아주 값싼 비용으로,, 타워 스폰알 팔수잇도록 해줘야ㅕ 

<주민 마을에 등장하도록

 

 

 

  1. 타겟 고집: 한 번 타겟을 잡으면 그놈이 죽거나 도망가기 전까지는, 옆에서 다른 좀비가 더 가까이 다가와도 원래 쏘던 놈만 계속 쏩니다. (매 순간 타겟을 새로 고침 하지는 않기 때문)

 


더 개선할점 잇나 

(종류야, 나중에 추가하면 되고, 기능적으로 추가해야할점들) 

 

[  ]힐토템, 일단 데미지 1을 주고, 아군 힐 시켜줌

<이 정도는 blockbench로 만들만 할지도 

 

 

 

 

 

 

 


 

 

 

✅ 최종 결론 (한 문장)

타워는 LivingEntity로 만들고,
이동·AI는 다 끄고,
타겟 탐색은 마크가 이미 제공하는 getNearestEntity 같은 메소드 쓰면 된다.

이게 끝임.


🔹 왜 LivingEntity냐

  • 좀비는 LivingEntity만 공격함
  • 피격 / 어그로 / 데미지 / 전투 로직 전부 자동으로 해결됨
  • 블록으로 만들면 이걸 전부 수작업해야 함 → 비효율

👉 그래서 LivingEntity가 정답


🔹 “렉 걸리지 않냐?”에 대한 최종 답

  • ❌ 이동 없음
  • ❌ Pathfinding 없음
  • ❌ Wander / Look Goal 없음

➡️ 사실상 블록이랑 비슷한 비용

타워 100개 있어도 문제 거의 없음

 

 

 

🔹 이동 안 하게 하는 핵심 코드

 
@Override public void tick() { super.tick(); this.setDeltaMovement(Vec3.ZERO); }

또는

 
@Override public void travel(Vec3 vec) {}

🔹 회전 / 공격도 문제 없음

  • yaw / pitch 직접 계산해서 회전
  • 그 방향으로 투사체 발사

 

 

🔹 “구조물로 만들고 싶었는데?”에 대한 정리

  • 구조물(블록) = 성능은 좋음
  • 하지만 공격/피격/어그로 구현 난이도 폭증

👉 LivingEntity로 만드는 게 제일 단순 + 안정


🧠 한 줄 요약

타워 = “움직이지 않는 LivingEntity”
타겟 탐색 = 마크 기본 nearest 메소드
다중상속 / 구조물 집착 → 버려도 됨

 

 

 

 

3. 서버 통신이 필요한가?

마인크래프트는 **"서버가 모든 로직을 결정하고, 클라이언트는 보여주기만 한다"**는 원칙을 따릅니다.

  • 서버 (Server Side): 타겟 탐색, 발사체 생성, 체력 계산, 데미지 입히기. (거의 모든 로직)
  • 클라이언트 (Client Side): 타워가 회전하는 애니메이션, 투사체가 날아가는 그래픽 효과, 타워 클릭 시 UI 표시.

통신이 필요한 순간:

  1. 커스텀 UI: 타워를 우클릭해서 "공격력 업그레이드" 버튼을 눌렀을 때, "이 타워 레벨 올려줘!"라고 서버에 신호를 보낼 때 (Packet 사용).
  2. 특수 이펙트: 타워가 레이저를 쏜다면, 서버에서 "여기서 여기까지 레이저 그려!"라고 클라이언트에 패킷을 쏴줘야 합니다.

 

 


 

 

isPushable() = false로 설정해 좀비가 밀어도 밀리지 않는 "구조물" 같은 특성을 가집니다.

 

 

  1. 확장: 나중에 TowerAttackGoal shoot() 코드만 수정하면 화살을 쏘거나, 불을 뿜거나, 번개를 내리치는 타워로 쉽게 바꿀 수 있습니다.

주의: TowerAttackGoal에서 MagicProjectileEntity를 사용하므로, 법사2 패키지의 해당 파일이 정상적으로 있어야 에러가 나지 않습니다. 만약 패키지 경로가 다르다면 import 문을 확인해 주세요.

 

 

 

 

이 현상은 좀비에게 맞았을 때 발생하는 '넉백(Knockback)' 때문에 그렇습니다.

비록 코드에서 이동을 막아놨더라도, 좀비에게 맞으면 아주 미세하게 뒤로 밀리거나 속도(Velocity)값이 발생합니다. 마인크래프트의 애니메이션 시스템은 **"속도가 0이 아니네? 그럼 걷고 있는 거구나!"**라고 판단해서 다리를 휘젓는 애니메이션을 재생해버리는 것이죠.

이를 해결하려면 두 가지를 추가해야 합니다.

  1. 넉백 저항 수치 최대화: 맞아도 아예 밀리지 않게 합니다.
  2. 애니메이션 강제 초기화: 설령 미세하게 움직임이 감지되어도 다리 움직임을 0으로 고정합니다.

 

 

 

 

 

 

  1. Attributes.KNOCKBACK_RESISTANCE, 1.0D:
    • 이게 없으면 좀비가 타워를 칠 때마다 타워가 아주 조금씩 뒤로 밀립니다. 1.0(100%)으로 설정하면 바위처럼 그 자리에 고정됩니다.
  2. this.walkAnimation.setSpeed(0.0F):
    • 마인크래프트 렌더러는 엔티티의 walkAnimation 값을 보고 다리를 얼마나 빨리 흔들지 결정합니다. 이걸 매 틱마다 0으로 덮어씌우면, 시스템이 아무리 "이 몹 움직이고 있어!"라고 소리쳐도 다리는 꿈쩍도 하지 않습니다.

 

 

 

 

 

 

this.walkAnimation

이 메소드없음

 

현재 사용 중인 마인크래프트 버전(1.18.2 또는 1.19.2로 추정)에서는 walkAnimation이라는 객체가 도입되기 전이라 그렇습니다.

이 버전에서는 walkAnimation 대신 **animationSpeed**와 **animationPosition**이라는 변수를 직접 사용합니다. 이 변수들은 Mob의 부모인 LivingEntity에 정의되어 있습니다.

 

 

 

심볼 'oAnimStep' 을(를) 해결할 수 없습니다

 

 

 

  1. 변수명 변경: walkAnimation.setSpeed(0) 대신 **this.animationSpeed = 0.0F**를 사용합니다.
  2. oAnimStep 추가: 마인크래프트는 부드러운 애니메이션을 위해 "이전 틱의 위치"와 "현재 틱의 위치"를 보간(Interpolation)해서 사용합니다. oAnimStep까지 0으로 만들어야 완벽하게 멈춥니다.

 

이 버전에서 메소드이름이 뭔지 확인하려면 

 

 

 

 

  • animationSpeed & animationSpeedOld: 마인크래프트는 이 두 수치를 보고 "지금 몹이 이동 중이네? 다리를 흔들어야지!"라고 판단합니다. 둘 다 0이면 멈춰있다고 인식합니다.
  • animationPosition: 다리가 어느 각도만큼 벌어져 있는지를 나타냅니다. 0이면 차렷 자세입니다.
  • animStep & animStepO: 애니메이션이 진행되는 단계를 계산하는 중간 값들입니다. 이것들까지 0으로 밀어버리면 미세한 "떨림(Jittering)"조차 사라집니다.

 

 

 

 

다리 안움직이니깐 더 타워같아졋네

 

 

 

 

1. 벽에 막혔을 때의 현재 상태 (Dumb Tower)

현재 코드는 타워가 **'엑스레이 시야'**를 가진 것과 같습니다.

  • 인식: 벽 뒤에 좀비가 있어도 15블록 안에만 있으면 "어! 적이다!" 하고 조준합니다.
  • 발사: 투사체를 발사하지만, 투사체가 날아가다가 타워 바로 앞의 벽에 부딪혀 사라지거나 박혀버립니다.
  • 결과: 공격은 실패하고 쿨타임(2초)만 돌아갑니다.

2. 똑똑하게 만들려면? (시야 체크: Line of Sight)

타워가 "내 눈에 좀비가 실제로 보일 때만 쏘겠다"라고 판단하게 하려면 '시야 체크(Raytracing)' 로직이 들어가야 합니다.

  • 원리: 타워의 눈 위치에서 좀비의 눈 위치까지 가상의 직선을 긋습니다. 그 사이에 '공기'가 아닌 '블록(돌, 흙 등)'이 있는지 확인하는 과정입니다.
  • 장점: 벽 뒤에 있는 적에게 헛발질하지 않습니다. 타워가 더 효율적으로 작동합니다.

3. 연산량이 많이 늘어날까?

결론부터 말씀드리면, "전혀 걱정하실 필요 없습니다."

  • 길 찾기(Pathfinding) vs 시야 체크(Raytracing):
    • 길 찾기: 좀비가 타워까지 오는 복잡한 미로를 계산하는 것 (매우 무거움).
    • 시야 체크: 단순히 두 점 사이에 직선 하나 그어보는 것 (매우 가벼움).
  • 비교: 마인크래프트에는 이미 수많은 몹(스켈레톤, 가디언 등)이 매 순간 이 시야 체크를 하고 있습니다. 타워 100개가 동시에 시야 체크를 해도 컴퓨터는 눈 하나 깜짝하지 않을 정도의 연산량입니다.

4. 구현 전략 (생각해볼 점)

똑똑하게 만드는 방식도 두 가지 단계가 있습니다.

  1. 초급 똑똑함: "보이면 쏘고, 안 보이면 아예 안 쏜다." (가장 쉬움)
  2. 중급 똑똑함: "가장 가까운 적이 벽 뒤에 있으면, 그 적은 무시하고 보이는 적들 중에서 가장 가까운 놈을 찾아서 쏜다." (추천하는 방식)

 

 

 

마인크래프트 엔진에 내장된 checkLineOfSight() 기능을 활용하면 복잡한 수학 계산 없이 아주 쉽게 구현할 수 있습니다

 

 

 

 

checkLineOfSight'이(가) 'net.minecraft.world.entity.ai.targeting.TargetingConditions'에서 private 액세스를 가집니다

변환할 수 없는 타입; '<lambda parameter>'을(를) 'changmin.myMod.feature.zombie_ally.IZombieAlly'(으)로 형 변환할 수 없습니다

 

 

 

1. .checkLineOfSight() (TargetingConditions 안에서)

  • 역할: getNearestEntity가 주변을 검색할 때, 단순히 거리만 재는 게 아니라 **"나랑 얘 사이에 블록이 막혀있나?"**를 동시에 검사합니다.
  • 효과: 만약 1m 앞에 좀비A가 벽 뒤에 있고, 5m 앞에 좀비B가 벌판에 있다면, 기존에는 좀비A를 선택했겠지만 이제는 좀비B를 선택합니다.

2. !this.tower.getSensing().hasLineOfSight(target) (tick 안에서)

  • 역할: 타겟을 정하고 조준(2초)하는 동안 적이 벽 뒤로 숨었는지 감시합니다.
  • 효과: 적이 벽 뒤로 사라지면 즉시 현재 타겟을 버립니다(null). 그러면 다음 틱에 타워는 다시 canUse()를 실행하여 시야에 보이는 또 다른 적을 즉시 찾아내게 됩니다.

 

 

 

 

🧐 왜 이렇게 하면 해결되나요?

  1. 시야 체크는 자동으로 되나요?
    • TargetingConditions test 메소드 하단을 보면 다음과 같은 코드가 있습니다:Java
      if (this.checkLineOfSight && p_26886_ instanceof Mob) {
          Mob mob = (Mob)p_26886_;
          if (!mob.getSensing().hasLineOfSight(p_26887_)) {
              return false; // 시야에 안 보이면 탈락!
          }
      }
    • TowerEntity Mob의 자식이고, checkLineOfSight는 기본이 true이므로 아무것도 안 건드려도 자동으로 벽 체크를 수행합니다.
  2. 람다 에러는 왜 고쳐졌나요?
    • entity -> ... 라고 쓰면 컴파일러는 entity Entity인지 LivingEntity인지 추측해야 합니다.
    • 하지만 (LivingEntity entity) -> ... 라고 직접 적어주면 컴파일러가 "아, 이 녀석은 LivingEntity구나! 그럼 당연히 IZombieAlly인지 instanceof로 검사할 수 있지!"라고 확신하게 됩니다.

 

 

 

 

 

호출하지 않아도 계속 true 상태를 유지합니다.

public class TargetingConditions {
   // ... 생략 ...
   private boolean checkLineOfSight = true; // <--- 이미 처음부터 true로 설정됨!

자바에서 변수를 선언할 때 뒤에 = true를 붙이면, 이 객체가 만들어지는 그 즉시(태어날 때부터) 이 값은 true가 됩니다.

즉, 우리가 TargetingConditions.forCombat()를 실행해서 새로운 타겟팅 규칙을 만들면, 따로 아무런 설정을 안 해도 이미 "시야 체크 기능"이 켜진 상태로 태어나는 것입니다.

 

 

  • checkLineOfSight라는 이름의 변수는 있습니다. (하지만 private이라 우리 눈엔 안 보임)
  • ignoreLineOfSight()라는 이름의 메소드는 있습니다. (시야 체크를 끄는 기능)
  • 하지만 checkLineOfSight()라는 이름의 메소드는 아예 존재하지 않습니다.

그래서 컴퓨터는 **"어? 시야 체크를 켜는 버튼(checkLineOfSight())은 안 만들어져 있는데 왜 자꾸 누르려고 해?"**라고 에러를 낸 것입니다.

 

 

 

 

 

 

 

 

 

 

 

 

  • Bus.MOD: 게임이 켜질 때 딱 한 번 실행되는 **"설정/등록"**용 (체력 설정, 모델 등록 등)
  • Bus.FORGE: 게임 플레이 중에 계속 작동하는 **"로직"**용 (죽음 체크, 데이터 저장 등)

 

 

 

 

 

 

 

 

 

 

 

어떤 코드가 엔티티를 엔티티답게 만드는가?

엔티티 클래스(부모인 Entity.class) 안에는 우리가 인터페이스를 쓸 수밖에 없게 만드는 핵심 데이터들이 들어있습니다.

  1. UUID Id: 월드에서 유일한 존재임을 증명하는 번호.
  2. BoundingBox (충돌박스): 투사체가 부딪힐 수 있는 물리적인 부피.
  3. position (위치): 월드 어디에 있는지 나타내는 좌표.

이 세 가지를 가지고 있는 건 오직 엔티티 클래스뿐입니다.
투사체(화살)가 물리적으로 부딪히는 로직(onHitEntity)은 내부적으로 다음과 같이 돌아갑니다:

// 마인크래프트 내부 코드 예시
protected void onHitEntity(EntityHitResult result) {
    Entity victim = result.getEntity(); // 물리적으로 부딪힌 '그 몸체'를 가져옴
    
    // 여기서 우리가 만든 인터페이스 체크가 들어가는 것!
    if (victim instanceof ITowerEntity) { 
        // 아군이네? 데미지 주지 말자!
    }
}

 

기술적 핵심: 월드 엔티티 리스트 (Level.entities)

마인크래프트 서버의 모든 월드(Level)는 현재 그 세상에 소환된 모든 생명체를 하나의 커다란 리스트로 관리합니다.

  • 이 리스트의 타입은 List<Entity> 입니다.
  • 좀비가 주변을 탐색할 때나, 화살이 날아가다 뭔가를 맞췄을 때, 마인크래프트 엔진이 꺼내오는 데이터는 무조건 Entity 객체입니다.

엔티티 클래스에 작성해야 하는 이유:
만약 화살이 타워에 부딪혔을 때, 마크 엔진은 코드로 이렇게 묻습니다.

"어이, 방금 화살에 맞은 '그 객체'(Entity)! 너 ITowerEntity라는 신분증 가지고 있어?"

이때 '그 객체'가 바로 우리가 만든 TowerEntity의 **인스턴스(실제 메모리에 떠 있는 몸체)**입니다. 렌더러나 아이템 클래스는 이 리스트에 들어있지도 않고, 부딪힐 수도 없습니다.

 

 

 

1. 계급도 (상속의 사슬)

우리가 만든 코드를 거꾸로 올라가 보면 이런 사슬이 연결되어 있습니다.

  1. TowerEntity extends PathfinderMob
  2. PathfinderMob extends Mob
  3. Mob extends LivingEntity
  4. LivingEntity extends Entity (최상위 부모)

자바(Java) 언어의 규칙상, 부모를 상속받으면 자식은 **"부모의 한 종류"**가 됩니다.

  • PathfinderMob Entity의 자식입니다.
  • 따라서 PathfinderMob을 상속받은 TowerEntity는 자동으로 Entity 타입이 되는 것입니다.

2. 마크 엔진이 인식하는 방식

마인크래프트 엔진(소스 코드)은 월드에 있는 물체들을 다룰 때 이렇게 생각합니다.

"난 이 물체의 이름이 TowerEntity인지 TowerEntity2인지 관심 없어. 하지만 이 녀석의 조상을 쭉 올라가 보니 Entity 클래스가 있네? 오케이, 그럼 넌 엔티티 리스트에 들어올 자격이 있어!"

이것을 프로그래밍 용어로 **다형성(Polymorphism)**이라고 부릅니다.

 

 

 

 

 

public static final RegistryObject<EntityType<TowerEntity>> TOWER = 
    ENTITY_TYPES.register("tower_entity", // <-- 이건 마크 "게임 안에서" 부를 ID
        () -> EntityType.Builder.of(TowerEntity::new, MobCategory.CREATURE) // <-- TowerEntity 클래스 연결
                .sized(0.6f, 1.95f)
                .build("tower_entity"));

1. ENTITY_TYPES.register("tower_entity", ...)

  • 의미: "마크야, 내 장부에 **tower_entity**라는 이름의 자리를 하나 만들어줘."
  • 비유: 주민등록등본에 이름을 올리는 것과 같습니다. 나중에 명령어로 /summon mymod:tower_entity라고 칠 때 쓰는 그 이름입니다.

2. () -> EntityType.Builder.of(TowerEntity::new, MobCategory.CREATURE)

여기가 가장 중요합니다. 캐릭터의 **'태생'**을 정합니다.

  • TowerEntity::new: "마크야, 이 캐릭터가 소환되어야 할 때 어떤 설계도를 쓸까?" -> **"우리가 만든 TowerEntity 클래스의 설계도를 써서 새로(new) 만들어!"**라는 뜻입니다. (이게 클래스와 연결되는 핵심 코드입니다.)
  • MobCategory.CREATURE: 이 녀석의 직업군입니다. "너는 괴물(Monster)이니? 아니면 동물/생물(Creature)이니?"를 정합니다. (스폰 규칙이나 소리 등에 영향을 줍니다.)

3. .sized(0.6f, 1.95f)

  • 의미: "이 캐릭터의 '피격 판정(히트박스)' 크기는 얼마큼이니?"
  • 해석: 가로 0.6블록, 세로 1.95블록이라는 뜻입니다. (딱 주민이나 플레이어 크기입니다.) 이게 있어야 화살이 맞았는지 안 맞았는지 계산할 수 있습니다.

4. .build("tower_entity")

  • 의미: "이제 신청서 다 썼으니까 도장 쾅 찍어서 완성해!"
  • 해석: 지금까지 설정한 모든 정보(클래스, 카테고리, 크기)를 묶어서 하나의 EntityType 객체로 최종 완성하는 명령입니다.

5. public static final RegistryObject<EntityType<TowerEntity>> TOWER

  • 의미: "이 신청서 통과되면, 나중에 내가 자바 코드 안에서 **TOWER**라는 이름으로 이 캐릭터 정보를 꺼내 쓸게."
  • 비유: 신청서 사본을 변수에 담아두는 것입니다. 나중에 스폰 알을 만들거나 할 때 이 TOWER 변수를 사용합니다

 

 

 

        Entity victim = event.getEntity();
        Entity attacker = event.getSource().getEntity();

 

 

 

 

1. Entity victim = event.getEntity();

  • 직역: 이벤트가 발생한 주인공(엔티티)을 가져와서 victim이라는 변수에 담아라.
  • 의미: **"피해자(맞은 놈)"**입니다.
  • 설명: event.getEntity()는 이 이벤트의 중심이 되는 존재를 말합니다. 누군가 아파서 "아악!" 소리를 냈다면, 그 소리를 낸 본인이 바로 victim이 됩니다.

2. Entity attacker = event.getSource().getEntity();

이 줄은 두 단계로 나누어 봐야 합니다.

  • 1단계: event.getSource()
    • "데미지의 원인(DamageSource)"을 가져옵니다. (예: 불, 낙하, 화살, 몹의 공격 등)
  • 2단계: .getEntity()
    • 그 원인을 제공한 **"주체(Entity)"**를 가져옵니다.
  • 의미: **"공격자(때린 놈)"**입니다.
  • 설명: 만약 좀비가 타워를 때렸다면, 여기서 attacker는 좀비가 됩니다. 만약 타워가 쏜 화살에 좀비가 맞았다면, 여기서 attacker는 타워가 됩니다.

 

 

 

 

 

 

타워들끼리 데미지를 안받는 모습

 

 

 

 

 

조준: "어떻게 바라보나?"

  • 타겟을 잡으면 타워의 몸과 머리가 그 적을 향해 실시간으로 돌아갑니다. (setLookAt)
  • 적이 움직이면 타워도 적을 따라 계속 회전하며 조준을 유지합니다.

 

 

 

 

 

적인식, 

그냥 monster만이지만, 

iZombieally도 추가(어차피 monster이니깐 상관없을지도 ) 

 

 

 

 

 

 

특정 인터페이스를 구현한 대상 잡고 싶을 때는 코드가 훨씬 단순해집니다.

하지만 기술적으로 한 가지 알아두어야 할 점이 있습니다. getNearestEntity 메소드는 매개변수로 **'클래스(Class)'**를 받습니다. IZombieAlly는 **'인터페이스(Interface)'**이기 때문에, 마인크래프트 엔진에게 "이 인터페이스인 놈들만 모아와!"라고 직접 시킬 수는 없습니다.

대신, PathfinderMob(대부분의 움직이는 생물)이나 LivingEntity 중에서 찾되, 필터에서 IZombieAlly인 놈만 통과시키는 방식이 가장 효율적입니다.

 

 

 

 

 

 

 

시야처리하고나서, 

타워가 좀비를 안쏨

일단 시야처리는 걍 나중에 하자 

 

 

 

뭔가 더 똑똑하게 만들려고 하면 할수록, 

자꾸 고쳐야하는 것들이 많이 생기는듯 

 

 

 

 

 

 

1. 주요 차이점 분석

항목 첫 번째 코드 (작동함) 두 번째 코드 (작동 안 함)
대상 클래스 Monster.class (모든 적대적 몹) LivingEntity.class (모든 생명체)
필터 조건 없음 (모든 몬스터 공격) entity instanceof IZombieAlly (이 인터페이스가 있는 것만)
타겟 조건 forCombat() (전투용) forNonCombat() (비전투용)
시야 체크 기본적으로 시야 체크 함 시야 체크를 안 하려고 했으나 설정이 미흡함

2. 두 번째 코드가 안 되는 이유 (문제점)

  1. TargetingConditions.forNonCombat()의 문제:
    • forCombat()은 공격 대상을 찾을 때 사용하며, 기본적으로 "공격 가능한지"를 체크합니다.
    • forNonCombat()은 보통 플레이어를 따라다니는 펫이나 마을 주민 같은 "비공격 대상"을 찾을 때 사용합니다. 공격 로직(Attack Goal)에서 사용하면 내부 필터링에 걸려 타겟을 못 잡을 수 있습니다.
  2. IZombieAlly 인터페이스 구현 확인:
    • 공격하려는 좀비 객체가 코드상에서 public class MyZombie extends Zombie implements IZombieAlly와 같이 **IZombieAlly 인터페이스를 반드시 구현(implements)**하고 있어야 합니다. 만약 일반 좀비를 소환했다면 이 조건에 걸려 공격하지 않습니다.
  3. ignoreLineOfSight() 누락:
    • 코드 주석에는 "벽 뒤에 있더라도 공격한다"고 적혀 있지만, TargetingConditions .ignoreLineOfSight()를 명시적으로 붙여주지 않으면 마인크래프트는 기본적으로 벽 너머의 적을 찾지 못합니다.