블록 이벤트 — use, onPlace, onRemove
커스텀 Block 클래스를 만들어 use(우클릭), onPlace(설치), onRemove(제거) 이벤트를 오버라이드하는 방법을 학습합니다. 사이드 분기 가드 패턴도 함께 다룹니다.
블록 이벤트 — use, onPlace, onRemove
기본 Block 클래스는 우클릭·설치·제거 같은 상호작용에 아무 반응도 하지 않습니다. 이 챕터에서는 Block을 상속해 커스텀 Block 클래스를 만들고, 세 가지 핵심 이벤트 메소드를 오버라이드하는 방법을 배웁니다.
1. 커스텀 Block 클래스 만들기
Block을 직접 상속하는 클래스를 새로 만듭니다. 파일 위치는 ExampleMod.java와 같은 패키지 안에 두면 됩니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/block/GreetingBlock.java
package com.example.examplemod.block;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
public class GreetingBlock extends Block {
public GreetingBlock(BlockBehaviour.Properties props) {
super(props);
}
// 플레이어가 빈손으로 블록을 우클릭할 때 호출 (26.1.2: use → useWithoutItem)
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
if (!level.isClientSide()) {
player.sendSystemMessage(Component.literal("Hello from block at " + pos));
}
return InteractionResult.SUCCESS;
}
// 블록이 세계에 설치될 때 호출 (시그니처 26.1.2 확인 완료)
@Override
protected void onPlace(BlockState state, Level level, BlockPos pos,
BlockState oldState, boolean movedByPiston) {
if (!level.isClientSide()) {
level.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_PLACE,
SoundSource.BLOCKS, 1.0f, 1.0f);
}
}
// 블록이 세계에서 제거된 뒤 이웃 블록을 갱신할 때 호출 (26.1.2: onRemove → affectNeighborsAfterRemoval, Level→ServerLevel)
@Override
protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level, BlockPos pos,
boolean movedByPiston) {
// BlockEntity를 사용했다면 여기서 정리
super.affectNeighborsAfterRemoval(state, level, pos, movedByPiston);
}
}super.affectNeighborsAfterRemoval(...) 호출을 빠뜨리면 BlockEntity가 있는 블록에서 데이터가 누수됩니다. 항상 super를 먼저 호출하거나, 정리 로직을 super 호출 전에 배치하세요.
2. 오버라이드 시점 표
| 메소드 | 호출 시점 | 주요 용도 |
|---|---|---|
useWithoutItem | 플레이어가 빈손으로 우클릭 | GUI 열기, 메시지 전송, 아이템 소비 |
onPlace | 블록 설치 시 | 사운드 재생, 이웃 블록 알림 |
affectNeighborsAfterRemoval | 블록 제거 후 | BlockEntity 정리, 드롭 처리 |
tick | 매 tick (등록 필요) | 타이머, 성장, 자동화 |
neighborChanged | 이웃 블록 변경 시 | 레드스톤 반응, 연쇄 업데이트 |
tick은 BlockState에 randomTicks(true) 또는 BlockBehaviour.Properties.randomTicks()를 설정해야 활성화됩니다.
3. InteractionResult 반환값
useWithoutItem 메소드는 반드시 InteractionResult를 반환해야 합니다. 반환값에 따라 게임이 다음 동작을 결정합니다.
| 값 | 의미 |
|---|---|
SUCCESS | 성공. 플레이어 손 흔들기 애니메이션 재생 |
CONSUME | 성공. 손 흔들기 애니메이션 없음 |
PASS | 이 핸들러를 건너뜀. 다음 핸들러로 전달 |
FAIL | 실패. 아무 동작도 하지 않음 |
아이템을 소비하는 상호작용(예: 물약 사용)에는 CONSUME을 씁니다. 단순 정보 표시에는 SUCCESS가 적합합니다.
4. 블록 등록에 커스텀 클래스 연결
ExampleMod.java에서 GreetingBlock을 등록합니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/ExampleMod.java
public static final DeferredBlock<GreetingBlock> GREETING_BLOCK = BLOCKS.registerBlock("greeting_block",
GreetingBlock::new,
p -> p.strength(2.0f));26.1.2에서는 BLOCKS.registerBlock(...)에 GreetingBlock::new를 팩토리로 넘기고, 마지막 람다에서 Properties(파라미터 p)에 속성을 체이닝합니다. DeferredBlock의 타입 파라미터도 GreetingBlock으로 맞춰 두면 나중에 캐스팅 없이 접근할 수 있습니다.
5. 사이드 분기 — 가드 패턴
⚠️ 양 사이드 호출 미고려
// ❌ 클라이언트와 서버 모두에서 메시지를 보내면 이중 출력 protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { player.sendSystemMessage(Component.literal("Hello!")); // 양 사이드 호출됨 return InteractionResult.SUCCESS; } // ✅ 서버에서만 실행 protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { if (!level.isClientSide()) { player.sendSystemMessage(Component.literal("Hello!")); } return InteractionResult.SUCCESS; }
useWithoutItem,onPlace,affectNeighborsAfterRemoval은 클라이언트와 서버 양쪽에서 모두 호출됩니다. 서버 전용 로직(메시지 전송, DB 저장, 아이템 지급)은 반드시if (!level.isClientSide())가드 안에 넣으세요.
반대로 파티클·사운드 재생은 클라이언트에서만 해야 할 때도 있습니다. 그럴 때는 if (level.isClientSide()) 블록을 씁니다.
6. 주의사항
- 무거운 작업 금지:
useWithoutItem·onPlace·affectNeighborsAfterRemoval은 게임 루프 메인 스레드에서 실행됩니다. 네트워크 호출·파일 I/O·긴 루프는 절대 넣지 마세요. 서버 TPS가 즉시 떨어집니다. - BlockEntity 없이 상태 저장 금지: 블록 자체에 필드를 추가해 상태를 저장하려는 시도는 동작하지 않습니다. 블록 인스턴스는 공유되기 때문입니다. 상태가 필요하면
BlockEntity를 사용하세요 (phase-4에서 다룹니다). affectNeighborsAfterRemoval에서super호출:BlockEntity가 없더라도super.affectNeighborsAfterRemoval(...)를 호출하는 습관을 들이세요. 나중에BlockEntity를 추가할 때 버그를 예방합니다.
정리
이 챕터에서 배운 내용:
Block을 상속해 커스텀 클래스를 만드는 방법useWithoutItem,onPlace,affectNeighborsAfterRemoval오버라이드 시점과 용도InteractionResult반환값의 차이if (!level.isClientSide())가드로 서버 전용 로직 분리
다음 챕터에서는 블록에 BlockEntity를 붙여 상태를 저장하는 방법을 다룹니다.