이 챕터에서는 블록에 데이터를 저장하는 BlockEntity를 처음으로 만들어 봅니다. 블록 자체(Block)는 상태(BlockState)만 저장할 수 있고, 임의의 데이터를 유지하려면 반드시 BlockEntity가 필요합니다. 카운터 값을 NBT로 저장하는 CounterBlockEntity를 예제로, 등록부터 직렬화까지 전체 흐름을 익힙니다.
게임 종료·청크 언로드 시 BlockEntity의 데이터를 NBT로 저장해야 세계 재진입 후에도 데이터가 유지됩니다.
// 26.1.2: loadAdditional(CompoundTag, HolderLookup.Provider) 삭제 → ValueInput 기반 @Override protected void loadAdditional(ValueInput input) { super.loadAdditional(input); // 저장된 값이 없으면 기본값 0 사용 count = input.getIntOr("count", 0); } // 26.1.2: saveAdditional(CompoundTag, HolderLookup.Provider) 삭제 → ValueOutput 기반 @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); output.putInt("count", count); }
메서드
호출 시점
역할
saveAdditional
청크 저장·게임 종료
필드 → ValueOutput 기록
loadAdditional
청크 로드·세계 진입
ValueInput → 필드 복원
NeoForge 26.1.2에서는 직렬화 시그니처가 바뀌었습니다. 26.1.2 이전의 loadAdditional(CompoundTag, HolderLookup.Provider) / saveAdditional(CompoundTag, HolderLookup.Provider)는 삭제되고, 추상화된 ValueInput / ValueOutput 기반 시그니처를 사용합니다. 값을 읽을 때는 input.getIntOr("count", 0)처럼 기본값을 함께 지정해 저장된 값이 없을 때도 안전하게 처리합니다. (net.minecraft.world.level.storage.ValueInput / ValueOutput 임포트가 필요합니다.)
블록에 BlockEntity를 결합하려면 블록 클래스가 BaseEntityBlock을 상속하거나 EntityBlock 인터페이스를 구현해야 합니다. NeoForge 26에서는 BaseEntityBlock 상속을 권장합니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/block/CounterBlock.javapublic class CounterBlock extends BaseEntityBlock { // 26.1.2: BaseEntityBlock 은 abstract codec() 구현을 요구한다. // Block.simpleCodec(생성자) 로 Properties 기반 MapCodec 을 만든다. public static final MapCodec<CounterBlock> CODEC = simpleCodec(CounterBlock::new); public CounterBlock(BlockBehaviour.Properties props) { super(props); } @Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; } @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new CounterBlockEntity(pos, state); }}
newBlockEntity 메서드는 블록이 세계에 놓일 때 호출되어 해당 위치의 BlockEntity 인스턴스를 생성합니다. 반드시 @Override로 구현해야 합니다. 또한 BaseEntityBlock은 추상 codec() 구현을 요구하므로, Block.simpleCodec(CounterBlock::new)로 Properties 기반 MapCodec을 만들어 반환합니다.
// ❌ EntityBlock 구현 안 함 → BlockEntity 안 만들어짐public class CounterBlock extends Block { // newBlockEntity 없음 → BlockEntityType.Builder.of(...) 가 호출 안 됨}// ✅ BaseEntityBlock 상속public class CounterBlock extends BaseEntityBlock { @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new CounterBlockEntity(pos, state); }}
블록 클래스에 extends BaseEntityBlock 또는 implements EntityBlock이 없으면 블록을 설치해도 BlockEntity 인스턴스가 생성되지 않습니다. COUNTER_BE 등록 자체는 성공해도 실제 BlockEntity가 없으므로 데이터가 저장·복원되지 않습니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/ExampleMod.javapublic static final DeferredBlock<CounterBlock> COUNTER_BLOCK = BLOCKS.registerBlock("counter", CounterBlock::new, p -> p.strength(1.5f));
registerBlock(name, 생성자, Properties 변형)은 NeoForge 26.1.2 패턴입니다. 내부적으로 Properties.setId(ResourceKey)를 호출해 블록 id를 설정하므로, 람다 밖에서 new CounterBlock(BlockBehaviour.Properties.of()...)처럼 직접 생성하면 "id not set" 크래시가 발생합니다.
COUNTER_BE에서 COUNTER_BLOCK.get()을 참조하므로, 선언 순서는 COUNTER_BLOCK → COUNTER_BE 여야 합니다. Java 정적 필드는 선언 순서대로 초기화되기 때문입니다.