[ ]수정된 코드에서 왜 좀비 공격이 안됫는지 해결해야지
나중에 아군을 향해서, 물약던지는 타워도 만들수가 잇을듯
[ ] 타워는 스폰알로만 생성이 가능한가, 그러면, 사용자 정의 상인만들어서, 아주 값싼 비용으로,, 타워 스폰알 팔수잇도록 해줘야ㅕ
<주민 마을에 등장하도록
- 타겟 고집: 한 번 타겟을 잡으면 그놈이 죽거나 도망가기 전까지는, 옆에서 다른 좀비가 더 가까이 다가와도 원래 쏘던 놈만 계속 쏩니다. (매 순간 타겟을 새로 고침 하지는 않기 때문)
더 개선할점 잇나
(종류야, 나중에 추가하면 되고, 기능적으로 추가해야할점들)
[ ]힐토템, 일단 데미지 1을 주고, 아군 힐 시켜줌
<이 정도는 blockbench로 만들만 할지도
✅ 최종 결론 (한 문장)
타워는 LivingEntity로 만들고,
이동·AI는 다 끄고,
타겟 탐색은 마크가 이미 제공하는 getNearestEntity 같은 메소드 쓰면 된다.
이게 끝임.
🔹 왜 LivingEntity냐
- 좀비는 LivingEntity만 공격함
- 피격 / 어그로 / 데미지 / 전투 로직 전부 자동으로 해결됨
- 블록으로 만들면 이걸 전부 수작업해야 함 → 비효율
👉 그래서 LivingEntity가 정답
🔹 “렉 걸리지 않냐?”에 대한 최종 답
- ❌ 이동 없음
- ❌ Pathfinding 없음
- ❌ Wander / Look Goal 없음
➡️ 사실상 블록이랑 비슷한 비용
타워 100개 있어도 문제 거의 없음
🔹 이동 안 하게 하는 핵심 코드
또는
🔹 회전 / 공격도 문제 없음
- yaw / pitch 직접 계산해서 회전
- 그 방향으로 투사체 발사
🔹 “구조물로 만들고 싶었는데?”에 대한 정리
- 구조물(블록) = 성능은 좋음
- 하지만 공격/피격/어그로 구현 난이도 폭증
👉 LivingEntity로 만드는 게 제일 단순 + 안정
🧠 한 줄 요약
타워 = “움직이지 않는 LivingEntity”
타겟 탐색 = 마크 기본 nearest 메소드
다중상속 / 구조물 집착 → 버려도 됨
3. 서버 통신이 필요한가?
마인크래프트는 **"서버가 모든 로직을 결정하고, 클라이언트는 보여주기만 한다"**는 원칙을 따릅니다.
- 서버 (Server Side): 타겟 탐색, 발사체 생성, 체력 계산, 데미지 입히기. (거의 모든 로직)
- 클라이언트 (Client Side): 타워가 회전하는 애니메이션, 투사체가 날아가는 그래픽 효과, 타워 클릭 시 UI 표시.
통신이 필요한 순간:
- 커스텀 UI: 타워를 우클릭해서 "공격력 업그레이드" 버튼을 눌렀을 때, "이 타워 레벨 올려줘!"라고 서버에 신호를 보낼 때 (Packet 사용).
- 특수 이펙트: 타워가 레이저를 쏜다면, 서버에서 "여기서 여기까지 레이저 그려!"라고 클라이언트에 패킷을 쏴줘야 합니다.
isPushable() = false로 설정해 좀비가 밀어도 밀리지 않는 "구조물" 같은 특성을 가집니다.
- 확장: 나중에 TowerAttackGoal의 shoot() 코드만 수정하면 화살을 쏘거나, 불을 뿜거나, 번개를 내리치는 타워로 쉽게 바꿀 수 있습니다.
주의: TowerAttackGoal에서 MagicProjectileEntity를 사용하므로, 법사2 패키지의 해당 파일이 정상적으로 있어야 에러가 나지 않습니다. 만약 패키지 경로가 다르다면 import 문을 확인해 주세요.

이 현상은 좀비에게 맞았을 때 발생하는 '넉백(Knockback)' 때문에 그렇습니다.
비록 코드에서 이동을 막아놨더라도, 좀비에게 맞으면 아주 미세하게 뒤로 밀리거나 속도(Velocity)값이 발생합니다. 마인크래프트의 애니메이션 시스템은 **"속도가 0이 아니네? 그럼 걷고 있는 거구나!"**라고 판단해서 다리를 휘젓는 애니메이션을 재생해버리는 것이죠.
이를 해결하려면 두 가지를 추가해야 합니다.
- 넉백 저항 수치 최대화: 맞아도 아예 밀리지 않게 합니다.
- 애니메이션 강제 초기화: 설령 미세하게 움직임이 감지되어도 다리 움직임을 0으로 고정합니다.
- Attributes.KNOCKBACK_RESISTANCE, 1.0D:
- 이게 없으면 좀비가 타워를 칠 때마다 타워가 아주 조금씩 뒤로 밀립니다. 1.0(100%)으로 설정하면 바위처럼 그 자리에 고정됩니다.
- this.walkAnimation.setSpeed(0.0F):
- 마인크래프트 렌더러는 엔티티의 walkAnimation 값을 보고 다리를 얼마나 빨리 흔들지 결정합니다. 이걸 매 틱마다 0으로 덮어씌우면, 시스템이 아무리 "이 몹 움직이고 있어!"라고 소리쳐도 다리는 꿈쩍도 하지 않습니다.
this.walkAnimation
이 메소드없음
현재 사용 중인 마인크래프트 버전(1.18.2 또는 1.19.2로 추정)에서는 walkAnimation이라는 객체가 도입되기 전이라 그렇습니다.
이 버전에서는 walkAnimation 대신 **animationSpeed**와 **animationPosition**이라는 변수를 직접 사용합니다. 이 변수들은 Mob의 부모인 LivingEntity에 정의되어 있습니다.
심볼 'oAnimStep' 을(를) 해결할 수 없습니다
- 변수명 변경: walkAnimation.setSpeed(0) 대신 **this.animationSpeed = 0.0F**를 사용합니다.
- 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. 구현 전략 (생각해볼 점)
똑똑하게 만드는 방식도 두 가지 단계가 있습니다.
- 초급 똑똑함: "보이면 쏘고, 안 보이면 아예 안 쏜다." (가장 쉬움)
- 중급 똑똑함: "가장 가까운 적이 벽 뒤에 있으면, 그 적은 무시하고 보이는 적들 중에서 가장 가까운 놈을 찾아서 쏜다." (추천하는 방식)
마인크래프트 엔진에 내장된 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()를 실행하여 시야에 보이는 또 다른 적을 즉시 찾아내게 됩니다.
🧐 왜 이렇게 하면 해결되나요?
- 시야 체크는 자동으로 되나요?
- 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이므로 아무것도 안 건드려도 자동으로 벽 체크를 수행합니다.
- TargetingConditions의 test 메소드 하단을 보면 다음과 같은 코드가 있습니다:Java
- 람다 에러는 왜 고쳐졌나요?
- 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) 안에는 우리가 인터페이스를 쓸 수밖에 없게 만드는 핵심 데이터들이 들어있습니다.
- UUID와 Id: 월드에서 유일한 존재임을 증명하는 번호.
- BoundingBox (충돌박스): 투사체가 부딪힐 수 있는 물리적인 부피.
- 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. 계급도 (상속의 사슬)
우리가 만든 코드를 거꾸로 올라가 보면 이런 사슬이 연결되어 있습니다.
- TowerEntity extends PathfinderMob
- PathfinderMob extends Mob
- Mob extends LivingEntity
- 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. 두 번째 코드가 안 되는 이유 (문제점)
- TargetingConditions.forNonCombat()의 문제:
- forCombat()은 공격 대상을 찾을 때 사용하며, 기본적으로 "공격 가능한지"를 체크합니다.
- forNonCombat()은 보통 플레이어를 따라다니는 펫이나 마을 주민 같은 "비공격 대상"을 찾을 때 사용합니다. 공격 로직(Attack Goal)에서 사용하면 내부 필터링에 걸려 타겟을 못 잡을 수 있습니다.
- IZombieAlly 인터페이스 구현 확인:
- 공격하려는 좀비 객체가 코드상에서 public class MyZombie extends Zombie implements IZombieAlly와 같이 **IZombieAlly 인터페이스를 반드시 구현(implements)**하고 있어야 합니다. 만약 일반 좀비를 소환했다면 이 조건에 걸려 공격하지 않습니다.
- ignoreLineOfSight() 누락:
- 코드 주석에는 "벽 뒤에 있더라도 공격한다"고 적혀 있지만, TargetingConditions에 .ignoreLineOfSight()를 명시적으로 붙여주지 않으면 마인크래프트는 기본적으로 벽 너머의 적을 찾지 못합니다.
'모딩 > 마인크래프트 모드 개발 일지' 카테고리의 다른 글
| 마크 모딩) 26.1.31 (1) | 2026.01.31 |
|---|---|
| 마크 모딩) brutal boss처럼 (0) | 2026.01.30 |
| 맠모딩) 벌몹 (0) | 2025.12.28 |
| 마크) 1.18.2 게코립 설정 시 1.18.2가 아닌 1.18을 써야 하는 이유 (1) | 2025.12.28 |
| 맠모딩 ) 다른사람의 엔티티 모델 가져오기 (1) | 2025.12.27 |